composeRefs provides utilities to compose multiple React refs together, allowing you to forward refs to multiple destinations. This is essential when building reusable components that need to manage both internal refs and forward refs from parent components.
Installation
npm install @radix-ui/react-compose-refs
Functions
composeRefs
Composes multiple refs into a single ref callback function.
function composeRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T>
useComposedRefs
A React hook that composes multiple refs with proper memoization.
function useComposedRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T>
Type Definitions
type PossibleRef<T> = React.Ref<T> | undefined;
Usage
Basic Composition with useComposedRefs
import { useComposedRefs } from '@radix-ui/react-compose-refs';
import { useRef, forwardRef } from 'react';
const Input = forwardRef<HTMLInputElement, { onChange?: () => void }>(
(props, forwardedRef) => {
const internalRef = useRef<HTMLInputElement>(null);
const composedRefs = useComposedRefs(forwardedRef, internalRef);
return <input ref={composedRefs} {...props} />;
}
);
Multiple Internal Refs
import { useComposedRefs } from '@radix-ui/react-compose-refs';
import { useRef, forwardRef, useEffect } from 'react';
const FocusableDiv = forwardRef<HTMLDivElement>((props, forwardedRef) => {
const focusRef = useRef<HTMLDivElement>(null);
const observerRef = useRef<HTMLDivElement>(null);
const composedRefs = useComposedRefs(forwardedRef, focusRef, observerRef);
useEffect(() => {
// Use focusRef for focus management
focusRef.current?.focus();
}, []);
useEffect(() => {
// Use observerRef for intersection observer
const observer = new IntersectionObserver((entries) => {
console.log('Visibility changed:', entries);
});
if (observerRef.current) {
observer.observe(observerRef.current);
}
return () => observer.disconnect();
}, []);
return <div ref={composedRefs} {...props} />;
});
With Callback Refs
import { useComposedRefs } from '@radix-ui/react-compose-refs';
import { useState, forwardRef } from 'react';
const MeasuredDiv = forwardRef<HTMLDivElement>((props, forwardedRef) => {
const [width, setWidth] = useState(0);
const measureRef = (node: HTMLDivElement | null) => {
if (node) {
setWidth(node.offsetWidth);
}
};
const composedRefs = useComposedRefs(forwardedRef, measureRef);
return (
<div ref={composedRefs} {...props}>
Width: {width}px
</div>
);
});
Direct composeRefs Usage
import { composeRefs } from '@radix-ui/react-compose-refs';
import { useRef } from 'react';
function Component() {
const ref1 = useRef<HTMLDivElement>(null);
const ref2 = useRef<HTMLDivElement>(null);
// Compose refs without memoization
const composedRef = composeRefs(ref1, ref2);
return <div ref={composedRef}>Content</div>;
}
With React 19 Ref Cleanup
import { useComposedRefs } from '@radix-ui/react-compose-refs';
import { forwardRef, useCallback } from 'react';
const Component = forwardRef<HTMLDivElement>((props, forwardedRef) => {
const refWithCleanup = useCallback((node: HTMLDivElement | null) => {
if (node) {
console.log('Node attached:', node);
// Return cleanup function (React 19+)
return () => {
console.log('Node detached:', node);
};
}
}, []);
const composedRefs = useComposedRefs(forwardedRef, refWithCleanup);
return <div ref={composedRefs} {...props} />;
});
Building a Reusable Button
import { useComposedRefs } from '@radix-ui/react-compose-refs';
import { forwardRef, useRef, useImperativeHandle } from 'react';
interface ButtonHandle {
focus: () => void;
blur: () => void;
}
const Button = forwardRef<ButtonHandle, React.ComponentPropsWithoutRef<'button'>>(
(props, forwardedRef) => {
const buttonRef = useRef<HTMLButtonElement>(null);
useImperativeHandle(forwardedRef, () => ({
focus: () => buttonRef.current?.focus(),
blur: () => buttonRef.current?.blur(),
}));
return <button ref={buttonRef} {...props} />;
}
);
Implementation Details
The utilities handle:
- Callback refs: Functions that receive the element as an argument
- RefObject: Objects with a
current property
- Cleanup functions: React 19’s ref cleanup return values
- Undefined refs: Gracefully handles
undefined refs
Key features:
- Automatically detects and calls cleanup functions (React 19+)
- Safely handles null/undefined refs
- Works with both mutable ref objects and callback refs
- Properly memoized in
useComposedRefs to avoid unnecessary re-renders
Differences Between composeRefs and useComposedRefs
composeRefs: Returns a new ref callback every time. Use when you don’t need memoization.
useComposedRefs: Returns a memoized ref callback using useCallback. Use in components to avoid unnecessary re-renders.
Notes
useComposedRefs automatically memoizes the composed ref based on the input refs, preventing unnecessary re-renders when passed as props.
The utilities support React 19’s ref cleanup feature. When a callback ref returns a function, that function will be called when the ref is detached or replaced.
Both utilities handle the differences between callback refs and RefObject refs transparently, so you can mix and match different ref types.