Using useRef
Without Triggering Re-renders
A common React misunderstanding occurs when developers use useRef
to store values but expect the component to re-render when the ref’s value changes. This doesn’t happen because refs are designed to be mutable storage that doesn’t trigger reactivity.
The Problem
function Counter() {
const countRef = useRef(0);
const increment = () => {
countRef.current++; // ❌ Mutation won't trigger re-render
console.log(countRef.current); // Value changes but UI doesn't update
};
return (
<div>
<button onClick={increment}>Increment</button>
<span>Count: {countRef.current}</span> {/* Stays at 0 */}
</div>
);
}
Why this happens:
useRef
provides a mutable object ({ current: value }
)- Mutating
current
doesn’t trigger component re-renders - Refs are designed for storing values without reactivity
- The component only renders when state/props change
Correct Solutions
1. Use State for Reactive Values (Recommended)
function Counter() {
const [count, setCount] = useState(0); // ✅ Triggers re-renders
const increment = () => {
setCount(c => c + 1); // Updates UI
};
return (
<div>
<button onClick={increment}>Increment</button>
<span>Count: {count}</span> {/* Updates properly */}
</div>
);
}
2. Combine Ref and State
function Counter() {
const [_, forceUpdate] = useState(); // For re-renders
const countRef = useRef(0);
const increment = () => {
countRef.current++;
forceUpdate({}); // ✅ Force update when needed
};
return (
<div>
<button onClick={increment}>Increment</button>
<span>Count: {countRef.current}</span>
</div>
);
}
3. Proper useRef Use Cases
function Form() {
const inputRef = useRef(null); // ✅ Good for DOM references
const renderCount = useRef(0); // ✅ Good for tracking values
useEffect(() => {
renderCount.current++; // Doesn't need re-render
inputRef.current.focus(); // DOM operation
}, []);
return <input ref={inputRef} />;
}
When to Use Refs vs State
useRef | useState |
---|---|
DOM element references | Values that affect rendering |
Persistent values between renders | Reactive data |
Storing previous values | Form inputs |
Interval/Timeout IDs | Any visual state |
Mutable variables that shouldn’t trigger updates | Data that affects UI |
Common Mistakes
- Expecting re-renders from ref changes:
const valueRef = useRef(0);
valueRef.current = 42; // ❌ No re-render
- Using refs for derived state:
const filteredRef = useRef(items.filter(/*...*/)); // ❌ Use useMemo
- Overusing forceUpdate patterns:
// ❌ Anti-pattern in most cases
const [_, forceUpdate] = useState();
Best Practices
- Use state for any data that should trigger UI updates
- Use refs for:
- Accessing DOM elements
- Storing mutable values that shouldn’t affect rendering
- Keeping previous values
- Storing timeout/interval IDs
- Avoid forceUpdate – Rethink your component structure instead
- Combine with useEffect for side effects:
const prevCountRef = useRef();
useEffect(() => {
prevCountRef.current = count; // Track previous value
});
Advanced Patterns
1. Previous Value Tracking
function Counter({ count }) {
const prevCountRef = useRef();
useEffect(() => {
prevCountRef.current = count;
}, [count]);
const prevCount = prevCountRef.current;
return <div>Now: {count}, Before: {prevCount}</div>;
}
2. Stable Function References
function Form() {
const inputRef = useRef(null);
// ✅ Stable function reference
const focusInput = useCallback(() => {
inputRef.current?.focus();
}, []);
return (
<>
<input ref={inputRef} />
<button onClick={focusInput}>Focus</button>
</>
);
}
3. Measuring DOM Elements
function Box() {
const divRef = useRef(null);
const [width, setWidth] = useState(0);
useLayoutEffect(() => {
if (divRef.current) {
setWidth(divRef.current.offsetWidth);
}
}, []);
return <div ref={divRef}>Width: {width}px</div>;
}