Home > @cascadiacollections/fluentui-compat > useEventCallback
useEventCallback() function
Hook to create a stable event handler that always calls the latest version of the callback.
This hook solves the problem where event handlers need access to the latest props/state without causing unnecessary re-renders or re-registrations of event listeners.
**Key Features**: - **Stable reference**: The returned callback has a stable identity that never changes - **Fresh values**: Always executes the latest version of the callback with current props/state - **Performance optimized**: Prevents re-renders in child components that depend on the callback - **Type safe**: Full TypeScript support with generic argument and return types - **React 19 compatible**: Uses useLayoutEffect for synchronous updates
**When to use**: - Event handlers that depend on frequently changing props/state - Callbacks passed to memoized child components to prevent unnecessary re-renders - Event listeners attached to window, document, or long-lived DOM elements - Callbacks in useEffect dependencies that shouldn't trigger the effect on every change
**Implementation Details**: - Uses useLayoutEffect to ensure callback ref updates synchronously before paint - Leverages useConst for a truly stable wrapper function that never changes - Throws error if called during render to enforce proper React patterns - Minimal overhead with no additional dependencies or complex state management
Signature:
export declare function useEventCallback<Args extends unknown[], Return>(fn: (...args: Args) => Return): (...args: Args) => Return;
Parameters
|
Parameter |
Type |
Description |
|---|---|---|
|
fn |
(...args: Args) => Return |
The callback function to wrap. Can access current props/state when invoked. |
Returns:
(...args: Args) => Return
A stable callback that always invokes the latest version of fn.
Example 1
// Event handler with frequently changing state
function SearchComponent() {
const [query, setQuery] = useState('');
const [filters, setFilters] = useState({});
// Without useEventCallback: handleSearch would change on every query/filters update
// causing MemoizedResults to re-render unnecessarily
const handleSearch = useEventCallback(() => {
performSearch(query, filters);
});
return (
<>
<input value={query} onChange={e => setQuery(e.target.value)} />
<MemoizedResults onSearch={handleSearch} />
</>
);
}
Example 2
// Window event listener with stable reference
function ResizeComponent() {
const [width, setWidth] = useState(0);
const handleResize = useEventCallback(() => {
setWidth(window.innerWidth);
});
useEffect(() => {
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, [handleResize]); // handleResize never changes, so effect only runs once
return <div>Width: {width}px</div>;
}
Example 3
// Callback in useEffect dependencies
function DataFetcher({ userId }: { userId: string }) {
const [data, setData] = useState(null);
const [refreshCount, setRefreshCount] = useState(0);
const fetchData = useEventCallback(async () => {
const result = await api.fetchUser(userId);
setData(result);
});
useEffect(() => {
fetchData();
}, [refreshCount]); // Only re-fetch when refreshCount changes, not when userId changes
// userId changes are picked up by fetchData automatically
return <div>{data?.name}</div>;
}
Example 4
// Generic type support
function GenericHandler<T>({ items, onSelect }: { items: T[], onSelect: (item: T) => void }) {
const handleClick = useEventCallback((item: T, index: number) => {
console.log('Selected', item, 'at index', index);
onSelect(item);
});
return (
<ul>
{items.map((item, i) => (
<li key={i} onClick={() => handleClick(item, i)}>
{String(item)}
</li>
))}
</ul>
);
}