useRef
one box that survives every render and never causes one. the other half of
useState: change it and the screen does not notice.
the same object every time
useRef(initial) returns { current: initial }. react hands you the exact same
object on every render, and mutating .current is a plain assignment. nothing
is scheduled, nothing re-renders, nothing is compared.
that is the whole hook. everything useful about it follows from those two properties: it persists, and it is invisible to react.
the trap: mutating and expecting the screen to move
const count = useRef(0);
return <button onClick={() => count.current++}>{count.current}</button>;
click it ten times and the label still reads 0. the value really did become 10, but nothing asked react to render again, so the dom was never touched.
if the value belongs on screen, it is state:
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
the test is simple. does anyone need to see it? state. does only your code need to read it? ref.
what it is actually for
a dom node.
const input = useRef(null);
useEffect(() => input.current.focus(), []);
return <input ref={input} />;
note the timing. during the first render input.current is still null,
because the element does not exist yet. react fills it in after committing to
the dom, which is why the read lives inside an effect and not in the render
body.
a value that must persist without rendering. a timer id you need in order to clear it, the previous value of a prop, a websocket instance. all of them have to survive re-renders, and none of them belong on screen.
const timer = useRef(null);
useEffect(() => {
timer.current = setInterval(tick, 1000);
return () => clearInterval(timer.current);
}, []);
a plain let inside the component would be reset on every render. state would
work, but every assignment would trigger a render you do not want.
when not to use it
- the value shows up in the output. you will mutate it and spend an
afternoon wondering why the ui is stale. that is what
useStateis for. - you are reading or writing it during render. refs are for effects and
handlers. touching
.currentwhile rendering makes the render impure and breaks under concurrent rendering. - you are using it to dodge a dependency array. stuffing a value in a ref to
keep
useEffectquiet hides the problem instead of fixing it, and the effect goes on reading a value nobody updated.