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
useComposedRefsin 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
nullbefore setting a new node. Handle bothnulland 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).