useEffectEvent is designed to approximate the behavior of React’s experimental useEffectEvent hook. It provides a stable function reference that can be safely used in effect dependencies while always executing with the latest props and state.
Installation
npm install @radix-ui/react-use-effect-event
Function Signature
function useEffectEvent<T extends (...args: any[]) => any>(
callback?: T
): T
Parameters
The callback function to be wrapped. The returned function will always call the latest version of this callback.
Return Value
A stable function reference that always calls the latest version of the provided callback. Safe to use in effect dependencies without causing re-runs.
Usage
Basic Example
import { useEffectEvent } from '@radix-ui/react-use-effect-event';
import { useEffect } from 'react';
function Chat({ roomId, onMessage }: {
roomId: string;
onMessage: (msg: string) => void;
}) {
const handleMessage = useEffectEvent(onMessage);
useEffect(() => {
const connection = connectToRoom(roomId);
connection.on('message', handleMessage);
return () => connection.disconnect();
}, [roomId]); // onMessage is not in dependencies
return <div>Chat Room: {roomId}</div>;
}
Avoiding Stale Closures
import { useEffectEvent } from '@radix-ui/react-use-effect-event';
import { useEffect, useState } from 'react';
function Logger({ userId }: { userId: string }) {
const [count, setCount] = useState(0);
const logEvent = useEffectEvent(() => {
// Always uses the latest count and userId
console.log(`User ${userId} count: ${count}`);
});
useEffect(() => {
const interval = setInterval(() => {
logEvent();
}, 1000);
return () => clearInterval(interval);
}, []); // Empty dependencies, but logEvent always has latest values
return (
<button onClick={() => setCount(c => c + 1)}>
Increment ({count})
</button>
);
}
Event Handlers in Effects
import { useEffectEvent } from '@radix-ui/react-use-effect-event';
import { useEffect } from 'react';
function WindowListener({ onResize }: {
onResize: (width: number, height: number) => void
}) {
const handleResize = useEffectEvent(onResize);
useEffect(() => {
const listener = () => {
handleResize(window.innerWidth, window.innerHeight);
};
window.addEventListener('resize', listener);
return () => window.removeEventListener('resize', listener);
}, []); // Effect only runs once, but handleResize always current
return null;
}
With External APIs
import { useEffectEvent } from '@radix-ui/react-use-effect-event';
import { useEffect } from 'react';
function Subscription({ topic, onData, apiKey }: {
topic: string;
onData: (data: any) => void;
apiKey: string;
}) {
const handleData = useEffectEvent((data) => {
// Can access latest apiKey without re-subscribing
if (apiKey) {
onData(data);
}
});
useEffect(() => {
// Only re-subscribe when topic changes
const unsubscribe = subscribe(topic, handleData);
return unsubscribe;
}, [topic]);
return <div>Subscribed to {topic}</div>;
}
Implementation Details
The hook has three implementation strategies:
- React’s native
useEffectEvent (when available in future React versions)
useInsertionEffect for React 18+ (when available)
useLayoutEffect as a fallback for older React versions
The implementation:
- Stores the callback in a ref
- Updates the ref before layout/paint using the most appropriate hook
- Returns a stable memoized function that calls the latest callback
- Throws an error if called during render
Notes
This is a shim/polyfill for React’s experimental useEffectEvent hook. When React’s official version becomes stable, this hook will automatically use the native implementation.
The returned function cannot be called during render - it will throw an error if you try. It’s designed specifically for use in effects, event handlers, and callbacks.
This hook is similar to useCallbackRef but is specifically designed for the effect event pattern where you want the latest closure without re-running effects.