principlejavascriptreactModerate
useRef vs useState — which to use for mutable values
Viewed 0 times
React 16.8+
useRefuseStatemutable valuere-render triggertimer refprevious valueDOM ref
Problem
Developers confuse useRef and useState. Using useState for values that don't need to trigger re-renders (DOM refs, timer IDs, previous values, third-party instances) causes unnecessary renders. Using useRef for values that should trigger renders causes stale UI.
Solution
Rule: useRef for values that are used for side effects but not for rendering. useState for values that determine what the UI shows.
// useRef: timer ID — changing it should not re-render
function Timer() {
const timerRef = useRef(null);
function start() {
timerRef.current = setInterval(tick, 1000); // no re-render
}
function stop() {
clearInterval(timerRef.current); // no re-render
}
}
// useRef: previous value — only used in effect, not in JSX
function usePrevious(value) {
const ref = useRef();
useEffect(() => { ref.current = value; }, [value]);
return ref.current; // previous render's value
}
// useState: display count — changing it must re-render
function Counter() {
const [count, setCount] = useState(0);
return <p>{count}</p>; // used in JSX → must be state
}
// useState: form input — shown to user
function Form() {
const [email, setEmail] = useState('');
return <input value={email} onChange={e => setEmail(e.target.value)} />;
}
// useRef: timer ID — changing it should not re-render
function Timer() {
const timerRef = useRef(null);
function start() {
timerRef.current = setInterval(tick, 1000); // no re-render
}
function stop() {
clearInterval(timerRef.current); // no re-render
}
}
// useRef: previous value — only used in effect, not in JSX
function usePrevious(value) {
const ref = useRef();
useEffect(() => { ref.current = value; }, [value]);
return ref.current; // previous render's value
}
// useState: display count — changing it must re-render
function Counter() {
const [count, setCount] = useState(0);
return <p>{count}</p>; // used in JSX → must be state
}
// useState: form input — shown to user
function Form() {
const [email, setEmail] = useState('');
return <input value={email} onChange={e => setEmail(e.target.value)} />;
}
Why
useRef holds a mutable object { current: value } that persists across renders. Mutating .current doesn't trigger re-renders. useState triggers a re-render and schedules a new render cycle when the setter is called. The distinction maps to: does the UI need to change when this value changes?
Gotchas
- Reading ref.current during render is unreliable in concurrent mode — refs are for effects and event handlers
- useRef(initialValue) only uses the initial value on the first render — subsequent initialValue expressions are evaluated but discarded
- Don't write to ref.current during render — only in effects and event handlers
- useRef is also used for DOM access — assign to the ref prop of a JSX element
Code Snippets
useRef vs useState decision
// Decision rule:
// Does changing this value need to update the UI? → useState
// Is this value only used in effects/handlers, not JSX? → useRef
const count = useState(0); // shown in JSX → state
const timerId = useRef(null); // used in clearInterval → refContext
When deciding whether a mutable value should be stored in useRef or useState
Revisions (0)
No revisions yet.