React · intermediate
React render-loop checklist
A practical blog-style checklist for diagnosing React render loops, organized around an editorial argument: confirm the loop is real, isolate which update channel is rescheduling renders, then fix the smallest root cause and verify with a regression check.
The symptoms
- •Component logs or profiler traces show renders firing faster than expected, with identical input props across consecutive passes.
- •Browser main thread stays saturated near 100% and React DevTools Profiler commits never settle, even with no user interaction.
- •Network or instrumentation panels show a steady stream of identical effects, fetches, or analytics events emitted each render.
- •React StrictMode in development doubles the observed render cadence because effects are intentionally re-invoked on commit.
Likely causes
- •State update is performed unconditionally inside the render body, so each render schedules the next render in the same tick.
- •Effect dependency array omits a value that the effect itself writes, producing an effect-driven state setter on every commit.
- •Parent passes a new object, array, or function reference each render and a memoized child treats it as changed input.
- •Context provider value is rebuilt inline, so every consumer re-renders and any consumer that calls setState in render or effect restarts the cycle.
- •Subscription or store hooks return a fresh object each call, defeating shallow equality and triggering downstream renders.
First ten minutes
- 01Reproduce the loop in isolation by reducing to the smallest subtree that still exhibits the symptom, and capture the render count before any code change.
- 02Open React DevTools Profiler, start recording, and let it run for two to three seconds; sort commits by count and identify the component with the highest commits-per-second.
- 03Mark the suspect component with a console log guarded by a render counter, then read whether the counter advances on every commit or only on prop changes.
- 04Disable StrictMode temporarily to separate development-only double-invocation from a genuine production loop, and note the baseline difference.
- 05List every setState, dispatch, and context write reachable from the suspect subtree and tag whether it is gated by an equality check or a dependency.
Evidence to collect
- •Render count per component from a guarded render counter or from the Profiler commits-per-second view.
- •Effect invocations and their dependency array contents, captured from a logged effect body or from a useEffect trace.
- •Identity stability of props passed into memoized children, captured via a shallow equality check on the boundary.
- •StrictMode flag value in the build, and whether the same render cadence appears in a production build with the flag removed.
- •Network and analytics emissions per render, captured by tagging outgoing requests with a render sequence number.
Where to look
- •The render body of the suspect component, specifically the top-to-bottom scan for setState calls outside conditionals or effects.
- •The dependency array of every useEffect and useMemo call reachable from the suspect subtree, compared against the values written inside.
- •The boundary between a parent that rebuilds objects or functions inline and a child wrapped in React.memo or relying on referential equality.
- •The context provider component, where the value prop may be a fresh object literal each render, causing consumer re-renders.
- •Custom hooks and store bindings, where a returned object may be reconstructed each call and downstream equality checks fail.
Diagnostic steps
- 01If a setState appears unconditionally in render, wrap it in a guard or move it into a useEffect with a stable dependency, then re-measure render count to confirm the loop ends.
- 02If an effect writes to state and the dependency array omits that state, either include the dependency, remove the write, or replace the pattern with a derived value or ref.
- 03If a memoized child re-renders despite stable inputs, add referential equality logging on the boundary to identify which prop is changing identity, then stabilize it at the source.
- 04If context consumers re-render excessively, memoize the provider value with useMemo and verify the consumer is not itself performing an unguarded setState.
- 05If a store hook returns a new object each call, wrap the return in a memoized selector that compares the selected fields, and confirm consumers re-render only on meaningful change.
Common mistakes
- •Removing StrictMode and assuming the loop is gone, when StrictMode only doubles calls and the underlying update is still present.
- •Adding an empty dependency array to silence the loop, which can hide a state-write that should have been derived or ref-based.
- •Wrapping a setState in a setTimeout to break the cycle, which delays symptoms rather than addressing the rescheduling cause.
- •Memoizing every prop by default, which obscures the real boundary where identity is being lost and inflates memory cost.
- •Concluding a loop from a single high render count without checking whether renders are increasing or have reached a plateau after the initial mount.
Safe fixes
- •When state is set unconditionally in render, gate the update behind a comparison against the previous value or move the logic into an event handler or effect with a stable dependency.
- •When an effect writes to a piece of state it also reads, restructure to derive the value from props or use a ref to bridge across renders, then re-measure render count.
- •When a parent rebuilds an object or function each render, stabilize the value with useMemo or useCallback and verify the memoized child no longer re-renders on every parent render.
- •When a context value is rebuilt inline, memoize the value object with useMemo against the actual source state, and confirm consumer commit rate drops in the Profiler.
- •When a custom hook returns a new object, expose only the selected primitive values and let consumers derive locally, then confirm equality checks now pass at the boundary.
Prove the fix
- 01Render count per suspect component reaches a stable plateau after the initial mount, with no further commits recorded by an idle Profiler over a ten-second window.
- 02Effect invocations for the suspect subtree fire exactly once per meaningful dependency change, verified by a logged effect body and by a stable network or analytics emission pattern.
- 03Memoized children re-render only when their inputs change identity, verified by a boundary equality log that shows a hit rate consistent with user actions and not with parent re-renders.
- 04A canary production build with the original code re-introduces the loop, confirming the fix is local to the identified boundary rather than a coincidence of environment.
Prevention and next steps
- •Establish a coding rule that setState must never appear unconditionally in a render body, enforced by review and by a lint rule where available.
- •Favor derived values over effect-driven mirrors, so the data flow from props to state stays one-directional and auditable.
- •Stabilize props that cross a memoized boundary at the producer, so consumers can rely on referential equality without redundant memoization.
- •Keep React StrictMode enabled in development to surface effect re-invocation early, and treat its warnings as evidence to investigate rather than noise to suppress.
Safe commands and checks
git log --oneline -n 5 -- <path-to-suspect-component>
grep -RInE 'setState|set[A-Z][A-Za-z0-9]*\(' src --include='*.tsx' --include='*.jsx' --include='*.ts' --include='*.js'
grep -RInE 'useEffect|useLayoutEffect|useMemo|useCallback' src --include='*.tsx' --include='*.jsx' --include='*.ts' --include='*.js'
node -e 'process.stdout.write(require("./package.json").dependencies.react || "unknown")'