React 19 introduces a powerful new hook called useEffectEvent, designed to make effects more predictable and easier to manage. If you’ve ever run into the “stale closure” problem or written overly complex dependency arrays, this hook is for you.
The Problem with useEffect
Let’s start with a classic problem every React developer has faced:
import { useEffect, useState } from "react";
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
console.log("Count:", count);
setCount(count + 1);
}, 1000);
return () => clearInterval(interval);
}, []); // Problem: `count` is stale!
return <p>Count: {count}</p>;
}At first glance, this looks fine. But since count is not included in the dependency array, the closure inside setInterval always references the initial value of count (0).
If you add count to the dependencies, the effect will re-run every second, recreating a new interval repeatedly—leading to multiple intervals running at once.
This issue happens because functions in React capture values from when they were created.
And that’s exactly the problem useEffectEvent solves.
What is useEffectEvent?
useEffectEvent is a new React 19 hook that lets you define event functions inside effects that can always access the latest props or state, without causing the effect to re-run.
It gives you a stable reference to a function that always reads the most current data.
Using useEffectEvent: Example
Here’s how we can fix the Timer component using useEffectEvent:
import { useState, useEffect, useEffectEvent } from "react";
function Timer() {
const [count, setCount] = useState(0);
// Define a stable event that always uses the latest state
const onTick = useEffectEvent(() => {
console.log("Count:", count);
setCount(count + 1);
});
useEffect(() => {
const interval = setInterval(() => {
onTick();
}, 1000);
return () => clearInterval(interval);
}, [onTick]); // stable reference, effect runs only once
return <p>Count: {count}</p>;
}What changed:
- The useEffect itself still runs once.
- The onTick event always has access to the latest count.
- We’ve eliminated stale closures without adding dependencies.
Practical Example — Handling Window Events
Let’s see a practical case where useEffectEvent shines — handling a resize event:
import { useState, useEffect, useEffectEvent } from "react";
function WindowTracker() {
const [width, setWidth] = useState(window.innerWidth);
const handleResize = useEffectEvent(() => {
setWidth(window.innerWidth);
console.log("Resized:", width); // always latest width
});
useEffect(() => {
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, [handleResize]);
return <p>Window width: {width}px</p>;
}Here’s what’s happening:
- The handleResize function always reads the latest width.
- The useEffect doesn’t re-run unnecessarily.
- Your event listener stays stable and accurate — even after state changes.
When to Use useEffectEvent
Use useEffectEvent when:
- You’re defining a function inside an effect that reads the latest props or state.
- You want to avoid adding too many dependencies in your useEffect.
- You need stable event callbacks inside effects like setInterval, setTimeout, or event listeners.
Avoid using it when:
- You’re calling it during render.
- You need a memoized callback for UI props — use useCallback instead.
Best Practices
- Keep logic small and specific inside useEffectEvent.
- Use it only inside effects or event handlers — not render.
- Don’t replace useCallback with it — they solve different problems.
- If your effect uses async operations or subscriptions, useEffectEvent makes cleanup easier and safer.
To read more about What are the Key Features in React 19, refer to our blog What are the Key Features in React 19.