How to Optimize useRef Hook in React
The useRef hook in React is a powerful tool for
persisting values across renders without triggering a re-render and for
directly accessing DOM elements. This article provides a practical guide
on how to optimize your use of useRef, covering lazy
initialization of expensive values, avoiding rendering phase side
effects, and preventing memory leaks through proper cleanup.
1. Implement Lazy Initialization for Expensive Objects
When you pass an initial value directly to useRef(),
React executes that initial value expression on every single render,
even though the ref’s value is only set once during the initial
mount.
For example, this instantiates a new class on every render:
// Unoptimized: Expensive object is created on every render
const myRef = useRef(new ExpensiveClass());To optimize this and ensure the expensive object is only created
once, use lazy initialization by checking if the ref is
null:
// Optimized: Expensive object is created only once
const myRef = useRef(null);
if (myRef.current === null) {
myRef.current = new ExpensiveClass();
}2. Avoid
Reading or Writing ref.current During Render
React expects components to behave like pure functions with respect
to rendering. Reading or writing to useRef during the
render phase can lead to unpredictable behavior, especially in React’s
Concurrent Mode where renders can be aborted or retried.
Incorrect:
function MyComponent() { const renderCount = useRef(0); renderCount.current++; // Modifying ref during render return <div>{renderCount.current}</div>; // Reading ref during render }Correct: Always modify or read refs inside
useEffectblocks or event handlers.function MyComponent() { const renderCount = useRef(0); useEffect(() => { renderCount.current++; }); return <div>Render tracked safely.</div>; }
3. Use
useRef to Prevent Unnecessary Re-renders
Use useRef instead of useState for any data
that does not affect what is displayed on the screen. Updating state
triggers a component re-render, whereas updating a ref does not.
Common use cases for optimization include storing: * Timer or interval IDs * Tracking variables (e.g., whether a component has mounted) * Storing previous prop values for comparison
function Timer() {
// Storing the interval ID in a ref prevents re-renders when the interval starts/stops
const intervalRef = useRef(null);
const startTimer = () => {
if (intervalRef.current !== null) return;
intervalRef.current = setInterval(() => {
// timer logic
}, 1000);
};
const stopTimer = () => {
clearInterval(intervalRef.current);
intervalRef.current = null;
};
return (
<div>
<button onClick={startTimer}>Start</button>
<button onClick={stopTimer}>Stop</button>
</div>
);
}4. Properly Clean Up Refs to Prevent Memory Leaks
If you use useRef to hold third-party library instances,
timers, or WebSocket connections, always clean them up when the
component unmounts. Failing to do so can cause memory leaks.
useEffect(() => {
const socket = new WebSocket('ws://example.com');
socketRef.current = socket;
return () => {
// Cleanup on unmount
if (socketRef.current) {
socketRef.current.close();
}
};
}, []);