useRef creates a mutable container (an object with a .current property) that persists across renders without causing re-renders when its value changes. It's one of the most versatile hooks in React.
Think of it like this: Imagine a sticky note that you attach to a component. You can write any value on it, and even when the component re-renders, the note stays there with whatever you last wrote — but looking at the note doesn't trigger a new render.
Two primary use cases:
1. DOM Access — get a reference to a DOM element:
1import { useRef, useEffect } from "react";23function TextInput() {4 const inputRef = useRef<HTMLInputElement>(null);56 // Focus input on mount7 useEffect(() => {8 inputRef.current?.focus();9 }, []);1011 return (12 <div>13 <input ref={inputRef} placeholder="Auto-focused input" />14 <button onClick={() => inputRef.current?.select()}>15 Select text16 </button>17 </div>18 );19}
2. Storing mutable values — keep values that don't trigger re-renders:
1function Timer() {2 const [count, setCount] = useState(0);3 const intervalRef = useRef<NodeJS.Timeout | null>(null);4 const renderCount = useRef(0);56 // Track render count without re-renders7 useEffect(() => {8 renderCount.current += 1;9 });1011 // Store interval ID for cleanup12 const startTimer = () => {13 intervalRef.current = setInterval(() => {14 setCount(c => c + 1);15 }, 1000);16 };1718 const stopTimer = () => {19 if (intervalRef.current) {20 clearInterval(intervalRef.current);21 intervalRef.current = null;22 }23 };2425 return (26 <div>27 <p>Count: {count} (Rendered {renderCount.current} times)</p>28 <button onClick={startTimer}>Start</button>29 <button onClick={stopTimer}>Stop</button>30 </div>31 );32}
Key points:
.current changes do NOT trigger re-renders — this is the fundamental difference from useStateuseRef(initialValue) — the argument becomes .currentuseRef vs useState comparison:
| Feature | useRef | useState |
|---------|--------|----------|
| Re-render on change | No | Yes |
| Access current value | .current | State variable directly |
| Best for | DOM refs, timers, mutable data | UI state that should trigger renders |
Common mistakes:
.current in render — the value is always the value from the last render, not the "latest"Advanced pattern — previous value:
1function usePrevious<T>(value: T): T | undefined {2 const ref = useRef<T>();3 useEffect(() => {4 ref.current = value;5 });6 return ref.current;7}89// Usage10function Counter() {11 const [count, setCount] = useState(0);12 const prevCount = usePrevious(count);13 return <p>Now: {count}, Before: {prevCount}</p>;14}