How to Optimize React Refs for Performance
React Refs (useRef) are powerful tools that allow you to
directly access DOM nodes and persist mutable values across renders
without triggering a re-render. However, inefficient use of refs can
lead to memory leaks, redundant computations, and unpredictable
component behavior. This article provides a direct, practical guide on
how to optimize React Refs, focusing on avoiding common pitfalls like
inline callback recreation, managing cleanup, and using callback refs
efficiently.
Avoid Inline Callback Refs
When you pass an inline function as a ref, React calls it twice
during every update: first with null and then with the DOM
element. This happens because a new function instance is created on
every render, forcing React to clear the old ref and set up the new
one.
To optimize this, define the callback ref using
useCallback so that the function identity remains stable
across renders.
import { useCallback, useRef } from 'react';
function OptimizedInput() {
// Use useCallback to memoize the ref callback
const inputRef = useCallback((node) => {
if (node !== null) {
node.focus();
}
}, []); // Empty dependency array ensures stability
return <input ref={inputRef} type="text" />;
}Don’t Use Refs for Render-Critical Data
Refs do not trigger a re-render when their .current
property changes. If you store data in a ref that is directly rendered
in the JSX, your UI will become out of sync with your application
state.
- Use State (
useState): For data that directly impacts what is rendered on the screen. - Use Refs (
useRef): For instance variables, timers, scroll positions, and direct DOM interactions.
// BAD: UI will not update when count changes
const countRef = useRef(0);
return <button onClick={() => countRef.current++}>{countRef.current}</button>;
// GOOD: Use state for values rendered in the UI
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;Clean Up Refs to Prevent Memory Leaks
When attaching external libraries, event listeners, or observers
(like IntersectionObserver or ResizeObserver)
to a ref, you must clean them up when the component unmounts to prevent
memory leaks.
Always perform cleanup inside the return function of a
useEffect hook.
import { useEffect, useRef } from 'react';
function ScrollTracker() {
const containerRef = useRef(null);
useEffect(() => {
const element = containerRef.current;
if (!element) return;
const handleScroll = () => {
console.log('Scrolled:', element.scrollTop);
};
element.addEventListener('scroll', handleScroll);
// Cleanup to prevent memory leaks
return () => {
element.removeEventListener('scroll', handleScroll);
};
}, []);
return <div ref={containerRef} style={{ overflowY: 'scroll', height: '100px' }}>Content</div>;
}Optimize Parent-Child Communication with useImperativeHandle
Exposing an entire DOM node to a parent component via
forwardRef can break encapsulation and lead to accidental
mutations. You can optimize and secure this relationship by using
useImperativeHandle to limit what the parent component can
access.
import { useRef, useImperativeHandle, forwardRef } from 'react';
const CustomInput = forwardRef((props, ref) => {
const inputRef = useRef(null);
// Only expose specific methods to the parent component
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
},
clear: () => {
inputRef.current.value = '';
}
}));
return <input ref={inputRef} type="text" />;
});Lazy Initialize Mutable Values
If you need to store a computationally expensive object (like a heavy
class instance) in a ref, initializing it directly inside
useRef will recreate the object on every render, even
though the ref only keeps the value from the initial render.
// BAD: ExpensiveClass is instantiated on every single render
const apiRef = useRef(new ExpensiveClass());To optimize this, initialize the ref with null and
instantiate the object conditionally.
// GOOD: ExpensiveClass is only instantiated once
const apiRef = useRef(null);
function getApi() {
if (apiRef.current === null) {
apiRef.current = new ExpensiveClass();
}
return apiRef.current;
}