Skip to content
tezvyn:

React's Stale Closure Problem in Hooks

Source: react.devHardHow cards are made

React's Stale Closure Problem in Hooks

A useEffect closure captures props and state from its render. If the effect doesn't re-run, its functions can operate on old, "stale" data. This surfaces in timers or socket listeners. The footgun is omitting dependencies, which causes these subtle bugs.

Why it exists

React components re-render when state or props change, creating new functions and values. However, some effects, like setting up a subscription or a timer, should only run once. This creates a conflict: how can a function inside a one-time effect access the latest state from subsequent renders?

The mental model

Think of a useEffect with an empty dependency array ([]) as taking a photograph of your props and state on the first render. Any function defined inside that effect is looking at that old photograph. Even if the real-world scene (your component's state) changes, the function inside the effect still sees the original, stale image.

How it works

When a component mounts, useEffect with [] runs its setup function. If that setup includes an asynchronous callback, like for setInterval or a WebSocket's onmessage handler, that callback forms a closure. It captures the props and state as they existed during that initial render. When state updates later, the component re-renders, but the original effect and its closure are unaffected. The callback continues to execute with the initial, now-stale values. The traditional fix, adding the state to the dependency array, would cause the effect to tear down and re-run on every state change, which is often not what you want (e.g., resetting a timer or a connection).

When to use it

The stale closure problem is solved by separating the event-like logic from the effect's lifecycle. The useEffectEvent hook (in React 19+) is designed for this. You wrap the logic that needs fresh state (like onMessage or onTick) in useEffectEvent. You can then call this stable function from inside your useEffect without adding it to the dependency array, ensuring it always accesses the latest props and state.

When not to use it

Do not use this pattern to simply silence the dependency linter. If an effect's core logic must re-synchronize when a value changes (e.g., re-fetching data when an id prop changes), that value belongs in the dependency array. useEffectEvent is for functions that behave like event handlers fired from an effect, not for values that control the effect's existence.

One canonical example

Imagine a setInterval in a useEffect that logs a count state every second. If useEffect has an empty dependency array, it will log the initial count (e.g., 0) forever, even as the count state increases. The function inside setInterval has a stale closure over the initial count. Wrapping the console.log(count) logic in useEffectEvent and calling it from the interval would solve this, as it would always read the latest count without needing to reset the interval itself.

Interview question

When a useEffect with an empty dependency array sets up a setInterval that needs to log the latest component state, what is the primary benefit of using useEffectEvent?

  • a.It allows the setInterval to run only once, preventing unnecessary re-creations on state changes.
  • b.It ensures the setInterval callback always receives the initial state captured at mount time.
  • c.It automatically adds the setInterval callback to the useEffect's dependency array, resolving linter warnings.
  • d.It enables the setInterval's callback to access the most current state without forcing the useEffect to re-execute.Correct
Why?

useEffectEvent provides a stable function that, when called, always accesses the latest props and state from the component's current render, even if the useEffect it's called from has an empty dependency array and thus doesn't re-run. This directly solves the stale closure problem. Option B describes the stale closure problem itself, not the benefit of the solution. Option A describes the behavior of an empty dependency array, not the specific benefit of useEffectEvent. Option C is incorrect as useEffectEvent is used to avoid adding functions to the dependency array.

Just read this? Test yourself on what you have been reading.

Read the original → react.dev

You just looked this up. Could you explain it out loud?

That is the part interviews actually test. Tezvyn takes questions like this one and gives you what the interviewer is really checking, the answer that lands, and the mistake that ends the conversation, in the four minutes before your next meeting.

The iPhone app is on the way

We are building it. Until it lands, nothing here is held back from you: every interview card, your saved cards, streaks and the job board all work in Safari, plus hundreds of free practice quizzes of thirty questions each. Sign in and it all carries over to the app the day it arrives.

Want it as an icon? Tap Share at the bottom of Safari, then Add to Home Screen. It opens full screen and the cards you have read stay available offline.

Get it on Google PlayiPhone app coming soon

We are hiring for this. Open roles that interview on react — each one lists the topics its interview covers.

See open roles