LOKE Design System
Hooks

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 callback you provided. If callback is undefined, 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 callback in 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. If callback is optional, the returned function can be invoked safely; it no-ops when the underlying callback is undefined.
  • this binding: The returned function is a plain function; do not rely on this. Prefer lexical binding or arrow functions.
  • SSR: The hook uses useEffect internally; it’s safe for SSR – the effect simply attaches the latest callback after hydration.

Import

import { useCallbackRef } from '@loke/ui/use-callback-ref';