useCallbackRef
Return a stable callback that always calls the latest function, avoiding unnecessary re-renders and effect re-runs.
useCallbackRef
useCallbackRef creates a stable function whose identity never changes, but whose internal reference updates to the latest callback you pass in. This is useful when you want to:
- Avoid re-running effects that depend on a callback
- Pass handlers down as props without changing identity every render
- Subscribe/unsubscribe to DOM events or third-party APIs once, while still using the freshest logic
Unlike useCallback, which memoizes based on a dependency list, useCallbackRef returns a single stable function and swaps the underlying implementation via a ref.
API
function useCallbackRef<T extends (...args: any[]) => any>(
callback: T | undefined,
): T;Prop
Type
Return value:
- A stable function (identity does not change between renders) that, when invoked, calls the latest
callbackyou provided. Ifcallbackisundefined, invoking the stable function is a no-op.
Usage
1) Prevent effect re-runs caused by callback identity
import * as React from 'react';
import { useCallbackRef } from '@loke/ui/use-callback-ref';
export function Timer({ onTick }: { onTick?: (n: number) => void }) {
const [count, setCount] = React.useState(0);
// Create a stable wrapper that always calls the latest onTick
const onTickRef = useCallbackRef(onTick);
React.useEffect(() => {
const id = setInterval(() => {
setCount((n) => {
const next = n + 1;
onTickRef?.(next);
return next;
});
}, 1000);
return () => clearInterval(id);
}, [onTickRef]); // won't change identity every render
return <div className="text-sm text-muted-foreground">Count: {count}</div>;
}Without useCallbackRef, the interval effect would need to re-subscribe any time onTick changes identity.
2) Stable event subscriptions (DOM / third-party APIs)
import * as React from 'react';
import { useCallbackRef } from '@loke/ui/use-callback-ref';
export function GlobalKeyLogger({ onKey }: { onKey?: (e: KeyboardEvent) => void }) {
const handleKey = useCallbackRef(onKey);
React.useEffect(() => {
function listener(e: KeyboardEvent) {
handleKey?.(e);
}
window.addEventListener('keydown', listener);
return () => window.removeEventListener('keydown', listener);
}, [handleKey]);
return null;
}Here, we add the event listener once and always invoke the latest onKey.
3) Passing handlers down without forcing memoization elsewhere
import * as React from 'react';
import { useCallbackRef } from '@loke/ui/use-callback-ref';
function ToolbarButton({
onPress,
children,
}: {
onPress?: (e: React.MouseEvent<HTMLButtonElement>) => void;
children: React.ReactNode;
}) {
const onPressRef = useCallbackRef(onPress);
return (
<button
type="button"
// Stable identity; downstream memoized children won't re-render due to handler identity
onClick={(e) => onPressRef?.(e)}
className="inline-flex items-center rounded-md border px-3 py-2"
>
{children}
</button>
);
}How it works
- Internally stores your latest
callbackin a ref - Returns a single function created with
useMemo(empty dependency array) - When invoked, that stable function calls
callbackRef.current?.(...args) - Because the wrapper identity is stable, you can safely:
- Put it into dependency arrays for effects
- Pass it down as a prop without causing child re-renders
If you pass undefined as the callback, the returned function is still stable, but calling it is a no-op.
Patterns and tips
- Use for long-lived subscriptions (DOM events, WebSocket, timers) where you want mount/unmount only once.
- Ideal when handing a callback to a memoized child that shouldn’t re-render every parent render.
- If you truly need a new function identity when dependencies change, use useCallback instead.
Edge cases
- Return type: The returned function type matches your input type
T. Ifcallbackis optional, the returned function can be invoked safely; it no-ops when the underlying callback isundefined. thisbinding: The returned function is a plain function; do not rely onthis. Prefer lexical binding or arrow functions.- SSR: The hook uses
useEffectinternally; it’s safe for SSR – the effect simply attaches the latest callback after hydration.
Import
import { useCallbackRef } from '@loke/ui/use-callback-ref';