Introduction — From Curiosity to Clarity
It started with a simple question: Why does a deeply nested React component keep re-rendering even when it shouldn’t? I was experimenting with a dashboard layout, and despite memoizing the leaf component, the console kept spitting out “Child renders.”
Coincidentally, I was also testing similar logic in a Flutter widget tree and ran into the same dilemma—uncontrolled rebuilds. So I committed to solving it by building controlled 3-level nesting render scenarios in both frameworks, studying how state, props, memoization, and context affect behavior.
This post captures that journey, organized as:
- Scenario 1: Direct state to parent
- Scenario 2: Context or provider consumption
- Scenario 3: Memoization or optimization attempts
Each scenario has sub-cases to dissect render behavior.
⚛️ React Render Scenarios — 3-Level Nesting
Structure
App → Parent → Middle → Child
Prop Drilling from App
1.1 – Simple Prop Cascade
function App() {
const [count, setCount] = useState(0);
return <>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
<Parent count={count} />
</>;
}
function Parent({ count }) { return <Middle count={count} />; }
function Middle({ count }) { return <Child count={count} />; }
function Child({ count }) {
console.log("Child renders with", count);
return <p>{count}</p>;
}
✅ Outcome: Every level re-renders on state change.
1.2 – Memoized Child
const Child = React.memo(({ count }) => {
console.log("Memoized Child renders:", count);
return <p>{count}</p>;
});
✅ Outcome: Child
skips re-render only if count
hasn’t changed.
1.3 – useMemo for JSX Stability
function Middle({ count }) {
const memoizedChild = useMemo(() => <Child count={count} />, [count]);
return memoizedChild;
}
✅ Outcome: JSX reused if count
is stable—Child
render skipped.
Context Consumption
2.1 – Context in Child
const ThemeContext = React.createContext();
function Child() {
const theme = useContext(ThemeContext);
console.log("Theme:", theme);
return <p>{theme}</p>;
}
⚠️ Outcome: Context change forces re-render—even if wrapped with React.memo
.
Mixed Optimization
3.1 – Memo + Context
const Child = React.memo(() => {
const theme = useContext(ThemeContext);
console.log("Memoized with theme:", theme);
return <p>{theme}</p>;
});
⚠️ Outcome: Still re-renders on context change. Memoization doesn’t isolate context.
🧾 React Summary
Scenario | Sub-case | Trigger | Behavior |
---|---|---|---|
1. Prop drilling | 1.1 | count changes | Full re-render |
1.2 | Stable props | Child skips render | |
1.3 | JSX memoization | Child JSX reused | |
2. Context usage | 2.1 | Context changes | Always re-renders |
3. Mixed use | 3.1 | Context with memo | Memo breaks on context change |
🐦 Flutter Render Scenarios — Widget Nesting
🌱 Structure
App → Parent → Middle → Child
Prop Drill via Constructor Parameters
1.1 – Stateless Widgets
class App extends StatefulWidget {
@override
State<App> createState() => _AppState();
}
class _AppState extends State<App> {
int count = 0;
Widget build(BuildContext context) {
return Column(
children: [
ElevatedButton(
onPressed: () => setState(() => count++),
child: Text("Increment"),
),
Parent(count: count),
],
);
}
}
class Parent extends StatelessWidget {
final int count;
const Parent({required this.count});
Widget build(BuildContext context) => Middle(count: count);
}
class Middle extends StatelessWidget {
final int count;
const Middle({required this.count});
Widget build(BuildContext context) => Child(count: count);
}
class Child extends StatelessWidget {
final int count;
const Child({required this.count});
Widget build(BuildContext context) {
print("Child builds with $count");
return Text('$count');
}
}
✅ Outcome: All widgets rebuild on setState
.
Provider Consumption
2.1 – Scoped Consumer in Child
class Child extends StatelessWidget {
const Child();
Widget build(BuildContext context) {
return Consumer<Counter>(
builder: (_, counter, __) {
print("Scoped rebuild");
return Text('${counter.count}');
},
);
}
}
✅ Outcome: Only Child
rebuilds—ancestor widgets remain untouched.
Optimization with const
3.1 – Const Constructors
class Middle extends StatelessWidget {
const Middle(); // No props
Widget build(BuildContext context) => const Child();
}
class Child extends StatelessWidget {
const Child();
Widget build(BuildContext context) {
print("Child builds");
return Container();
}
}
✅ Outcome: No rebuild unless explicitly triggered.
🧾 Flutter Summary
Scenario | Sub-case | Trigger | Behavior |
---|---|---|---|
1. Prop drilling | 1.1 | setState | Full tree rebuild |
2. Provider usage | 2.1 | notifyListeners() | Child-only rebuild |
3. Optimization | 3.1 | No state/props | Const prevents rebuild |
⚠️ Common Mistakes
- ❌ Memoizing React child with context inside
- ❌ Passing props without isolating updates
- ❌ Forgetting
useMemo
for JSX in React - ❌ Not using
Consumer
in Flutter’s leaf widgets - ❌ Avoiding
const
unnecessarily in Flutter
📦 Real-World Use Cases
Use Case | Platform | Optimization Tip |
---|---|---|
Product card grid | React | React.memo + useMemo for JSX |
Profile update screen | Flutter | Consumer for status updates |
Dynamic charts | React | Context isolation + memo |
Chat message list | Flutter | Scoped Consumer per bubble |
Blog tag rendering | React | Memoized tag JSX elements |
Reflection — Rendering Awareness Unlocks Performance
This deep dive reshaped how I approach UI. Whether I’m building a React SPA or Flutter mobile app, render isolation matters more than I imagined.
Memoization, context control, and scoped rebuilds aren’t “advanced”—they’re essential. Instead of blindly optimizing, ask yourself:
- What actually needs to rebuild?
- Can you memoize props or JSX?
- Are you leaking state downward unnecessarily?
Understanding render flows changes how you build—and how users experience your app.