JavaScript Development Space

Mastering React Refs: Advanced Techniques with useCombinedRef Hook

Add to your RSS feed6 February 20257 min read
Mastering React Refs: Advanced Techniques with useCombinedRef Hook

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

tsx
1 import React, { useCallback, useState } from 'react';
2
3 function MountUnmountTracker() {
4 const [isVisible, setIsVisible] = useState(false);
5
6 const handleRef = useCallback((node: HTMLDivElement | null) => {
7 if (node) {
8 console.log('Mounted:', node);
9 } else {
10 console.log('Unmounted');
11 }
12 }, []);
13
14 return (
15 <div>
16 <button onClick={() => setIsVisible((prev) => !prev)}>
17 {isVisible ? 'Hide' : 'Show'} Element
18 </button>
19 {isVisible && <div ref={handleRef}>Tracked Element</div>}
20 </div>
21 );
22 }
23
24 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.

tsx
1 import React, { useState, useCallback, useReducer } from 'react';
2
3 function Basic() {
4 const [showDiv, setShowDiv] = useState(false);
5 const [, forceRerender] = useReducer((v) => v + 1, 0);
6
7 const toggleDiv = () => setShowDiv((prev) => !prev);
8
9 const refCallback = useCallback((node: HTMLDivElement | null) => {
10 console.log('div', node);
11 }, []);
12
13 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 }
21
22 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:

  1. Callback refs fire before useLayoutEffect and useEffect.
  2. useLayoutEffect runs after DOM updates but before the browser repaints.
  3. useEffect runs after the browser has painted.
tsx
1 import React, { useEffect, useLayoutEffect, useCallback } from 'react';
2
3 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 }, []);
11
12 useLayoutEffect(() => {
13 console.log('useLayoutEffect triggered');
14 }, []);
15
16 useEffect(() => {
17 console.log('useEffect triggered');
18 }, []);
19
20 return <div ref={refCallback}>Tracked Element</div>;
21 }
22
23 export default WhenCalled;

Console Output Order:

  1. "Callback ref assigned: [div element]"
  2. "useLayoutEffect triggered"
  3. "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.

tsx
1 import { useCallback, useEffect, useRef, useState } from 'react';
2
3 interface ResizeObserverOptions {
4 elemRef: React.RefObject<HTMLElement>;
5 onResize: ResizeObserverCallback;
6 }
7
8 function useResizeObserver({ elemRef, onResize }: ResizeObserverOptions) {
9 useEffect(() => {
10 const element = elemRef.current;
11 if (!element) return;
12
13 const resizeObserver = new ResizeObserver(onResize);
14 resizeObserver.observe(element);
15
16 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.

tsx
1 import { useCallback, useRef, useState } from 'react';
2
3 function useResizeObserver(onResize: ResizeObserverCallback) {
4 const roRef = useRef<ResizeObserver | null>(null);
5
6 const attachResizeObserver = useCallback(
7 (element: HTMLElement) => {
8 const resizeObserver = new ResizeObserver(onResize);
9 resizeObserver.observe(element);
10 roRef.current = resizeObserver;
11 },
12 [onResize]
13 );
14
15 const detachResizeObserver = useCallback(() => {
16 roRef.current?.disconnect();
17 }, []);
18
19 const refCb = useCallback(
20 (element: HTMLElement | null) => {
21 if (element) attachResizeObserver(element);
22 else detachResizeObserver();
23 },
24 [attachResizeObserver, detachResizeObserver]
25 );
26
27 return refCb;
28 }
29
30 export default function App() {
31 const [bool, setBool] = useState(false);
32
33 const handleResize = useCallback((entries: ResizeObserverEntry[]) => {
34 console.log('Resize observed:', entries);
35 }, []);
36
37 const resizeRef = useResizeObserver(handleResize);
38
39 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.

tsx
1 import { forwardRef, useCallback, useEffect, useRef } from 'react';
2
3 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 }
12
13 const Input = forwardRef<HTMLInputElement>((props, ref) => {
14 const localRef = useRef<HTMLInputElement>(null);
15 const combinedRef = useCombinedRef(ref, localRef);
16
17 useEffect(() => {
18 console.log(localRef.current?.getBoundingClientRect());
19 }, []);
20
21 return <input {...props} ref={combinedRef} />;
22 });
23
24 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.

tsx
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.

tsx
1 import { useEffect, useRef } from 'react';
2 import { forwardRef, useCallback } from 'react';
3
4 type RefItem<T> =
5 | ((element: T | null) => void)
6 | React.MutableRefObject<T | null>
7 | null
8 | undefined;
9
10 /**
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 }
26
27 interface InputProps {
28 value?: string;
29 onChange?: React.ChangeEventHandler<HTMLInputElement>;
30 }
31
32 /**
33 * Custom Input component that supports multiple refs.
34 */
35 const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
36 props,
37 ref
38 ) {
39 const inputRef = useRef<HTMLInputElement>(null);
40 const combinedInputRef = useCombinedRef(ref, inputRef);
41
42 useEffect(() => {
43 if (inputRef.current) {
44 console.log('Input position:', inputRef.current.getBoundingClientRect());
45 }
46 }, []);
47
48 return <input {...props} ref={combinedInputRef} />;
49 });
50
51 /**
52 * Example usage of the Input component with combined refs.
53 */
54 export function UsageWithCombine() {
55 const inputRef = useRef<HTMLInputElement | null>(null);
56
57 const focus = () => {
58 inputRef.current?.focus();
59 };
60
61 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. 🚀

JavaScript Development Space

© 2025 JavaScript Development Space - Master JS and NodeJS. All rights reserved.