Local State Exercise

A guided exercise in using DevTools to find unnecessary re-renders and fixing them by pushing state down.

March 20, 20252 min read3 / 3

The best way to internalise the "push state down" rule is to feel the difference with your own hands -- first watching everything flash in DevTools, then watching only the relevant components flash after the fix.

The Setup

Take any component where all state lives at the top level. The signature of the problem:

  • A parent component with multiple useState calls
  • Those state values passed as props through components that don't use them
  • Every state change triggers a full re-render of the tree

If you enable "Highlight updates when components render" and see the entire page flash when you type in a single input, you've found one.

The Exercise

Step 1 -- Measure. Record in the Profiler before touching anything. Note the total render time and which components appear in the flame graph for a single keystroke.

Step 2 -- Identify. Which state only affects a specific subtree? An input's draft value doesn't need to live in the root component if nothing outside the form reads it.

Step 3 -- Move it. Cut the useState from the parent. Paste it into the component that actually needs it. Delete the props that were threading it through.

Step 4 -- Measure again. Record the same interaction. Compare the flame graphs.

What Good Looks Like

After the refactor, the root component often ends up looking like this:

TSX
function App() { return ( <div> <Header /> <CreateThought /> {/* owns its own draft state */} <ThoughtList /> {/* doesn't know CreateThought exists */} </div> ); }

A root component with no props being passed down is a healthy sign. Each subtree owns what it needs. Changes in one don't cascade into others.

The Numbers in Context

After pushing state down, you might see render times like 1–2ms for an input change. The browser paints at 60fps -- that's a 16.6ms frame budget. A 1.3ms render means you have 15ms to spare.

At that point, stop optimising. It doesn't matter how many times something flashes in DevTools if the total time is imperceptible. You cannot see something that takes less than a single frame.

The goal isn't zero re-renders. It's correct re-renders that stay within budget.

The Pattern to Watch For

Pushing state down often reveals a second problem: callback functions defined inline are redefined on every render, giving child components new prop references each time -- defeating any React.memo you might add.

Pushing State Down covers this in detail with a code example. The fix is useCallback, which is the next topic.

Practice what you just read.

Colocate Form State
1 exercise