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