Lesson 4 · Hooks in depth
Refs (useRef)
Access to the DOM, values without re-rendering, ref as a prop and useImperativeHandle.
Loading lesson…
Lesson 4 · Hooks in depth
Access to the DOM, values without re-rendering, ref as a prop and useImperativeHandle.
Loading lesson…
In the lesson on state we said that state is a notebook React keeps for the cook. Every entry in the notebook means a new round of cooking – a new render. That's exactly what we want for things that the guests see: the number in the cart, the text in a field, an open menu.
Picture it as the specials board on the restaurant wall. When something changes, the board has to be rewritten so the guests see it up to date. But rewriting takes work.
The cook also needs to remember things the guests will never see: "the oven timer is number 3", "the last order came in at 6:42 pm". They're not going to rewrite the board because of that. They jot it on a slip of paper and drop it in the drawer under the counter. They can reach into the drawer any time, nobody notices and nothing gets rewritten.
That drawer is a ref in React. It remembers a value between renders (like state), but changing it doesn't trigger a render.
A ref has a second use. Normally React puts the page together for you and you can't get at the individual HTML elements – and mostly you don't need to. But there are things that can't be done by description ("what the page should look like"): "put the cursor in this field", "scroll here", "measure how big this box is". For that you need a key to a particular door – a reference to a real element on the page. React hands you that in a ref too.
A typical case for the drawer: a stopwatch with Start and Stop buttons. You start the interval in one handler and have to stop it in another. To stop it you need its ID, which setInterval returns. Where do you store it?
function Stopwatch() {const [seconds, setSeconds] = useState(0) // the board – the user sees itconst intervalRef = useRef<number | null>(null) // the drawer – a technical detailfunction start() {intervalRef.current = window.setInterval(() => {setSeconds((s) => s + 1)}, 1000)}function stop() {if (intervalRef.current !== null) {clearInterval(intervalRef.current)intervalRef.current = null}}return (<><p>{seconds} s</p><button onClick={start}>Start</button><button onClick={stop}>Stop</button></>)}
What the ref does here:
useRef(null) creates an object { current: null }. On every render React gives you back the same object – it's always the same drawer.intervalRef.current at any time. React won't find out and re-renders nothing.Why not some other way?
| Where to store the ID | What happens |
|---|---|
let id | The variable is created again on every render (the cook without a memory). After the first tick the ID is lost and Stop stops nothing. |
useState | It works, but storing the ID triggers a needless render – the user never sees the ID. You rewrite the board because of a slip of paper for the drawer. |
useRef | It remembers and doesn’t re-render. Correct. |
| useState (the board) | useRef (the drawer) | |
|---|---|---|
| A change causes a render | Yes | No |
| The value in a handler | A snapshot from the render | Always current (.current) |
| How it changes | Only through the setter | By writing directly to .current |
| Use it for | Data shown in the UI | Timer IDs, page elements, library instances, previous values |
What you see: Two counters. The upper one is in state (the board), the lower one in a ref (the drawer). The text next to the ref shows the value the ref had at the last render. At the bottom is the component's number of renders.
Try it:
The takeaway: A ref remembers a value just like state, but changing it re-renders nothing. It only gets onto the screen by chance, when something else triggers a render.
Loading the interactive part…
The second use: a key to a particular door. A typical example – a search field with a "Search" button that, after a click, puts the cursor back in the field so the user can carry on typing straight away.
function Search() {const inputRef = useRef<HTMLInputElement>(null)function handleSearch() {// …start the search…inputRef.current?.focus() // put the cursor back in the field}return (<><input ref={inputRef} /><button onClick={handleSearch}>Search</button></>)}
When the page element gets into the ref:
Search(). At this moment the <input> element doesn't exist yet, inputRef.current is null.<input> and sees ref={inputRef} on it. It puts the element into inputRef.current.focus() method – exactly the one every <input> in the browser has.null.That's why the ref has a question mark: inputRef.current?.focus(). The element is available in a handler and an effect, not during render.
What you see: Three typical tasks that can't be done by description: putting the cursor in a field, measuring an element's size and scrolling to an item in a list. Each uses a ref to a different element.
Try it:
The takeaway: A ref is a key to a page element. You use it for actions that aren't "what the page should look like" but "do this with this element now".
Loading the interactive part…
When you want to put the cursor in a field hidden inside your own component (<FancyInput />), you need that component to "pass the ref on". In React 19 it's simple: ref is an ordinary prop.
// React 19: ref arrives as a prop and you pass it on to the <input>function MyInput({ ref, ...props }: { ref?: Ref<HTMLInputElement> } & ComponentProps<'input'>) {return <input ref={ref} {...props} />}// Usage – as if it were an ordinary <input>const inputRef = useRef<HTMLInputElement>(null)<MyInput ref={inputRef} />// The older syntax (React 18 and earlier) – you'll still meet it in other people's codeconst MyInput = forwardRef<HTMLInputElement, Props>((props, ref) => <input ref={ref} {...props} />)
When you hand the parent the whole <input>, it can do anything with it: change styles, clear the contents, add listeners. It's like giving someone a TV along with a screwdriver. It's often better to give them just a remote control with a few buttons. That's what useImperativeHandle is for:
interface FancyInputHandle {focus: () => voidshake: () => void}function FancyInput({ ref }: { ref?: Ref<FancyInputHandle> }) {const inputRef = useRef<HTMLInputElement>(null) // its own, private refuseImperativeHandle(ref, () => ({focus: () => inputRef.current?.focus(),shake: () => { /* a short shaking animation */ },}))return <input ref={inputRef} />}// The parent sees only the two buttons of the remote:const emailRef = useRef<FancyInputHandle>(null)emailRef.current?.shake()
<input> and gives it to nobody.useImperativeHandle says: "into the ref the parent sent me, don't put the page element, put this object".focus() and shake(). You can rework the inside of the component any time and the parent won't notice a thing.What you see: The FancyInput component from the example above. The parent has two buttons that call methods from its "remote control".
Try it:
useImperativeHandle. Notice that the parent never gets the <input> itself.The takeaway: The component decides what it lets the parent control. A small, clear API is safer than handing over the whole element.
Loading the interactive part…
Connect the ref to the input and move the cursor into it when the button is clicked.
CHALLENGE: Focus the field
The ref is created but not connected to anything yet.
1. Attach the ref to the input: <input ref={inputRef} … />
2. Make the button call inputRef.current?.focus() on clickrow, stack, card, list, btn, input or muted are the course's ready-made styles in src/styles.css (row = side by side, stack = stacked, card = bordered box, muted = grey text).Loading the interactive part…
The chat practises sections 1 and 2: a ref to a page element (scroll, focus) and a ref as memory without a render. The countdown practises section 3 – you'll design your own remote control.
A combination of a ref to a page element, a ref as memory without a render and an effect that syncs the scroll.
Medium and hard challenges are a bonus for signed-in readers. Signing up is free – just an e-mail, no password.
Design a component with a three-button remote control (start, pause, reset) – exactly how video players in UI libraries work.
Medium and hard challenges are a bonus for signed-in readers. Signing up is free – just an e-mail, no password.
forwardRef any more.