useRef
Remembering something without waking React up
useRef is a little box React keeps between passes. Putting something in it does not cause a redraw – which is exactly the point.
const box = useRef(initial);
box.current // look inside
box.current = value; // put something in -> React does NOT redraw useState is a scoreboard: write on it and everyone looks. useRef is a note in your pocket: it stays with you, but nobody notices when you change it.
Purpose 1: touching an element on the page
Sometimes you have to tell the browser something directly: “put the cursor here”, “scroll there”, “play this video”. For that you need the real element – and React puts it in the box for you.
function Search() {
const inputRef = useRef<HTMLInputElement>(null);
return (
<>
<input ref={inputRef} />
<button onClick={() => inputRef.current?.focus()}>Focus</button>
</>
);
} On the first pass the element does not exist yet – React builds it afterwards. So you reach for it with ?. or from inside a click handler.
Purpose 2: remembering something quietly
// The id of a timer, so you can cancel it later
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
function onType() {
if (timerRef.current) clearTimeout(timerRef.current);
timerRef.current = setTimeout(search, 500);
}
// How many times did it draw? (just to look at, not to display)
const counter = useRef(0);
counter.current += 1; State or ref? One single question
const countRef = useRef(0);
function click() {
countRef.current += 1;
}
return <p>{countRef.current}</p>; const [count, setCount] = useState(0);
function click() {
setCount((c) => c + 1);
}
return <p>{count}</p>; On the left the number in the box really does go up – but React never redraws, so you always see 0. Visible → state. Invisible → ref.
- +
Touching an element:
focus(), scrolling, measuring, controlling a video - +
Keeping timer ids around
- +
Remembering the previous value of something
- +
Holding objects from other libraries (a map, a chart, an editor)
- −
Values that should appear on screen – that is state
- −
As a trick to “save” redraws – the display then freezes
- −
Reading or writing in the middle of drawing
Read and write refs in click handlers or in useEffect – not directly in the function body. Otherwise the result depends on when React happens to draw, and React cannot promise you that.
▸ In more depth: callback refs and ref as a prop optional
Instead of a box you can also hand the ref attribute a function. React calls it as soon as the element lands on the page – and again when it leaves. Handy when you want to measure something right afterwards.
function Box() {
const [height, setHeight] = useState(0);
return (
<div
ref={(node) => {
if (node) setHeight(node.getBoundingClientRect().height);
// React 19: whatever you return here serves as the cleanup
}}
>
…
</div>
);
} ref is now an ordinary prop: function Input({ ref, ...rest }) { … }. Older code still uses forwardRef for this – that keeps working, it is just no longer necessary.
Does it stick?
4 questions on this lesson. Wrong answers show up in your stats.
Now write it yourself
Two jobs: the button should put the cursor in the input, and next to it you should count how often the component drew – without the counting itself causing a draw.
- □Clicking
Focusputs the cursor in the field - □The number next to
Rendersgoes up as you type - □The counting itself causes no extra draw
import { useRef, useState } from "react"; export default function App() { const [text, setText] = useState(""); // TODO: ref for the input field // TODO: ref for the counter return ( <div style={{ fontFamily: "system-ui", padding: 16 }}> <input aria-label="field" value={text} onChange={(e) => setText(e.target.value)} />{" "} <button onClick={() => { /* TODO: focus */ }}>Focus</button> <p data-testid="renders">Renders: 0</p> </div> ); }