React re-render cascade: when one click re-renders the whole tree
What is happening
In React, a state update re-renders the component and, by default, all of its children. If the state lives high in the tree (a context, a page-level store), a single keystroke or click can re-render hundreds of components — most of which display exactly the same thing as before.
Each render is cheap; thousands of them in one event handler are not. The work lands in the processing time phase of INP.
How to recognise it
- React DevTools Profiler shows wide flame graphs: components re-render without their props visibly changing.
- Typing into a controlled input lags; every keystroke re-renders large lists or tables.
- Processing time dominates INP, and the stack in DevTools Performance is full of React internals.
How to fix it
- Move state down: keep it in the smallest component that needs it instead of a page-level component or context.
- Wrap expensive subtrees in React.memo and stabilise the props you pass them with useCallback/useMemo — memo is useless if a new callback is created on every render.
- Mark non-urgent updates (filtering a list while typing) with useTransition so the urgent render (the input itself) paints first.
Problem
const Page = () => {
const [query, setQuery] = useState("")
// every keystroke re-renders Page → Header, Sidebar, BigTable, Footer
return <>
<SearchInput value={query} onChange={setQuery} />
<BigTable rows={filterRows(rows, query)} />
</>
}
Fix
const BigTable = React.memo(({ rows }) => /* ... */)
const Page = () => {
const [query, setQuery] = useState("")
const [isPending, startTransition] = useTransition()
const rows = useMemo(() => filterRows(allRows, query), [query])
const onChange = (q) =>
startTransition(() => setQuery(q)) // input paints first, table follows
return <>
<SearchInput onChange={onChange} />
<BigTable rows={rows} />
</>
}
Scan your site for free →