Understanding useRef and Component Re-renders
A common misconception in React is expecting a component to re-render when a useRef
value changes. This doesn’t happen because refs are designed specifically to persist values without triggering re-renders.
The Core Issue
function Counter() {
const countRef = useRef(0);
const increment = () => {
countRef.current += 1;
// ❌ Expecting component to re-render automatically
console.log(countRef.current); // Value updates but no re-render
};
return (
<div>
<button onClick={increment}>Increment</button>
<div>Count: {countRef.current}</div> {/* Won't update! */}
</div>
);
}
Why Refs Don’t Trigger Re-renders
- Mutable Container:
useRef
returns a mutable object ({ current: value }
) - No React Tracking: Changes to
.current
aren’t observed by React - Performance Purpose: Designed for values that shouldn’t trigger updates
- Render Isolation: Values persist across renders without affecting output
When to Use Refs (Properly)
1. Storing Mutable Values Without Re-renders
function Timer() {
const intervalRef = useRef();
useEffect(() => {
intervalRef.current = setInterval(() => {
console.log('Tick');
}, 1000);
return () => clearInterval(intervalRef.current);
}, []);
}
2. Accessing DOM Elements
function InputFocus() {
const inputRef = useRef();
const focusInput = () => {
inputRef.current.focus();
};
return (
<>
<input ref={inputRef} />
<button onClick={focusInput}>Focus</button>
</>
);
}
3. Keeping Previous Values
function Counter() {
const [count, setCount] = useState(0);
const prevCountRef = useRef();
useEffect(() => {
prevCountRef.current = count;
});
return (
<div>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
<div>Now: {count}, Before: {prevCountRef.current}</div>
</div>
);
}
Solutions When You Need Re-renders
1. Combine with useState (Recommended)
function Counter() {
const [count, setCount] = useState(0);
const countRef = useRef(count);
const increment = () => {
const newCount = countRef.current + 1;
countRef.current = newCount;
setCount(newCount); // ✅ Triggers re-render
};
return (
<div>
<button onClick={increment}>Increment</button>
<div>Count: {count}</div>
</div>
);
}
2. Force Update Pattern (Rare Cases)
function useForceUpdate() {
const [, setTick] = useState(0);
return () => setTick(t => t + 1);
}
function Counter() {
const countRef = useRef(0);
const forceUpdate = useForceUpdate();
const increment = () => {
countRef.current += 1;
forceUpdate(); // ✅ Manual re-render trigger
};
return /* ... */;
}
Key Differences: useRef vs useState
Feature | useRef | useState |
---|---|---|
Triggers re-render | ❌ No | ✅ Yes |
Value persistence | ✅ Across renders | ✅ Across renders |
Best for | DOM refs, intervals | UI state |
Updates | Synchronous | Asynchronous |
Read value | .current property | State variable |
Common Pitfalls
- Expecting UI Updates from ref changes
- Overusing refs for state that should trigger UI
- Race Conditions when mixing refs and state
- Stale Closures in callbacks using ref values
Best Practices
- Use refs for values that shouldn’t affect rendering
- Combine with state when you need both persistence and updates
- Avoid derived state in refs – use
useMemo
instead - Document ref usage since they’re less visible in the component
- Consider alternatives like context or state management for complex cases
Remember: Refs are escape hatches from React’s reactivity system. Use them when you need to store values that shouldn’t trigger re-renders, and combine them with state when you need both persistence and UI updates.