Skip to main content

Reasons for component re-rendering in React

Re-rendering is the process where React calls your component function again to build a fresh virtual DOM tree and compare it with the previous one.

Theory

TL;DR

  • State change always queues a re-render, even if the value looks the same
  • Parent re-render cascades down to all children unless blocked with React.memo
  • Context change re-renders every consumer in the tree, not just the nearest one
  • Analogy: re-rendering is like a kitchen reprinting an order ticket. A new order (state), the manager calling out (parent re-render), or a special request (context) all trigger a fresh print to check what changed
  • Rule of thumb: profile with React DevTools Profiler before adding any memoization

Quick example

jsx
import { useState } from 'react'; function Counter() { const [count, setCount] = useState(0); console.log('Counter rendered'); // logs on every re-render return ( <button onClick={() => setCount(count + 1)}> Clicked {count} times </button> ); } // Every click → setCount → component runs again → "Counter rendered" logs

Clicking the button calls setCount, React re-runs Counter, builds a new virtual tree, diffs it, and updates only the button text. That is the full cycle.

What triggers a re-render

State change. Calling setCount or any state setter always queues a re-render. React 18 batches multiple state updates within the same event handler into a single render pass, so two setState calls in one click handler produce one re-render, not two.

Props change. By default, any parent re-render re-runs all child components regardless of whether their props actually changed. React does not compare props before calling the child. That is why React.memo exists.

Parent re-render. This is the most common source of unexpected re-renders in production. A counter update in the parent causes every child to re-run, even children that receive no props at all.

Context change. Every component calling useContext(MyContext) re-renders when the context value changes. If you pass a new object literal to the provider on every render (value={{ theme: 'dark' }}), that is a new reference each time, so all consumers re-render even when the data is identical.

useReducer dispatch. Dispatching an action always triggers a re-render of the component that owns the reducer, then cascades down to its children.

Key difference: re-render vs DOM commit

Re-render and DOM update are two separate steps. React calls your component function (re-render), builds a new fiber tree, diffs it against the old one, and only then writes the minimal set of changes to the real DOM (commit). A component can re-render dozens of times with zero DOM mutations if the output is identical. React DevTools Profiler shows render count separately from paint time for exactly this reason.

When to optimize

  • Slow list rows with many items - wrap row components in React.memo, stabilize list data with useMemo
  • Callback props breaking child memo - wrap callbacks in useCallback so the reference stays stable across parent renders
  • Context triggering too many re-renders - split one large context into smaller focused ones, or wrap the value object in useMemo
  • Computed values recalculating on every render - cache with useMemo
  • Before any of this - run React DevTools Profiler to confirm the problem is real. Optimizing without measurement is guesswork

How React handles re-renders internally

React's scheduler queues fiber nodes when state or props change. The reconciler walks the fiber tree, compares previous and next props using shallow equality, and marks nodes that need updates. In React 18, automatic batching groups multiple state updates in async contexts (promises, setTimeout) into one render pass. The commit phase then writes only the diffed mutations to the real DOM. This is why 50 re-renders can result in 3 DOM updates.

Common mistakes

Mutating state directly

jsx
const [arr, setArr] = useState([1, 2]); arr.push(3); setArr(arr); // same reference → React skips re-render

React compares references with Object.is. Same reference means no update. Fix: setArr([...arr, 3]) creates a new array with a new reference.

Inline object or function props breaking memo

jsx
const Child = React.memo(({ user }) => <div>{user.name}</div>); function Parent() { const [count, setCount] = useState(0); const user = { name: 'Alice' }; // new object on every render return <Child user={user} />; } // "Child rendered" logs on every Parent re-render despite React.memo

React.memo does a shallow comparison. { name: 'Alice' } !== { name: 'Alice' } because they are different object references. Fix: const user = useMemo(() => ({ name: 'Alice' }), []);

Context value as a new object literal

jsx
<MyContext.Provider value={{ theme: 'dark' }}>

A new object is created on every parent render. All consumers re-render. Fix: wrap in useMemo or lift the value into state.

Thinking useEffect prevents re-renders

useEffect runs after the render, not instead of it. Adding an effect does not skip the render cycle. To skip re-renders, memoize the component or stabilize its props.

Inline arrow functions as props

jsx
<Child onClick={() => doSomething()} />

This creates a new function reference on every parent render. If Child is wrapped in React.memo, memo is useless here because the prop always looks new. Fix: const handleClick = useCallback(() => doSomething(), []);

Real-world usage

  • React TodoMVC - state arrays trigger full list re-renders; React.memo on individual rows for 1000+ items
  • Redux Toolkit - Immer creates new state references on every action, so useSelector needs precise selectors to avoid unnecessary re-renders
  • Next.js - router prop changes re-render page components; useSWR stabilizes data references to cut extra renders
  • React Query - cache mutations selectively notify only subscribed query observers, not the whole component tree
  • Material-UI - theme context changes cascade to all styled components; stable selectors in makeStyles limit the scope

Follow-up questions

Q: Why does my memoized component still re-render?
A: Check for unstable prop references: functions created inline, objects created inline, or context values without memoization. React DevTools Profiler shows "why did this render" for each component.

Q: State did not change but the component re-rendered. Why?
A: A parent re-render is the most common cause. Wrap the component in React.memo and verify all its props have stable references.

Q: What is the difference between a re-render and a DOM commit?
A: Re-render is React calling your component function. DOM commit is React writing actual changes to the browser. You can have many re-renders with very few DOM mutations if the output does not change.

Q: How does React 18 automatic batching affect re-renders?
A: Before React 18, each setState inside a setTimeout or Promise.then caused a separate render. React 18 batches all of them automatically into one pass. For non-urgent updates, startTransition lets React deprioritize heavy re-renders to keep the UI responsive.

Q: How do you optimize a list of 10,000 items that renders slowly?
A: Combine windowing (react-window or react-virtual), React.memo on row components, and useMemo on data transforms. Windowing gives the biggest win because it removes most DOM nodes from the tree entirely.

Examples

Basic: state triggers a re-render

jsx
import { useState } from 'react'; function Counter() { const [count, setCount] = useState(0); console.log('rendered'); // runs on every click return ( <button onClick={() => setCount(count + 1)}> Clicked {count} times </button> ); }

Every click passes a new value to setCount. React re-runs Counter, logs "rendered", and patches the button text. If you call setCount(count) with the same value, React bails out after the first re-render and skips DOM updates.

Intermediate: parent re-render cascading without and with memo

jsx
function TodoList() { const [todos, setTodos] = useState([]); // empty deps array works because we use functional update form below const addTodo = useCallback((text) => { setTodos(prev => [...prev, text]); }, []); return ( <div> <AddTodo onAdd={addTodo} /> <TodoItems todos={todos} /> </div> ); } // Without memo: re-renders on every parent update // With memo: re-renders only when todos array reference changes const TodoItems = React.memo(({ todos }) => { console.log('TodoItems rendered'); return todos.map((todo, i) => <div key={i}>{todo}</div>); });

Without React.memo, TodoItems re-renders every time the parent does, even when todos has not changed. Adding memo together with a stable addTodo callback cuts re-renders to only when the list actually changes.

Advanced: the object prop trap

jsx
const UserCard = React.memo(({ user }) => { console.log('UserCard rendered'); // still logs every time return <div>{user.name}</div>; }); function Dashboard() { const [tick, setTick] = useState(0); // new object on every render, shallow comparison always fails const user = { name: 'Alice', id: 1 }; return ( <> <button onClick={() => setTick(t => t + 1)}>Tick</button> <UserCard user={user} /> </> ); } // Fix: // const user = useMemo(() => ({ name: 'Alice', id: 1 }), []);

React.memo compares props with shallow equality. Two object literals { name: 'Alice' } are never equal by reference, so memo never blocks the re-render. useMemo with an empty dependency array keeps the same reference across renders. I have seen this exact bug in a dashboard with 40+ cards where a 100ms timer tick was re-rendering the whole list because of one inline style object passed as a prop.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?