Why Do Nested Components Keep Re-rendering? My Journey Through React and Flutter Optimization

Explore how deeply nested React and Flutter components behave during state updates—an engineer’s breakdown of render propagation, memoization, Consumers, and optimization patterns for scalable UIs.

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

ScenarioSub-caseTriggerBehavior
1. Prop drilling1.1count changesFull re-render
1.2Stable propsChild skips render
1.3JSX memoizationChild JSX reused
2. Context usage2.1Context changesAlways re-renders
3. Mixed use3.1Context with memoMemo 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

ScenarioSub-caseTriggerBehavior
1. Prop drilling1.1setStateFull tree rebuild
2. Provider usage2.1notifyListeners()Child-only rebuild
3. Optimization3.1No state/propsConst 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 CasePlatformOptimization Tip
Product card gridReactReact.memo + useMemo for JSX
Profile update screenFlutterConsumer for status updates
Dynamic chartsReactContext isolation + memo
Chat message listFlutterScoped Consumer per bubble
Blog tag renderingReactMemoized 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.

Leave a Reply

Your email address will not be published. Required fields are marked *