Mastering React Refs: Advanced Techniques with useCombinedRef Hook
Add to your RSS feed6 February 20257 min readTable of Contents
When developing in React, direct interaction with DOM elements is often necessary. React provides the refs mechanism to access elements after rendering. The most common approach is using object refs via useRef
, but another option is callback refs, which offer greater flexibility and control over the lifecycle of elements. This article explains how callback refs work, their advantages, common issues, and practical use cases.
How Callback Refs Work
Callback refs provide a more precise way to manage ref bindings compared to object refs. Here's how they function:
- Mounting: When an element is mounted in the DOM, React calls the ref function with the element itself, allowing immediate interaction.
- Unmounting: When an element is removed, React calls the ref function with null, enabling cleanup operations.
Example: Tracking Mounting and Unmounting
1 import React, { useCallback, useState } from 'react';23 function MountUnmountTracker() {4 const [isVisible, setIsVisible] = useState(false);56 const handleRef = useCallback((node: HTMLDivElement | null) => {7 if (node) {8 console.log('Mounted:', node);9 } else {10 console.log('Unmounted');11 }12 }, []);1314 return (15 <div>16 <button onClick={() => setIsVisible((prev) => !prev)}>17 {isVisible ? 'Hide' : 'Show'} Element18 </button>19 {isVisible && <div ref={handleRef}>Tracked Element</div>}20 </div>21 );22 }2324 export default MountUnmountTracker;
In this example, the handleRef
callback logs the element when it mounts and unmounts. The isVisible
state toggles the visibility of the tracked element. Whenever the element appears or disappears, handleRef
is called, allowing us to track its lifecycle.
Common Issues and Solutions
Issue: Unnecessary Callback Ref Calls
A common problem is that React recreates the ref function on every re-render, leading to unwanted ref reassignments. This happens because React thinks it's dealing with a new ref, triggering a null
call before assigning the new node.
Solution: Memoizing with useCallback
We can avoid this by using useCallback
to ensure the function remains unchanged unless dependencies change. This memoization prevents unnecessary ref calls and maintains the correct ref binding.
1 import React, { useState, useCallback, useReducer } from 'react';23 function Basic() {4 const [showDiv, setShowDiv] = useState(false);5 const [, forceRerender] = useReducer((v) => v + 1, 0);67 const toggleDiv = () => setShowDiv((prev) => !prev);89 const refCallback = useCallback((node: HTMLDivElement | null) => {10 console.log('div', node);11 }, []);1213 return (14 <div>15 <button onClick={toggleDiv}>Toggle Div</button>16 <button onClick={forceRerender}>Rerender</button>17 {showDiv && <div ref={refCallback}>Example div</div>}18 </div>19 );20 }2122 export default Basic;
This ensures that refCallback is created only once and prevents unnecessary reassignments. The ref function remains stable across re-renders, maintaining the correct ref binding.
Callback Refs vs useEffect
and useLayoutEffect
Callback refs interact differently with useEffect
and useLayoutEffect
:
- Callback refs fire before
useLayoutEffect
anduseEffect
. useLayoutEffect
runs after DOM updates but before the browser repaints.useEffect
runs after the browser has painted.
1 import React, { useEffect, useLayoutEffect, useCallback } from 'react';23 function WhenCalled() {4 const refCallback = useCallback((node: HTMLDivElement | null) => {5 if (node) {6 console.log('Callback ref assigned:', node);7 } else {8 console.log('Callback ref removed');9 }10 }, []);1112 useLayoutEffect(() => {13 console.log('useLayoutEffect triggered');14 }, []);1516 useEffect(() => {17 console.log('useEffect triggered');18 }, []);1920 return <div ref={refCallback}>Tracked Element</div>;21 }2223 export default WhenCalled;
Console Output Order:
"Callback ref assigned: [div element]"
"useLayoutEffect triggered"
"useEffect triggered"
Solving Issues with Dynamic Elements
Problem: useRef
Fails with Changing Elements
Using useRef
with elements that dynamically change (e.g., swapping a <div>
for a <p>
) can cause issues because useRef holds onto the old element reference.
1 import { useCallback, useEffect, useRef, useState } from 'react';23 interface ResizeObserverOptions {4 elemRef: React.RefObject<HTMLElement>;5 onResize: ResizeObserverCallback;6 }78 function useResizeObserver({ elemRef, onResize }: ResizeObserverOptions) {9 useEffect(() => {10 const element = elemRef.current;11 if (!element) return;1213 const resizeObserver = new ResizeObserver(onResize);14 resizeObserver.observe(element);1516 return () => resizeObserver.unobserve(element);17 }, [onResize, elemRef]);18 }
Here, when toggling elements, ResizeObserver
may still track the removed element.
Solution: Using Callback Refs
Callback refs ensure proper reassignment when the element changes.
1 import { useCallback, useRef, useState } from 'react';23 function useResizeObserver(onResize: ResizeObserverCallback) {4 const roRef = useRef<ResizeObserver | null>(null);56 const attachResizeObserver = useCallback(7 (element: HTMLElement) => {8 const resizeObserver = new ResizeObserver(onResize);9 resizeObserver.observe(element);10 roRef.current = resizeObserver;11 },12 [onResize]13 );1415 const detachResizeObserver = useCallback(() => {16 roRef.current?.disconnect();17 }, []);1819 const refCb = useCallback(20 (element: HTMLElement | null) => {21 if (element) attachResizeObserver(element);22 else detachResizeObserver();23 },24 [attachResizeObserver, detachResizeObserver]25 );2627 return refCb;28 }2930 export default function App() {31 const [bool, setBool] = useState(false);3233 const handleResize = useCallback((entries: ResizeObserverEntry[]) => {34 console.log('Resize observed:', entries);35 }, []);3637 const resizeRef = useResizeObserver(handleResize);3839 return (40 <div>41 <button onClick={() => setBool((v) => !v)}>Toggle</button>42 {bool ? <p ref={resizeRef}>Text</p> : <div ref={resizeRef}>Div</div>}43 </div>44 );45 }
Here, the observer automatically attaches/detaches as elements change. The useResizeObserver
hook manages the ResizeObserver lifecycle, ensuring it tracks the correct element.
Combining Multiple Refs
Callback refs can also help when multiple refs need to be merged.
1 import { forwardRef, useCallback, useEffect, useRef } from 'react';23 function useCombinedRef<T>(...refs: (React.Ref<T> | null)[]) {4 return useCallback((element: T | null) => {5 refs.forEach((ref) => {6 if (!ref) return;7 if (typeof ref === 'function') ref(element);8 else (ref as React.MutableRefObject<T | null>).current = element;9 });10 }, refs);11 }1213 const Input = forwardRef<HTMLInputElement>((props, ref) => {14 const localRef = useRef<HTMLInputElement>(null);15 const combinedRef = useCombinedRef(ref, localRef);1617 useEffect(() => {18 console.log(localRef.current?.getBoundingClientRect());19 }, []);2021 return <input {...props} ref={combinedRef} />;22 });2324 export function UsageWithCombine() {25 const inputRef = useRef<HTMLInputElement | null>(null);26 return (27 <div>28 <Input ref={inputRef} />29 <button onClick={() => inputRef.current?.focus()}>Focus</button>30 </div>31 );32 }
This allows both internal logic and external props-based refs to function correctly.
React 19 Updates
React 19 improves callback refs by automatically cleaning up old references, simplifying code.
1 <input ref={(ref) => () => { /* Cleanup logic */ }} />
When to Use Callback Refs
- Use
useRef
for simple element access. - Use callback refs for complex lifecycle control, reusable hooks, or dynamic elements.
Optimized useCombinedRef
Hook
Below is an optimized implementation of useCombinedRef
, improving readability, performance, and maintainability.
1 import { useEffect, useRef } from 'react';2 import { forwardRef, useCallback } from 'react';34 type RefItem<T> =5 | ((element: T | null) => void)6 | React.MutableRefObject<T | null>7 | null8 | undefined;910 /**11 * Hook to combine multiple refs into one.12 * Supports both object refs and callback refs.13 */14 function useCombinedRef<T>(...refs: RefItem<T>[]) {15 return useCallback((element: T | null) => {16 refs.forEach((ref) => {17 if (!ref) return;18 if (typeof ref === 'function') {19 ref(element);20 } else {21 ref.current = element;22 }23 });24 }, [refs]);25 }2627 interface InputProps {28 value?: string;29 onChange?: React.ChangeEventHandler<HTMLInputElement>;30 }3132 /**33 * Custom Input component that supports multiple refs.34 */35 const Input = forwardRef<HTMLInputElement, InputProps>(function Input(36 props,37 ref38 ) {39 const inputRef = useRef<HTMLInputElement>(null);40 const combinedInputRef = useCombinedRef(ref, inputRef);4142 useEffect(() => {43 if (inputRef.current) {44 console.log('Input position:', inputRef.current.getBoundingClientRect());45 }46 }, []);4748 return <input {...props} ref={combinedInputRef} />;49 });5051 /**52 * Example usage of the Input component with combined refs.53 */54 export function UsageWithCombine() {55 const inputRef = useRef<HTMLInputElement | null>(null);5657 const focus = () => {58 inputRef.current?.focus();59 };6061 return (62 <div>63 <Input ref={inputRef} />64 <button onClick={focus}>Focus</button>65 </div>66 );67 }
Advantages of Using useCombinedRef
1. Supports Multiple Ref Types
- Works with both callback refs and object refs without conflicts.
- Ensures seamless integration with various ref-based APIs.
2. Improved Code Readability and Reusability
- The
useCombinedRef
function can be reused across multiple components. - Reduces redundant logic, keeping components clean and maintainable.
3. Prevents Unnecessary Re-Renders
- Uses
useCallback
to memoize the function, avoiding ref resets on re-renders. - Keeps reference handling stable for optimal performance.
4. Automatic Resource Management
- Ensures old references are properly updated or cleaned up.
- Helps prevent memory leaks in complex component structures.
5. Versatility in Component Design
- Useful for library authors and UI framework developers.
- Ensures existing logic remains intact while adding ref-based functionality.
Conclusion
Using useCombinedRef
provides a cleaner, more efficient approach to handling multiple refs in React components. Whether you're building reusable UI components or integrating third-party libraries, this hook ensures seamless ref management without unnecessary complexity. 🚀