LOKE Design System
Lib

Compose Refs Utility

Combine multiple React refs (RefObject and callback refs) into a single ref, with a hook that memoizes the composed ref for stable usage.

Compose Refs Utility

The compose-refs helpers let you safely set and combine multiple refs on the same element. This is essential when a component needs to forward a ref while also keeping its own internal ref, or when multiple hooks/components need the same DOM node.

Supports both ref object and callback ref styles, and includes a hook (useComposedRefs) that memoizes the composed ref via useCallback.


Why use it?

  • Forward a ref while maintaining an internal ref in your component
  • Compose refs from multiple hooks (e.g., intersection observer + forwarded ref)
  • Works with both RefObject and function (callback) refs
  • Stable callback via useComposedRefs to avoid extra re-renders

API

setRef

Sets a given ref to a given value. Works for both callback refs and RefObject refs.

Prop

Type

Function signature (conceptual):

function setRef<T>(ref: React.Ref<T> | undefined, value: T): void | (() => void);

composeRefs

Creates a single ref callback that sets multiple refs at once.

Prop

Type

Function signature (conceptual):

function composeRefs<T>(...refs: Array<React.Ref<T> | undefined>): React.RefCallback<T>;

useComposedRefs

A custom hook that composes multiple refs and memoizes the resulting callback using useCallback. Prefer this in function components so the ref identity is stable between renders.

Prop

Type

Hook signature (conceptual):

function useComposedRefs<T>(...refs: Array<React.Ref<T> | undefined>): React.RefCallback<T>;

Usage

Forwarding ref + internal ref

import * as React from 'react';
import { useComposedRefs } from '@loke/ui/compose-refs';

type InputProps = React.ComponentPropsWithoutRef<'input'>;

export const TextInput = React.forwardRef<HTMLInputElement, InputProps>(
  function TextInput({ ...props }, ref) {
    // internal ref (e.g., for focusing, measuring, etc.)
    const internalRef = React.useRef<HTMLInputElement>(null);

    // stable composed ref
    const composedRef = useComposedRefs(internalRef, ref);

    React.useEffect(() => {
      // internalRef.current is always up-to-date
      // Example: focus on mount
      internalRef.current?.focus();
    }, []);

    return <input {...props} ref={composedRef} className="border rounded px-3 py-2" />;
  }
);

Adding a hook that needs a ref

import * as React from 'react';
import { useComposedRefs } from '@loke/ui/compose-refs';

function useLogRef<T extends HTMLElement>() {
  const ref = React.useRef<T>(null);
  React.useEffect(() => {
    // eslint-disable-next-line no-console
    console.log('Node changed:', ref.current);
  }, [ref.current]);
  return ref;
}

export const LoggedButton = React.forwardRef<HTMLButtonElement, React.ButtonHTMLAttributes<HTMLButtonElement>>(
  function LoggedButton(props, ref) {
    const logRef = useLogRef<HTMLButtonElement>();
    const composed = useComposedRefs(logRef, ref);

    return (
      <button
        {...props}
        ref={composed}
        className="inline-flex items-center rounded-md border px-3 py-2"
      />
    );
  }
);

Composing multiple callback refs

import * as React from 'react';
import { composeRefs } from '@loke/ui/compose-refs';

function Example() {
  const storeNode = (node: HTMLDivElement | null) => {
    // custom logic
  };

  const logNode = (node: HTMLDivElement | null) => {
    // eslint-disable-next-line no-console
    console.log('node is', node);
  };

  const composed = React.useMemo(
    () => composeRefs<HTMLDivElement>(storeNode, logNode),
    []
  );

  return <div ref={composed}>Hello</div>;
}

Patterns and tips

  • Prefer useComposedRefs in components so the ref identity is stable between renders.
  • Order matters only insofar as side effects in callback refs — keep them pure if possible.
  • When you forward refs, always compose your internal ref with the forwarded ref rather than choosing one or the other.
  • Works seamlessly with libraries/hooks that require refs while still keeping your local ref logic.

Troubleshooting

  • “My ref is null” — remember refs are populated after mount. Read them in effects or event handlers, not during render.
  • “Ref callback runs often” — callback refs can run with null before setting a new node. Handle both null and element values.
  • “Multiple refs not updating” — ensure you’re attaching the composed ref to the actual DOM node (or the root element of the slotted component).