Explain how useState triggers re-renders and is used for UI state, while useRef holds a mutable value that persists across renders without re-rendering — and when each is the correct tool (DOM refs, timers, previous values, avoiding stale closures, and render-driving state).
What’s the difference between useRef and useState? When should each be used?
Use guided tracks for structured prep, then practice company-specific question sets when you want targeted interview coverage.
Short answeruseState is for data that should drive what you render (changing it re-renders). useRef is for data you want to persist across renders but changing it should not trigger a re-render (a mutable container).
Hook | What it stores | What happens when it changes |
|---|---|---|
useState | A render-driving value (UI state) | ✅ Triggers a re-render |
useRef | A mutable container: { current: ... } | ❌ Does NOT trigger a re-render |
First-principles mental model
React re-renders when you want the UI to reflect new data. So ask yourself one question:
"Should changing this value update what the user sees right now?"
If yes → useState.
If no (but you still need the value to persist) → useRef.
function Example() {
const [count, setCount] = useState(0); // UI should update when this changes
const clicksRef = useRef(0); // internal bookkeeping; UI doesn't need it
const onClick = () => {
clicksRef.current += 1; // won't re-render
setCount((c) => c + 1); // will re-render
};
return (
<button onClick={onClick}>
Count: {count}
</button>
);
}
Common useState use cases
Use useState when the value affects rendering: text, visibility, selection, form inputs, loading state, errors, filters, toggles, etc.
Scenario | Correct tool | Why |
|---|---|---|
Show/hide a modal | useState | UI must update immediately |
Form input value | useState | Render depends on current value |
Loading / error / fetched data | useState | UI should reflect async results |
Selected tab / filter | useState | Directly affects what is shown |
Common useRef use cases
Use useRef when you need a value to persist, but you don’t want a render every time it changes.
Scenario | Why useRef fits | Example |
|---|---|---|
DOM element access | Imperative access to a node | focus(), scrollIntoView() |
Timer / interval IDs | Store mutable IDs across renders | setTimeout / setInterval cleanup |
Latest value for async callbacks | Avoid stale closures without rerendering | read latest state in an interval |
Previous value tracking | Persist last render's value | prevCount.current |
function Search() {
const inputRef = useRef(null);
return (
<div>
<input ref={inputRef} />
<button onClick={() => inputRef.current?.focus()}>
Focus input
</button>
</div>
);
}
The big trap: using useRef as "state"
People do this to avoid re-renders, but it’s usually a bug factory. If the UI depends on it and you store it in a ref, the UI won’t update and you’ll get out-of-sync screens.
function Bad() {
const valueRef = useRef('');
return (
<div>
<input onChange={(e) => (valueRef.current = e.target.value)} />
<p>Typed: {valueRef.current}</p> {/* ❌ UI won't update reliably */}
</div>
);
}
function Good() {
const [value, setValue] = useState('');
return (
<div>
<input value={value} onChange={(e) => setValue(e.target.value)} />
<p>Typed: {value}</p> {/* ✅ UI updates */}
</div>
);
}
Rule of thumb that wins interviews
useState = "React should re-render when this changes."
useRef = "I need to remember something between renders, but it’s not part of the UI."
Bonus: combining both (best of both worlds)
Sometimes you want rendering plus a latest value for async callbacks (avoiding stale closures). Keep the UI in state and mirror it into a ref:
function Example() {
const [count, setCount] = useState(0);
const countRef = useRef(count);
useEffect(() => {
countRef.current = count;
}, [count]);
useEffect(() => {
const id = setInterval(() => {
console.log('latest:', countRef.current);
}, 1000);
return () => clearInterval(id);
}, []);
return <button onClick={() => setCount((c) => c + 1)}>Count: {count}</button>;
}
useState holds render-driving state and triggers re-renders. useRef holds a mutable value that persists across renders but does not trigger re-renders. Use state for anything that affects UI, and refs for DOM access, timers, latest values in async callbacks, and other non-visual bookkeeping.