How to Call a Method After State Updates in React
Advertisement
🧩 Scenario
Architecture Walkthrough
State Setter Is Not Synchronous
Calling a state setter does not immediately update the state variable. React schedules a re-render and the new value is applied before the next render cycle runs. On the line of code directly after the setter call, the state variable still holds the value from the current render. The new value is only available in the next render's function body.
This is one of the most common sources of confusion for developers new to React. The fix is not to try to read the new state immediately but to use useEffect to run logic after the re-render with the new state value.
useEffect for Post-Update Reactions
useEffect with a dependency on the state variable runs after every render where that variable changed. The callback receives no arguments, but the state variable it depends on is already updated by the time the callback runs, because the component has already re-rendered with the new value.
This is the correct place for side effects that should happen in response to a state change: logging, sending analytics, updating local storage, triggering a data fetch based on a new filter value, or measuring the DOM after a list update.
useLayoutEffect for DOM Measurements
useLayoutEffect fires synchronously after React commits DOM changes but before the browser paints. If the post-update logic reads or writes DOM dimensions that depend on the new state, useLayoutEffect prevents the user from seeing the un-corrected layout. useEffect fires after the browser has painted, so a DOM correction inside useEffect may cause a visible flash.
Use useLayoutEffect sparingly. Blocking the browser paint with slow logic inside useLayoutEffect directly degrades perceived performance.
Key Code Explained
function SearchResults() {
const [query, setQuery] = useState('');
const [results, setResults] = useState<Result[]>([]);
const listRef = useRef<HTMLUListElement>(null);
// Runs after every render where query changed.
// At this point, 'query' is the new value (component already re-rendered).
useEffect(() => {
if (!query) {
setResults([]);
return;
}
fetchResults(query).then(setResults);
}, [query]); // dependency: run only when query changes
// useLayoutEffect: scroll to top before browser paints new results
// Prevents flash of old scroll position while new results load
useLayoutEffect(() => {
if (listRef.current) {
listRef.current.scrollTop = 0;
}
}, [results]); // runs synchronously after DOM update, before paint
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
<ul ref={listRef}>
{results.map((r) => (
<li key={r.id}>{r.title}</li>
))}
</ul>
</div>
);
}
// Stale state bug — the classic misunderstanding
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
console.log(count); // still 0 — setter is not synchronous
// The new value is only available in the next render
};
// Correct: run logic after the state update using useEffect
useEffect(() => {
if (count === 10) {
console.log('Reached 10!');
// analytics.track('milestone', { count });
}
}, [count]); // runs after render where count changed to 10
return <button onClick={handleClick}>Count: {count}</button>;
}
// If you need the new value synchronously in the same handler,
// compute it as a local variable and use it directly
function FormTracker() {
const [formData, setFormData] = useState({ name: '', email: '' });
const handleChange = (field: string, value: string) => {
const newData = { ...formData, [field]: value };
setFormData(newData);
// 'newData' has the updated value immediately — no need for useEffect
if (field === 'email') {
validateEmail(newData.email); // uses the new value synchronously
}
};
}
The useLayoutEffect for scroll position is important for UX: if you used useEffect here, the browser would paint the new results list at the old scroll position, then useEffect would fire and scroll to top, causing a visible jump. useLayoutEffect runs before paint so the user never sees the wrong scroll position.
Tradeoffs
| Hook | Fires when | Blocks paint | Use for |
|---|---|---|---|
useEffect | After render + browser paint | No | Async work, data fetch, logging, subscriptions |
useLayoutEffect | After render, before browser paint | Yes | DOM measurements, correcting layout before paint |
What Interviewers Actually Check
- Whether you know the state setter is not synchronous and reading state after the setter gives the old value
- Whether you can write a
useEffectwith a specific dependency to react to one state change - Whether you know the timing difference between
useEffectanduseLayoutEffect - Whether you know to compute the new value as a local variable if you need it synchronously in the same handler
- Whether you can identify the gotcha of using
useEffectwith no deps (runs on every render) vs specific deps
Follow-Up Questions
- How does
useInsertionEffect(React 18) differ fromuseLayoutEffectand when is it used? - If you have three
useEffecthooks that all depend on the same state variable, in what order do they run? - How would you debounce the side effect triggered by a rapidly changing state value like a search query?
- What is the
useEffectEventRFC (nowuseEventproposal) and how would it change the way you handle stale closures in effects? - How does
useEffect's cleanup function interact with a state update that fires before the previous effect's cleanup has run?
Common Candidate Mistakes
- Reading state on the line after the setter call and expecting the new value
- Using
useEffectwith no dependency array intending to react to one state variable, accidentally running on every render - Using
useLayoutEffectfor async operations like data fetching, blocking the browser paint for the duration of the request - Not knowing the local variable pattern for cases where the new value is needed synchronously in the same handler
- Failing to return a cleanup function from an effect that sets up a subscription or timer
Interview Readiness Checklist
Before you leave this question, make sure you can answer:
- Can you write a
useEffectthat runs only when a specific state variable changes? - Can you explain why reading state on the line after the setter call gives the old value?
- Can you explain the timing difference between
useEffectanduseLayoutEffect? - Can you compute the new value synchronously using a local variable when
useEffectis overkill? - Can you separate multiple state-change reactions into separate
useEffectcalls?
Summary
State setters in React do not update the state variable synchronously. Calling setCount(count + 1) schedules a re-render; the count variable in the current render function remains unchanged. To run logic after the state update is applied, use useEffect with the state variable as a dependency. The callback runs after the component re-renders with the new state value, so the state variable inside the callback reflects the updated value.
useEffect runs asynchronously after the browser has painted the new frame. If the post-update logic reads or corrects DOM measurements that affect visual layout, use useLayoutEffect instead. It fires synchronously after React commits DOM changes but before the browser paints, preventing visible layout shifts.
If the post-update logic needs the new value in the same event handler without waiting for a re-render, compute it as a local variable before calling the setter. This is the simplest pattern when the logic is synchronous and does not involve the DOM or async operations.
Why does reading state right after calling setState give the old value?
State updates in React are asynchronous. Calling setState schedules a re-render. The new state value is only available in the next render, not on the next line of code after the setter call.
Advertisement
Stay Updated
Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.
Advertisement