Back to React
React
medium
mid

How do unnecessary re renders happen in React and how do you avoid them?

Re-renders happen when state, props, or context changes. Unnecessary ones: parent re-renders cascade to children even when their props are identical; new object/array/function literals on every render bust React.memo; context value object recreated each render re-renders every consumer. Fix selectively: lift state down, split context, memoize stable references with useCallback/useMemo, use React.memo on expensive subtrees, profile before optimizing. Don't memoize everything — measure first.

8 min read·~5 min to think through

React re-renders a component when:

  1. Its own state updates (setState).
  2. A parent re-renders (cascade).
  3. A context it subscribes to changes.
  4. A subscribed store value changes (Redux, Zustand).

Unnecessary re-renders are when (2), (3), or (4) fire even though the visible output won't change. They're usually cheap individually, but at scale (long lists, deep trees) they add up to noticeable jank.

Common causes

1. Parent re-render cascades

jsx
function Parent() {
  const [open, setOpen] = useState(false);
  return (
    <>
      <button onClick={() => setOpen(o => !o)}>Toggle</button>
      <ExpensiveList items={items} />   {/* re-renders even though items didn't change */}
    </>
  );
}

Toggling open re-renders ExpensiveList even though its props are the same.

Fix A: React.memo so it only re-renders when props change (shallow compare):

jsx
const ExpensiveList = React.memo(function ExpensiveList({ items }) { … });

Fix B (often better): lift the open state down so the toggle doesn't live in the parent at all.

2. New literal objects/arrays/functions in JSX

jsx
<Child style={{ marginTop: 8 }} onClick={() => doStuff()} />

{ marginTop: 8 } and the arrow function are new references on every render → React.memo doesn't help because shallow compare fails.

Fix: hoist or memoize:

jsx
const childStyle = { marginTop: 8 };  // outside component if truly constant
const onClick = useCallback(() => doStuff(), []);
<Child style={childStyle} onClick={onClick} />

3. Context with object value

jsx
<ThemeContext.Provider value={{ theme, setTheme }}>

{ theme, setTheme } is a new object every render → every consumer re-renders on every parent render.

Fix: useMemo the value:

jsx
const value = useMemo(() => ({ theme, setTheme }), [theme]);
<ThemeContext.Provider value={value}>

Better: split into two contexts — one for the value, one for the setter. Setters rarely change identity, values do.

4. State up too high

If the entire app re-renders whenever a search input updates, search state is too high in the tree. Move it into a smaller component that contains only what depends on it.

5. Subscribing to too-wide a slice

jsx
const { user, posts, comments } = useSelector(s => s);  // re-renders on any change

Subscribe narrowly:

jsx
const user = useSelector(s => s.user);

For multiple values use shallowEqual or split into multiple selectors.

When to optimize

Don't memoize everything by default. useMemo and useCallback cost CPU + memory; React.memo adds a shallow compare on every render. They pay off when the rendered subtree is expensive, not when it's tiny.

Profile first:

  • React DevTools → Profiler → record an interaction → look for components that re-render with no visible change.
  • Highlight updates: DevTools → Components → ⚙ → Highlight updates when components render. Flashing borders show you exactly what's re-rendering.

If a component renders 100 times in 5 seconds but each render is 0.1ms, don't bother. If it renders 5 times and each is 50ms, fix it.

Patterns that scale

  • Compose with children prop instead of rendering inside Provider:

``jsx <Provider>{children}</Provider> `` Children are stable references from the outer render; Provider state updates don't re-render them.

  • Pull state down: any state used by only one subtree belongs in that subtree.
  • Split context: separate frequently-changing values from rarely-changing setters.
  • React.memo on list items + stable keys + memoized item props for big lists.
  • External stores (Zustand, Jotai) for fine-grained subscriptions — components subscribe only to the atoms they use.

Things that won't help

  • Wrapping primitives in useMemo (useMemo(() => 5, []) is wasteful).
  • Memoizing a callback that's only called once.
  • React.memo on a component whose props always change.
  • Memoizing inside a list item but creating new props in the parent's map().

Mental model

Re-renders aren't the enemy — expensive re-renders are. Make components fast first; memoize as a targeted fix for measured slowness; design state placement to limit the blast radius of updates. React 19's compiler will auto-memoize a lot of this, which makes the manual work less necessary — but understanding why and where matters now and after.

Follow-up questions

  • When is React.memo actually worth using?
  • How does the React 19 compiler change this?
  • What's the difference between useMemo and useCallback?
  • How do you debug a sluggish component with the React Profiler?

Common mistakes

  • Memoizing everything by default — net negative.
  • useMemo around primitives or trivial computations.
  • React.memo on a component whose props always change (object literals from parent).
  • Context value as inline object — every consumer re-renders on every parent render.
  • Subscribing to entire Redux state instead of a slice.
  • Lifting state too high — global state for local concerns.

Performance considerations

  • Cheap re-renders are fine — React's reconciler is fast. The pain hits in: (1) huge trees that all re-render on every state change, (2) per-item expensive children in long lists, (3) deep prop drilling with object props. Fix those three patterns and most apps feel snappy. The React 19 compiler will handle a lot of the boilerplate.

Edge cases

  • Strict mode double-invokes components in dev — expected, not a re-render bug.
  • React 18 automatic batching: multiple setStates inside async callbacks now batch (in 17 they didn't).
  • use() and Suspense can suspend mid-render — different render lifecycle to reason about.
  • Concurrent rendering may discard a render in progress — side effects in render are an anti-pattern.
  • External store subscriptions need useSyncExternalStore for tearing-free concurrent rendering.

Real-world examples

  • Form libraries (React Hook Form) deliberately avoid re-rendering the whole form on each keystroke — they isolate field state.
  • Zustand and Jotai's selling point is fine-grained subscription, fixing the 'context re-renders everything' pain.
  • TanStack Table virtualizes + memoizes rows for huge grids.

Senior engineer discussion

Seniors design state placement and context shape to minimize re-renders, then reach for memo as a targeted fix when profiling shows a problem. They know the next React version (19) ships an auto-memoizing compiler that removes most of this manual work. The fundamental skill — knowing where state should live, what depends on what — remains valuable regardless.

Related questions