How to Optimize Forwarding Refs in React

Optimizing forwarding refs in React is crucial for maintaining high-performing applications, especially when building reusable component libraries. This article explores practical strategies to optimize React’s forwardRef API, including combining it with React.memo to prevent redundant re-renders, using the useImperativeHandle hook to limit exposed DOM properties, and avoiding performance pitfalls like inline ref callbacks.

Combine forwardRef with React.memo

By default, wrapping a component in React.forwardRef does not prevent it from re-rendering when its parent re-renders. To optimize performance and prevent unnecessary renders, you should combine forwardRef with React.memo.

import React, { forwardRef, memo } from 'react';

const OptimizedInput = forwardRef((props, ref) => {
  return <input ref={ref} {...props} />;
});

export default memo(OptimizedInput);

Note: Order matters. Always pass the forwardRef component inside memo to ensure React correctly tracks the component’s reference while maintaining memoization.

Limit Exposed DOM Nodes with useImperativeHandle

Exposing a raw DOM node directly to a parent component can lead to unintended side effects, such as direct style mutations or unauthorized state changes. To optimize component encapsulation and reduce rendering bugs, use useImperativeHandle to limit the API exposed to the parent.

import React, { forwardRef, useImperativeHandle, useRef } from 'react';

const CustomInput = forwardRef((props, ref) => {
  const inputRef = useRef();

  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    }
  }));

  return <input ref={inputRef} {...props} />;
});

By using this hook, the parent component can only call .focus() on the ref, rather than having full access to the underlying <input> element’s DOM properties.

Avoid Inline Ref Callbacks

When you pass an inline function as a ref callback inside a forwarded component, React will execute the callback twice during every update: first with null, and then with the DOM element.

To optimize this and avoid unnecessary function instantiations, use a stable ref object created with useRef, or wrap your custom callback function in a useCallback hook.

// Avoid this:
<input ref={(node) => { console.log(node); }} />

// Optimize with useCallback:
const handleRef = useCallback((node) => {
  if (node !== null) {
    console.log(node);
  }
}, []);

return <input ref={handleRef} />;

Handle Conditional Ref Assignment Wisely

If you only need to forward a ref under specific conditions, avoid dynamically switching the ref prop value between null and the ref object during renders. Doing so forces React to detach and reattach the ref on every render cycle. Instead, keep the ref reference stable and handle conditional logic inside the ref handler or the component’s lifecycle hooks.