How to Call a Method After State Updates in React

Intermediate10 min interview
Skills tested:
Using useEffect with a dependency on the updated state value to run logic after a state changeUnderstanding why reading state directly after calling the setter gives the previous valueKnowing when to use useLayoutEffect vs useEffect for post-update logicUsing the updater function form when the post-update logic needs the new value synchronouslyRunning logic only when a specific state variable changes, not on every render

Advertisement

🧩 Scenario

Post-state-update logic is a common pattern: logging analytics after a form field changes, triggering a side effect after a counter reaches a threshold, scrolling to a specific DOM position after a list item is added. All of these need to run after the new state is applied and the DOM is updated, which is exactly what useEffect provides.

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

HookFires whenBlocks paintUse for
useEffectAfter render + browser paintNoAsync work, data fetch, logging, subscriptions
useLayoutEffectAfter render, before browser paintYesDOM 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 useEffect with a specific dependency to react to one state change
  • Whether you know the timing difference between useEffect and useLayoutEffect
  • 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 useEffect with no deps (runs on every render) vs specific deps

Follow-Up Questions

  1. How does useInsertionEffect (React 18) differ from useLayoutEffect and when is it used?
  2. If you have three useEffect hooks that all depend on the same state variable, in what order do they run?
  3. How would you debounce the side effect triggered by a rapidly changing state value like a search query?
  4. What is the useEffectEvent RFC (now useEvent proposal) and how would it change the way you handle stale closures in effects?
  5. 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 useEffect with no dependency array intending to react to one state variable, accidentally running on every render
  • Using useLayoutEffect for 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 useEffect that 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 useEffect and useLayoutEffect?
  • Can you compute the new value synchronously using a local variable when useEffect is overkill?
  • Can you separate multiple state-change reactions into separate useEffect calls?

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.

Related Videos
Watch these videos to learn more about this topic
Frequently Asked Questions

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