How to Run Code on Every Re-Render in React
Advertisement
🧩 Scenario
Architecture Walkthrough
How Every-Render Effects Work
When useEffect is called without a second argument, React runs the callback after every completed render. The first render fires the callback. Every subsequent re-render (from state changes, prop changes, or parent re-renders) fires the callback again. There is no condition that prevents re-execution.
Before each run, React first invokes the cleanup function returned by the previous run. The cycle for every-render is: render, browser paint, effect run. On next render: cleanup of previous effect, render, browser paint, effect run. This cleanup-before-rerun behavior matters for subscriptions and listeners: the previous subscription is always torn down before the new one is set up.
When This Pattern Is Legitimate
Truly render-agnostic side effects are rare. Logging "component rendered" to a debugging console is one example. Syncing a non-React mutable value that does not affect rendering (like a ref or a third-party instance that needs to stay in sync) is another. The common characteristic is that the side effect is equally valid regardless of which specific state or props change triggered the render.
In most cases, the correct approach is to list the specific values the effect depends on. An effect that should react to a count change should declare [count] as its dependency, not run on every render.
The Infinite Loop
Setting state inside an effect with no dependency array causes an infinite loop. The state update triggers a re-render. The re-render triggers the effect. The effect updates state again. This cycle runs until the browser tab crashes. React's ESLint rules (react-hooks/exhaustive-deps) catch missing dependencies but not the logical error of updating state in an undepended effect.
Key Code Explained
// Pattern: run after every render (no dependency array)
function RenderTracker({ label }: { label: string }) {
const renderCount = useRef(0);
// useRef is better for tracking renders — updating a ref does not cause a re-render
renderCount.current++;
useEffect(() => {
// Runs after every render — no dependency array
console.log(`${label} rendered. Total renders: ${renderCount.current}`);
// No cleanup needed here — logging has no teardown
});
return <div>{label}</div>;
}
// Legitimate use: syncing an external mutable instance to the latest props
function TooltipManager({ text, target }: { text: string; target: HTMLElement | null }) {
const tooltipRef = useRef<Tooltip | null>(null);
useEffect(() => {
if (!target) return;
if (!tooltipRef.current) {
tooltipRef.current = new Tooltip(target); // initialize once
}
// Sync the tooltip text on every render where it might have changed
tooltipRef.current.setText(text);
tooltipRef.current.setTarget(target);
return () => {
tooltipRef.current?.destroy();
tooltipRef.current = null;
};
}); // No deps — always syncs to latest props. Better would be: [text, target]
// Infinite loop — most common mistake
function BrokenCounter() {
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1); // sets state → re-render → effect runs → sets state → ...
}); // No dependency array: runs after EVERY render
return <div>{count}</div>; // This will crash
}
// The difference between no deps, empty deps, and specific deps
function LifecycleDemo({ value }: { value: string }) {
useEffect(() => {
console.log('Runs after EVERY render');
}); // no array
useEffect(() => {
console.log('Runs only on MOUNT');
return () => console.log('Runs only on UNMOUNT');
}, []); // empty array
useEffect(() => {
console.log('Runs on mount and when value changes');
return () => console.log('Cleanup before next run + on unmount');
}, [value]); // specific dep
return <div>{value}</div>;
}
The TooltipManager example shows a case where the every-render pattern can be justified: the tooltip instance needs to stay synchronized with the latest text and target on every render because either might change. However, the comment shows the better approach: add [text, target] as dependencies. The every-render version re-syncs even if neither text nor target changed, which is wasteful. Specific dependencies are almost always the right choice.
Tradeoffs
| Pattern | When it runs | Risk | Use for |
|---|---|---|---|
| No dependency array | Every render | Infinite loops, unnecessary work | Truly render-agnostic side effects |
Empty array [] | Mount + unmount | None specific to this pattern | One-time setup and teardown |
Specific deps [a, b] | Mount + when a or b changes | Missing deps cause stale closures | Most side effects |
What Interviewers Actually Check
- Whether you can explain the difference between no dependency array, empty array, and specific deps
- Whether you know that no dependency array runs on every render (including the first)
- Whether you can identify the infinite loop caused by setting state inside an undepended effect
- Whether you know the cleanup cycle: previous cleanup fires before the next run
- Whether you recognize that specific dependencies are almost always better than no dependency array
Follow-Up Questions
- The React ESLint plugin
react-hooks/exhaustive-depswarns about missing dependencies. How does following this rule affect the need for every-render effects? - If an every-render effect has a cleanup function that sets up and tears down a subscription, what is the performance cost compared to using specific deps?
- How would you correctly track render count for debugging purposes without using an every-render effect?
- What is the
useEffectEventproposal (RFC) and how would it allow you to read the latest values inside an effect without adding them to the dependency array? - If two sibling effects have no dependency array, do they run in the order they appear in the component?
Common Candidate Mistakes
- Accidentally omitting the dependency array when the intent was mount-only (
[]) - Calling a state setter inside an effect with no dependency array and producing an infinite loop
- Thinking the pattern is acceptable for any side effect just because it works on the first render
- Not knowing about the cleanup-then-rerun cycle and leaving dangling subscriptions between renders
- Using no dependency array as a shortcut to avoid figuring out the correct dependencies
Interview Readiness Checklist
Before you leave this question, make sure you can answer:
- Can you write a
useEffectthat runs after every render by omitting the dependency array? - Can you explain the cleanup-then-run cycle for every-render effects?
- Can you identify the two conditions that create an infinite loop: state update in an undepended effect?
- Can you explain why specific dependencies are almost always better than no dependency array?
- Can you describe at least one legitimate use case for an every-render effect?
Summary
useEffect without a dependency array runs its callback after every render of the component. Before each subsequent run, React fires the cleanup function returned by the previous run, tearing down the previous side effect before setting up the new one. The callback runs after the browser has painted, so it does not block the render.
This pattern is rarely the correct choice. In most cases, the effect should declare the specific state or prop values it depends on, so it only re-runs when those values change. The every-render pattern runs even when none of the relevant values changed, performing unnecessary work on every render from any cause (parent re-renders, unrelated state updates, context changes).
The most dangerous mistake with this pattern is setting state inside an effect with no dependency array. Every state update triggers a re-render, which triggers the effect, which updates state again, creating an infinite loop. React's ESLint plugin catches missing dependencies but does not prevent this logical error. The fix is always to either add appropriate dependencies or move the state update outside the effect.
Does useEffect without a dependency array run before or after the render?
After. useEffect always runs after the component renders and the browser has painted. It never runs before or during a render.
Advertisement
Stay Updated
Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.
Advertisement