React Avoiding Unnecessary Re-renders

In the last lesson, React useCallback Deep Dive, you learned to keep functions stable so React.memo works. This lesson brings it together into practical patterns to avoid unnecessary re-renders, and the best fixes are often about structure, not memoization.

🤔 The goal, restated

Quick reminder of what we’re fixing here:

  • A parent re-renders, so all its children re-render with it, even the ones nothing changed for.
  • For heavy children, those extra renders waste work and can feel slow.
  • We want to keep the renders that matter and skip the ones that don’t.

So the goal is targeted: stop the needless, expensive re-renders, without overcomplicating the code.

⬇️ Pattern 1: move state down

Often the simplest fix is to put state where it’s actually used, instead of high up where it forces everything to re-render.

// ❌ state high up - typing re-renders the whole app, including the heavy list
function App() {
const [text, setText] = useState("");
return (
<div>
<input value={text} onChange={(e) => setText(e.target.value)} />
<HeavyList /> {/* re-renders on every keystroke for no reason */}
</div>
);
}
// ✅ state moved into a small component - only that part re-renders
function SearchBox() {
const [text, setText] = useState("");
return <input value={text} onChange={(e) => setText(e.target.value)} />;
}
function App() {
return (
<div>
<SearchBox />
<HeavyList /> {/* untouched by typing now */}
</div>
);
}

See why this works:

  • When state lives in App, every keystroke re-renders App, and so HeavyList too.
  • Move the text state into a small SearchBox, and only SearchBox re-renders as you type.
  • HeavyList is a sibling now, so the input’s state never touches it.

So before reaching for memo, ask: can this state live lower, closer to where it’s used? Moving it down often removes the problem entirely.

Restructure before you memoize

The cleanest performance fix is usually moving state down, not adding memoization. Keep state as local as possible. A component only re-renders when its own state changes, so small, focused state means fewer things re-render.

🎁 Pattern 2: pass heavy content as children

Here’s a clever one. If a component holds state but wraps heavy content that doesn’t depend on that state, pass the content in as children.

// the wrapper has state, but children come from the parent
function Wrapper({ children }) {
const [open, setOpen] = useState(false);
return (
<div>
<button onClick={() => setOpen(!open)}>Toggle</button>
{open && <p>Extra panel</p>}
{children} {/* passed in from outside - not re-created by this state */}
</div>
);
}
function App() {
return (
<Wrapper>
<HeavyList /> {/* created by App, so Wrapper's state doesn't re-render it */}
</Wrapper>
);
}

Why this avoids the re-render:

  • HeavyList is created in App, then handed to Wrapper as children.
  • When Wrapper’s open state changes, Wrapper re-renders, but children is the same element App already made.
  • So HeavyList is not re-created, right? It doesn’t re-render from Wrapper’s state change.

So the children prop is a quiet, built-in way to protect content from a wrapper’s frequent state changes, with no memo needed.

🧩 Pattern 3: split big components

A large component that mixes fast-changing and stable parts re-renders all of it on every change. Splitting it lets each part re-render on its own:

  • Put the part with frequent state in its own small component.
  • Keep the stable, heavy part in a separate component.
  • Now a change in one doesn’t force the other to re-render.

So splitting isn’t just for tidiness. It naturally limits what re-renders, because state changes only affect the component that owns them.

🛡️ Pattern 4: memo + stable props

When restructuring isn’t enough, use the tools from the last lessons together: React.memo on the heavy child, plus stable object and function props.

import { useCallback, memo } from "react";
const HeavyChild = memo(function HeavyChild({ onAction }) {
// expensive rendering here
return <button onClick={onAction}>Do it</button>;
});
function Parent() {
// stable function so memo can skip re-renders
const onAction = useCallback(() => console.log("action"), []);
return <HeavyChild onAction={onAction} />;
}

Here’s the combination at work:

  • HeavyChild is wrapped in memo, so it skips renders when props are unchanged.
  • onAction is wrapped in useCallback, so it stays the same function across renders.
  • Together, HeavyChild re-renders only when something it actually depends on changes.

So when you can’t restructure away the problem, memo plus stable props (useMemo/useCallback) is the reliable fix.

⚠️ Common Mistakes

The biggest mistake is jumping straight to memoization everywhere instead of fixing the structure.

// ❌ memo-ing everything to fix a problem that moving state down would solve
const A = memo(ComponentA);
const B = memo(ComponentB);
const C = memo(ComponentC); // scattershot, hard to maintain
// ✅ first move state down / split components, then memo only what still needs it

Keep these in mind:

  • Don’t memo everything blindly. Try moving state down or splitting first.
  • Don’t forget that memo needs stable props, or else it does nothing.
  • Don’t optimize without measuring. Confirm the re-render is real and slow first.

✅ Best Practices

A checklist for cutting re-renders, in order:

  • First, move state as low as possible, so changes affect the smallest area.
  • Pass heavy, unrelated content as children so a wrapper’s state doesn’t re-render it.
  • Split big components so fast-changing and stable parts are separate.
  • Only then, use React.memo with stable props (useMemo/useCallback) on what still re-renders needlessly.
  • Always measure with DevTools before and after.

Structure first, memo last

Reach for memoization last, not first. Moving state down and splitting components solve most re-render problems more simply and with cleaner code. Save memo and the hooks for what’s left over.

🧩 What You’ve Learned

  • ✅ The goal is to stop needless, expensive re-renders, not all re-renders
  • ✅ Move state down so changes only re-render the small part that uses it
  • ✅ Pass heavy content as children so a wrapper’s state changes don’t re-render it
  • ✅ Split big components so fast-changing and stable parts re-render independently
  • ✅ Use React.memo with stable props as the fix for what restructuring can’t solve
  • ✅ Restructure first, memoize last, and always measure

Check Your Knowledge

Test what you learned. Pick an answer for each question, then click Check.

  1. 1

    What is often the simplest way to avoid unnecessary re-renders?

    Why: Keeping state as local as possible means a change only re-renders the component that owns it, not the whole tree. This often removes the problem with no memoization.

  2. 2

    Why does passing heavy content as children avoid a re-render?

    Why: Content passed as children is created outside the wrapper. When the wrapper's state changes, that same element is reused, so the heavy child isn't re-rendered.

  3. 3

    In what order should you approach re-render problems?

    Why: Structure first, memo last. Moving state down and splitting components fix most cases with cleaner code. Use memo and the hooks for what restructuring can't solve.

  4. 4

    What does React.memo need to actually prevent re-renders?

    Why: memo compares props with ===. If you pass fresh objects or functions each render, it can't help. Stable props from useMemo/useCallback are what make memo work.

🚀 What’s Next?

Now you have the practical patterns for cutting wasted renders. To close the module, let’s gather all the performance habits into one clear checklist you can apply to any React app.

React Performance Best Practices

Share & Connect