
With React 19, a new Hook called useEffectEvent is now stable.
It solves a long-standing problem in React: stale closures inside effects — where effects accidentally capture outdated state or props.
Let’s explore what useEffectEvent does, why it exists, and how to use it effectively in real React 19 projects.
The Problem: Stale Closures
Consider this common pattern:
import { useEffect, useState } from "react";
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
console.log("Count:", count);
}, 1000);
return () => clearInterval(id);
}, []); // <-- Empty dependencies
}
You might expect the interval to log the latest count value every second.
But it doesn’t — it keeps logging 0.
Why? Because when the effect first runs, it captures the initial count value (a stale closure).
Even though count updates later, the interval callback never sees it.
You could fix it by adding count to the dependency array:
useEffect(() => {
const id = setInterval(() => {
console.log("Count:", count);
}, 1000);
return () => clearInterval(id);
}, [count]);
…but that means the interval restarts every time count changes — which can cause flickering or performance issues.
The Solution: useEffectEvent
React 19 introduces useEffectEvent to fix this issue cleanly.
It allows you to define a stable function inside an effect that always sees the latest state and props — without forcing the effect to re-run.
Example:
import { useState, useEffect, useEffectEvent } from "react";
export default function Timer() {
const [count, setCount] = useState(0);
const onTick = useEffectEvent(() => {
console.log("Current count:", count);
});
useEffect(() => {
const id = setInterval(() => {
onTick();
}, 1000);
return () => clearInterval(id);
}, [onTick]);
return (
<button onClick={() => setCount((c) => c + 1)}>
Count: {count}
</button>
);
}
What’s happening here:
useEffectEventcreates a special callback (onTick) that always reads the latestcount.- The effect itself doesn’t need to re-run whenever
countchanges. - The
onTickreference is stable — you can safely include it in dependencies.
How useEffectEvent Works
- Think of it as a bridge between state and effects.
- It gives you a fresh version of your callback logic, without changing its identity.
- The function returned by useEffectEvent is stable (it doesn’t trigger re-renders), but when called, it uses the most recent values of your component’s state and props.
| Problem | Without useEffectEvent | With useEffectEvent |
|---|---|---|
| Access latest state | ❌ Stale closure | ✅ Always up-to-date |
| Effect re-runs on state change | ✅ Yes | ❌ No |
| Code clarity | ❌ More deps | ✅ Simpler |
| Function identity | ❌ Changes | ✅ Stable |
Example: Event Listeners
Another classic example is when you attach event listeners:
import { useState, useEffect, useEffectEvent } from "react";
export default function WindowLogger() {
const [width, setWidth] = useState(window.innerWidth);
const onResize = useEffectEvent(() => {
console.log("Current width:", width);
});
useEffect(() => {
window.addEventListener("resize", onResize);
return () => window.removeEventListener("resize", onResize);
}, [onResize]);
return <p>Window width: {width}px</p>;
}
Here, the onResize callback always accesses the latest width value —
but the listener never needs to be reattached when the state changes.
When Not to Use It
Don’t use useEffectEvent for UI event handlers like onClick, onChange, etc.
Those handlers should still be defined inline or using useCallback.
useEffectEvent is only for functions used inside effects, such as:
- Timers (
setInterval,setTimeout) - Event listeners (
addEventListener) - Async effects that need to read latest state
Real-World Use Case: Fetch Polling
Here’s an example of polling an API every few seconds using the latest token:
import { useEffect, useState, useEffectEvent } from "react";
function Poller({ token }) {
const [data, setData] = useState(null);
const fetchData = useEffectEvent(async () => {
const res = await fetch("/api/data", {
headers: { Authorization: `Bearer ${token}` },
});
const json = await res.json();
setData(json);
});
useEffect(() => {
fetchData();
const id = setInterval(fetchData, 5000);
return () => clearInterval(id);
}, [fetchData]);
return <pre>{JSON.stringify(data, null, 2)}</pre>;
}
Even if the token prop changes, fetchData() automatically uses the latest one —
no need to restart the interval.
Why It Matters
Before React 19, you had two awkward choices:
- Add everything to dependencies → effect re-runs too often
- Disable dependencies → risk using stale state
useEffectEvent gives you the best of both worlds:
- A stable effect
- Access to live, current values