Not everything a component remembers belongs on screen. An interval ID, a WebSocket, a scroll position to restore, a flag saying whether this is the first render — the component needs these across renders, but no pixel depends on them. In a class they were instance fields; in a function component they go in a ref. A plain variable will not do: the function body runs again on every render, so let id = null starts over each time, while useRef hands back the same box.
function Timer() {
const [n, setN] = useState(0);
const tid = useRef(null);
useEffect(() => {
tid.current = setInterval(() => setN(v => v + 1), 1000);
return () => clearInterval(tid.current); // also stops it on unmount
}, []);
return <button onClick={() => clearInterval(tid.current)}>Stop at {n}</button>;
}Putting tid in state would be wrong twice over: setting it would schedule a pointless render, and the value would be read one render behind whenever two handlers ran in the same batch.
The same trick gives you the value a prop had last time, which React 7,897 does not otherwise offer. Write the current value into the ref inside an effect with no dependency array; during the next render the ref still holds the previous one.
function usePrevious(value) {
const ref = useRef(undefined);
useEffect(() => { ref.current = value; }); // runs after every commit
return ref.current; // the value from the render before
}A ref used this way is invisible to React, and that cuts both ways: nothing re-renders when you change it, and nothing tells you when it changed. A useful test is to ask whether the rendered output would differ if the value changed. If it would, the value is state and you picked the wrong tool.