Manipulating the DOM with refs
Get the DOM nodes React creates with refs: focus, scroll and measure, refs for lists, refs to your own components, useImperativeHandle, and flushSync.
- 01Reach a DOM nodePut a ref on a tag to focus, scroll to, or measure the element React made, and give each item in a list its own ref.
- 02Pass refs through componentsTake
refas a prop in your own component, and share only the methods you choose withuseImperativeHandle. - 03Work with React, not against itKnow when React fills in a ref, use
flushSyncwhen you need the new DOM at once, and leave nodes React manages alone.
A search box that a button can focus
React changes the page for you: you describe what it should show, and React creates and updates the elements. Most of the time you never touch them. But a few jobs need the real element, the DOM node: moving the keyboard focus into a box, scrolling to an item, or measuring how big something is. JSX has no prop for those. Here is a search box with a button that puts the cursor in it. Click Find a tea in the result.
import { useRef } from "react"; export default function App() { const searchRef = useRef(null); function handleClick() { searchRef.current.focus(); } return ( <div> <input ref={searchRef} placeholder="Search teas" /> <button onClick={handleClick}>Find a tea</button> </div> );}The cursor jumps into the box, and you can type straight away. Line 4 makes a ref: useRef(null) returns an object with one property, current, which starts as null. Line 12 gives that object to the <input> as its ref attribute. That asks React: when you create this input's DOM node, put it in searchRef.current. Line 7, in the click handler, reads the node and calls the browser's own focus() method on it. Line 13 connects the handler to the button.
The ref object is the same one from Referencing values with refs: a box that keeps its current between renders, and that you can change without causing a render. This time you don't write to it yourself. React does.
useRef(null) gives you the box. ref={searchRef} asks React to put the <input> it creates into searchRef.current, so your click handler can call the node's own focus().At a theatre, you hand over your coat and get a ticket. The attendant decides which hook it goes on and writes it on your ticket. When you need the coat, the ticket takes you straight to it. You don't rearrange the cloakroom: that's the attendant's job.
- In real life: You ask for a ticket before your coat is hung up
- In React:
useRef(null)gives you an empty ref - In real life: The attendant hangs the coat and writes its hook on your ticket
- In React: React creates the node and puts it in
ref.current - In real life: Later, the ticket takes you straight to your coat
- In React: A click handler reads
ref.currentand callsfocus() - In real life: You fetch your coat, but the attendant runs the cloakroom
- In React: You may focus or measure the node, but React decides what the page holds
A ref on a JSX tag gives you the DOM node React created for it. Make a ref with useRef(null), pass it as ref={myRef}, and React puts the node in myRef.current when it adds the node to the page. Use it for jobs React has no prop for, like focus, scrolling, and measuring, and let React keep changing the page.
The ref attribute hands you the DOM node
Every built-in tag in JSX, like <input>, <li>, or <textarea>, takes a ref. Whatever node React makes for it, you can call any browser method on it. A component can hold as many refs as it needs. This shelf of teas scrolls sideways; each button brings one tea into view. Click Mint, then Green.
import { useRef } from "react"; export default function App() { const greenRef = useRef(null); const chaiRef = useRef(null); const mintRef = useRef(null); function show(ref) { ref.current.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center" }); } return ( <div> <button onClick={() => show(greenRef)}>Green</button> <button onClick={() => show(chaiRef)}>Chai</button> <button onClick={() => show(mintRef)}>Mint</button> <ul className="shelf"> <li ref={greenRef}>Green tea</li> <li ref={chaiRef}>Masala chai</li> <li ref={mintRef}>Mint tea</li> </ul> </div> );}Lines 4 to 6 make one ref per tea, and lines 18 to 20 attach them. The show function on lines 8 to 10 takes a ref and calls scrollIntoView on its node: inline: "center" scrolls the shelf sideways until the item sits in the middle, and block: "nearest" moves the page up or down only if it has to. Each button on lines 14 to 16 passes its own ref. The styles.css tab makes the shelf narrower than its three cards, so it scrolls.
Reading a node is just as useful as calling a method on it. This button measures the note box with getBoundingClientRect(). Click Measure the note, drag the box's corner to resize it, and measure again.
import { useRef, useState } from "react"; export default function App() { const noteRef = useRef(null); const [size, setSize] = useState(""); function handleMeasure() { const box = noteRef.current.getBoundingClientRect(); setSize(`${Math.round(box.width)} × ${Math.round(box.height)} pixels`); } return ( <div> <textarea ref={noteRef} defaultValue="Extra hot, please." /> <button onClick={handleMeasure}>Measure the note</button> <p>{size === "" ? "Not measured yet." : `The note is ${size}.`}</p> </div> );}Line 8 asks the browser for the box's size and place on the screen, in pixels. Line 9 rounds the width and height and keeps them in state, so line 16 can show them. Your numbers depend on your screen and font. The measuring happens in a click handler, after the box is on the page. A measurement you need without a click belongs in an Effect, which the next lesson, Synchronizing with effects, covers.
How to read the table: each row is a job you might need a DOM node for, the method you call on the ref, and where this lesson uses it.
| What you want to do | What you call | Where you see it |
|---|---|---|
| Move the keyboard focus into a box | ref.current.focus() | The search box at the top of this lesson. |
| Scroll an element into view | ref.current.scrollIntoView() | The tea shelf and the menu below. |
| Measure an element's size or place | ref.current.getBoundingClientRect() | The note you can measure below. |
| Select all the text in a box | ref.current.select() | The coupon box in exercise 2. |
| Play or pause a video | ref.current.play() and ref.current.pause() | Not shown here. Call it in a click handler, or in an Effect when it should follow a prop. |
A ref for every item in a list
Three refs for three teas is fine. But a menu built from an array can have any number of items. You can't call useRef once per item inside map():
{teas.map((name) => {
const ref = useRef(null); // Not allowed: a hook inside map()
return <li key={name} ref={ref}>{name}</li>;
})}Hooks like useRef must be called at the top level of a component, the same number of times on every render. React matches them up by their order, so a list that grows or shrinks would break that order. (The Rules of Hooks lesson explains why.)
Instead, pass a function as the ref. This is a ref callback. React calls it with the node when it adds the item to the page, and calls the function it returns, its cleanup, when it takes the item away. With it, one ref can hold a Map from each tea's name to its node. Click any tea name in the result:
import { useRef } from "react"; const teas = ["Assam", "Chai", "Darjeeling", "Earl Grey", "Green", "Jasmine", "Mint", "Oolong"]; export default function App() { const itemsRef = useRef(new Map()); function scrollToTea(name) { const node = itemsRef.current.get(name); node.scrollIntoView({ behavior: "smooth", block: "nearest" }); } return ( <div> <nav> {teas.map((name) => ( <button key={name} onClick={() => scrollToTea(name)}>{name}</button> ))} </nav> <ul className="menu"> {teas.map((name) => ( <li key={name} ref={(node) => { itemsRef.current.set(name, node); return () => itemsRef.current.delete(name); }} > {name} </li> ))} </ul> </div> );}Line 6 makes one ref whose current is a new, empty Map. (React keeps the first one; the new Map() on later renders is thrown away, which costs next to nothing.) Lines 24 to 27 are the ref callback each <li> gets: line 25 stores the node under the tea's name, and line 26 returns a cleanup that deletes it. Line 9 finds a node by name, and line 10 scrolls the menu to it. The key on line 23 is the usual list key; it has nothing to do with the ref.
Map. Each <li>'s ref callback stores its node under the tea's name, so itemsRef.current.get("Mint") finds the Mint item. Three of the eight teas are drawn here.Returning a cleanup from a ref callback is new in React 19. Older code didn't return anything. Instead, React called the same function a second time with null when the node went away, so the callback had to check:
<li
key={name}
ref={(node) => {
if (node) itemsRef.current.set(name, node);
else itemsRef.current.delete(name);
}}
>That still works, and React still calls with null when your callback returns no cleanup. When it does return one, React calls the cleanup instead and skips the null call. The React 19 release post describes the change.
To watch React call ref callbacks, this list logs every call. Look at the console under the result first, then press Add chai, then Remove the first.
import { useState } from "react"; export default function App() { const [items, setItems] = useState(["Tea", "Cake"]); return ( <div> <button onClick={() => setItems([...items, "Chai"])} disabled={items.includes("Chai")}> Add chai </button> <button onClick={() => setItems(items.slice(1))}>Remove the first</button> <ul> {items.map((name) => ( <li key={name} ref={(node) => { console.log("attach", name, node.tagName); return () => console.log("cleanup", name); }} > {name} </li> ))} </ul> </div> );}Line 17 logs attach with the item's name and the node's tag, and line 18 returns a cleanup that logs cleanup. When the result first appears, the console shows six lines: attach Tea LI, attach Cake LI, cleanup Tea, cleanup Cake, then both attaches again. That's StrictMode at work: in development, React attaches each new ref, cleans it up, and attaches it again, once, to check that your cleanup really undoes what the callback did. A production build shows only the two attaches.
Now press Add chai. You might expect one line for Chai, but Tea and Cake log a cleanup and an attach too. The arrow function on lines 16 to 19 is a new function on every render, and when the ref changes to a different function, React cleans up with the old one and calls the new one. Then Chai gets its own attach, cleanup, attach, because it's new. Remove the first logs cleanups for all three, then attaches for the two that stay. For a ref callback that only sets and deletes in a Map, these extra calls are harmless, which is why the menu above is written this way.
Passing a ref to your own component
Put a ref on a built-in tag and you get its node. Put one on your own component, like <SearchBox>, and React has no node to give: a component is a function, not an element. In React 19, the ref simply arrives as a prop named ref, and the component decides which of its tags gets it. Click Find a tea, then open the SearchBox.jsx tab.
import { useRef } from "react";import SearchBox from "./SearchBox.jsx"; export default function App() { const searchRef = useRef(null); return ( <div> <SearchBox label="Search teas" ref={searchRef} /> <button onClick={() => searchRef.current.focus()}>Find a tea</button> </div> );}In App.jsx, line 5 makes the ref and line 9 passes it to SearchBox, like any prop. In SearchBox.jsx, line 1 takes ref out of the props next to label, and line 4 puts it on the <input>. So searchRef.current in App is the input's node, and line 10 can focus it. A component that never passes its ref on leaves the parent's ref at null: try removing ref={ref} from line 4 of SearchBox.jsx, and the click fails with Cannot read properties of null (reading 'focus') in the console.
This is a choice the component makes on purpose. A component's DOM nodes are private unless it hands them out, so you can change a component's insides without breaking the components that use it.
Code written before React 19 couldn't read ref as a prop: React kept it back, like key. Those components are wrapped in forwardRef, which hands the ref over as a second argument. You'll see it in many libraries and older apps, and it still works:
import { useRef } from "react";import SearchBox from "./SearchBox.jsx"; export default function App() { const searchRef = useRef(null); return ( <div> <SearchBox label="Search teas" ref={searchRef} /> <button onClick={() => searchRef.current.focus()}>Find a tea</button> </div> );}Open the SearchBox.jsx tab. Line 3 wraps the component in forwardRef, and the component gets ref as its second argument, after the props. Line 6 passes it on, as before, and App.jsx is unchanged. The React 19 release post says new function components no longer need forwardRef, and that React plans to deprecate it. Write new components with the ref prop.
How to read the table: each row is one way a component can take a ref, which React versions use it, and how the ref arrives.
| How the component is written | Where you see it | How the ref arrives |
|---|---|---|
function SearchBox({ label, ref }) | React 19 and later. | ref arrives as a prop, next to label. Pass it on to a tag. |
forwardRef(function SearchBox({ label }, ref) { … }) | Older code. It still works in React 19. | ref arrives as a second argument. React plans to deprecate forwardRef, so new code doesn't need it. |
Sharing only some methods with useImperativeHandle
Handing a parent the whole <input> lets it do anything to it: change its styles, remove it, set its text. Most of the time the parent only needs one or two things, like focusing it. useImperativeHandle lets your component give the parent its own small object, called a handle, instead of the node. Type something in the box, then click Clear the search.
import { useRef } from "react";import SearchBox from "./SearchBox.jsx"; export default function App() { const searchRef = useRef(null); function handleClear() { searchRef.current.clear(); searchRef.current.focus(); console.log("App can call:", Object.keys(searchRef.current)); } return ( <div> <SearchBox ref={searchRef} /> <button onClick={handleClear}>Clear the search</button> </div> );}The box empties, the cursor goes into it, and the console prints App can call: followed by a list of two names, "focus" and "clear". In SearchBox.jsx, line 4 makes a ref of its own, and line 15 puts that one on the <input>. Lines 6 to 13 call useImperativeHandle with the ref prop and a function that returns the handle: an object with focus (line 7) and clear (line 10), each working on the private input. React puts that object in the parent's ref. So in App.jsx, lines 8 and 9 call the two methods, and line 10 lists everything the parent can reach: just those two.
clear on line 11 sets the input's value by hand. That's safe here because the input is uncontrolled: it has no value prop, so React never sets its text and nothing clashes.
SearchBox puts its ref prop on its <input>, so searchRef.current is the node. Bottom: with useImperativeHandle, searchRef.current is the object you return, with only focus and clear.Here are all four ways side by side. Pick a version of the code; the playground renders it with React, reads searchRef.current after the commit, and lets you call focus() on it.
export default function App() {
const searchRef = useRef(null);
return <input ref={searchRef} placeholder="Search teas" />;
}searchRef.current: …Not called yet.
React put the <input> node itself into searchRef.current, so focus() is the browser's own method.
A built-in <input> and a SearchBox that passes its ref on both give the node. A SearchBox that ignores its ref gives null, and calling focus() throws a TypeError. With useImperativeHandle, the ref holds an object with two functions, and focus() works because the handle has a method by that name.
When React fills in a ref
You met the two steps of every update in Render and commit: React calls your components to work out the new JSX (the render), then changes the page to match (the commit). Refs are filled in during the commit. This example reads its ref in two places on purpose, to show what each one sees. Look at the console, then click Add a cup.
import { useRef, useState } from "react"; export default function App() { const [cups, setCups] = useState(0); const cupsRef = useRef(null); console.log("Rendering: the ref holds", cupsRef.current?.textContent ?? null); function handleClick() { setCups(cups + 1); console.log("Clicked: the page still says", cupsRef.current.textContent); } return ( <div> <p ref={cupsRef}>Cups: {cups}</p> <button onClick={handleClick}>Add a cup</button> </div> );}Line 7 runs during every render. On the first one it logs Rendering: the ref holds null, with a small 2 beside it because StrictMode renders twice in development. The <p> on line 16 doesn't exist yet: it's made from the JSX this very render returns. Click Add a cup, and line 11 logs Clicked: the page still says Cups: 0. Line 10 asked for a new render, but it hasn't happened yet, so the page still shows the old number. Then the render logs Rendering: the ref holds Cups: 0: during the render, the page still has the old text too. Only after the commit does the <p> say Cups: 1.
App renders, the <p> doesn't exist yet, so cupsRef.current is null. React fills the ref in during the commit. By the time you click, handleClick finds the node.So read refs in event handlers, which run long after the commit. Don't read or write them during render: the value is null or out of date there, and rendering must stay pure. In exact terms, React sets each changed ref to null just before it updates the DOM, and to the new node right after. If there's no event to read a ref in, you need an Effect, which the next lesson covers.
How to read the table: each row is a moment in a component's life, what a ref on its <p> holds then, and why.
| The moment | What the ref holds | Why |
|---|---|---|
| During the first render | null | React hasn't made the <p> yet. |
| During a later render | The <p>, still showing the old text | React hasn't changed the page for this render yet. |
In a handler, right after setCups(…) | The <p>, still showing the old text | Setting state asks for a render; it doesn't run one now. |
Right after flushSync(() => setCups(…)) | The <p>, showing the new text | flushSync makes React render and commit before it returns. |
| After the element leaves the page | null | React clears the ref when it removes the node. |
The timing matters most when a handler changes state and then needs the new DOM straight away. A chat adds your message and should scroll to it. Pick a way to write the update, then press Send a message a few times.
import { useRef, useState } from "react";import { flushSync } from "react-dom"; export default function App() { const [messages, setMessages] = useState(["Message 1", "Message 2"]); const listRef = useRef(null); function handleSend() { const text = `Message ${messages.length + 1}`; setMessages([...messages, text]); const last = listRef.current.lastChild; last.scrollIntoView({ block: "nearest" }); console.log("Scrolled to", last.textContent); } return ( <div> <ul ref={listRef}> {messages.map((message) => ( <li key={message}>{message}</li> ))} </ul> <button onClick={handleSend}>Send a message</button> </div> );}no scroll yetPress Send a message a few times and watch which message gets the outline: that's the one the code scrolled to.
With just setMessages, the outline always lands one message behind. Setting state only queues a render, so line 11 reads lastChild from the old list. With flushSync, imported from react-dom on line 2, React renders and commits the update inside the call on lines 10 to 12, before it returns. The next line finds the new message. flushSync skips React's usual batching, so use it only when the next line needs the new DOM.
Leave the DOM React owns to React
Focusing, scrolling, and measuring don't change what React put on the page, so they never clash with it. Changing the nodes is different. React remembers what it rendered last time and changes the page by comparing against that memory, not against the page. Remove a node behind its back, and React's memory and the page disagree. Try it: click Toggle with state twice, then Remove with the DOM while the banner shows, then Toggle with state once more.
import { useRef, useState } from "react"; export default function App() { const [showBanner, setShowBanner] = useState(true); const bannerRef = useRef(null); return ( <div> <button onClick={() => setShowBanner(!showBanner)}>Toggle with state</button> <button onClick={() => bannerRef.current.remove()}>Remove with the DOM</button> {showBanner && <p ref={bannerRef}>Free delivery on orders over $20</p>} </div> );}The first two toggles hide and show the banner, the React way: line 9 flips showBanner, and line 11 renders the <p> only while it's true. Line 10 removes the <p> with the DOM's own remove(). The banner disappears, but React still thinks it's there. The last toggle asks React to remove it, React asks the browser to take it out of the <div>, and the browser throws: NotFoundError: Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node. The whole result disappears and the error shows in its place. Press Run again to start over.
Changing a node's text or class by hand doesn't crash, but it doesn't last: the next time React updates that node, it puts its own value back. Render and commit shows that with a heading changed by hand.
You can still change the DOM yourself in parts React has no reason to touch. An element that React always renders empty is one: React never adds or removes its children, so your code can.
import { useRef, useState } from "react"; export default function App() { const [count, setCount] = useState(0); const notesRef = useRef(null); function handleAdd() { const note = document.createElement("li"); note.textContent = `Note ${count + 1}, added by hand`; notesRef.current.append(note); setCount(count + 1); } return ( <div> <button onClick={handleAdd}>Add a note</button> <p>Notes so far: {count}</p> <ul ref={notesRef} /> </div> );}Click Add a note twice. Line 18 renders an empty <ul>, every time. Lines 8 to 10 create an <li> with plain DOM code and add it to that list. Line 11 changes state, so React renders again and updates the count on line 17, but the <ul> has no children in the JSX, so React leaves your notes alone. This is how non-React code, like a map or a chart library, lives inside a React app: React gives it an empty element and stays out.
A support chat that scrolls and focuses
Here is everything at work in one small support chat. Type a message and press Send (or Enter) a few times, until the list scrolls.
import { useRef, useState } from "react";import { flushSync } from "react-dom"; export default function App() { const [messages, setMessages] = useState(["Hi! How can we help?"]); const [text, setText] = useState(""); const listRef = useRef(null); const inputRef = useRef(null); function handleSend(e) { e.preventDefault(); if (text.trim() === "") return; flushSync(() => { setMessages([...messages, text]); setText(""); }); listRef.current.lastChild.scrollIntoView({ block: "nearest" }); inputRef.current.focus(); } return ( <div> <ul ref={listRef} className="messages"> {messages.map((message, index) => ( <li key={index}>{message}</li> ))} </ul> <form onSubmit={handleSend}> <input ref={inputRef} value={text} onChange={(e) => setText(e.target.value)} placeholder="Type a message" /> <button>Send</button> </form> </div> );}Lines 7 and 8 make two refs: one for the message list (line 23) and one for the input (line 29). handleSend stops the form from reloading the page (line 11) and ignores an empty message (line 12). Lines 13 to 16 add the message and empty the box inside flushSync, so when line 17 runs, the new <li> is already the list's last child, and it scrolls into view. Line 18 puts the cursor back in the box, so clicking Send doesn't leave the focus on the button. Everything React draws, the messages and the box's text, comes from state. The refs only scroll and focus.
searchRef.current.focus()lastMessage.scrollIntoView()tipRef.current.getBoundingClientRect()couponRef.current.select()bannerRef.current.remove()countRef.current.textContent = "3"boxRef.current.className = "error"cartListRef.current.append(newItem)
Sort each line: is it fine to run on a ref, or should it be state and JSX instead?
Common mix-ups
- "
ref.currentholds the node as soon as I calluseRef." No. It'snulluntil React commits the element. Read it in event handlers, or in an Effect. - "I need
forwardRefto pass a ref to my component." Not in React 19.refis an ordinary prop for function components.forwardRefis for older code. - "A ref on my component gives the parent the component." No. It gives whatever the component attaches the ref to: a DOM node, the handle from
useImperativeHandle, or nothing at all. - "After
setX(…), the DOM already shows the new value." No. React renders after your handler ends. UseflushSynconly when the next line needs the new DOM. - "Refs are a quicker way to change the page." No. Text or classes you change by hand get overwritten, and nodes you remove can crash React. Change state, and let React change the page.
- "Each list item needs its own
useRef." No. Hooks can't go insidemap(). Use one ref holding aMap, filled by ref callbacks.
Practice exercises
Each exercise shows its result under the code, or when you press Edit & run. Change the code, and the result updates as you type. Make a guess first, then use the hints.
What does the console print first? Answer, then press Edit & run to check.
import { useRef } from "react";
export default function App() {
const emailRef = useRef(null);
console.log("emailRef.current is", emailRef.current);
return <input ref={emailRef} type="email" placeholder="Your email" />;
}import { useRef } from "react"; export default function App() { const emailRef = useRef(null); console.log("emailRef.current is", emailRef.current); return <input ref={emailRef} type="email" placeholder="Your email" />;}Line 5 runs during the render, before React has created the <input>, so it logs emailRef.current is null, twice in development because StrictMode renders twice. React fills in the ref during the commit, after App returns.
Make Select the code select the whole coupon code, so the reader can copy it. Which method on the input does it?
import { useRef } from "react"; export default function App() { const couponRef = useRef(null); function handleCopy() { // Select all the text in the coupon box here. } return ( <div> <input defaultValue="TEA-FRIDAY-5" readOnly /> <button onClick={handleCopy}>Select the code</button> </div> );}import { useRef } from "react"; export default function App() { const couponRef = useRef(null); function handleCopy() { couponRef.current.select(); } return ( <div> <input ref={couponRef} defaultValue="TEA-FRIDAY-5" readOnly /> <button onClick={handleCopy}>Select the code</button> </div> );}Line 12 now attaches the ref, so couponRef.current is the <input> node. Line 7 calls select(), the browser's method that selects all the text in a box, ready to copy.
The buttons should scroll the list to their cake, but there are five cakes and you can't call useRef in a loop. Fill cakesRef with ref callbacks and finish scrollToCake. Under which key does the Map keep Banana bread's <li>?
import { useRef } from "react"; const cakes = [ { id: 1, name: "Carrot cake" }, { id: 2, name: "Lemon drizzle" }, { id: 3, name: "Chocolate fudge" }, { id: 4, name: "Banana bread" }, { id: 5, name: "Red velvet" },]; export default function App() { const cakesRef = useRef(new Map()); function scrollToCake(id) { // Find the cake's <li> and scroll it into view. } return ( <div> {cakes.map((cake) => ( <button key={cake.id} onClick={() => scrollToCake(cake.id)}>{cake.name}</button> ))} <ul className="cakes"> {cakes.map((cake) => ( <li key={cake.id}>{cake.name}</li> ))} </ul> </div> );}import { useRef } from "react"; const cakes = [ { id: 1, name: "Carrot cake" }, { id: 2, name: "Lemon drizzle" }, { id: 3, name: "Chocolate fudge" }, { id: 4, name: "Banana bread" }, { id: 5, name: "Red velvet" },]; export default function App() { const cakesRef = useRef(new Map()); function scrollToCake(id) { cakesRef.current.get(id).scrollIntoView({ behavior: "smooth", block: "nearest" }); } return ( <div> {cakes.map((cake) => ( <button key={cake.id} onClick={() => scrollToCake(cake.id)}>{cake.name}</button> ))} <ul className="cakes"> {cakes.map((cake) => ( <li key={cake.id} ref={(node) => { cakesRef.current.set(cake.id, node); return () => cakesRef.current.delete(cake.id); }} > {cake.name} </li> ))} </ul> </div> );}Each <li>'s ref callback stores its node under the cake's id, and its cleanup deletes it. scrollToCake looks the node up by id and scrolls the list to it. Banana bread's id is 4, so its node is under the key 4.
Each click should add an order and scroll to it. Click Add an order once and read the console: what does it print? Then fix the code so it scrolls to the order it just added.
import { useRef, useState } from "react"; export default function App() { const [orders, setOrders] = useState(["Order 1", "Order 2", "Order 3"]); const listRef = useRef(null); function handleAdd() { setOrders([...orders, `Order ${orders.length + 1}`]); const last = listRef.current.lastChild; console.log("Scrolled to", last.textContent); last.scrollIntoView({ block: "nearest" }); } return ( <div> <button onClick={handleAdd}>Add an order</button> <ul ref={listRef}> {orders.map((order) => ( <li key={order}>{order}</li> ))} </ul> </div> );}import { useRef, useState } from "react";import { flushSync } from "react-dom"; export default function App() { const [orders, setOrders] = useState(["Order 1", "Order 2", "Order 3"]); const listRef = useRef(null); function handleAdd() { flushSync(() => { setOrders([...orders, `Order ${orders.length + 1}`]); }); const last = listRef.current.lastChild; console.log("Scrolled to", last.textContent); last.scrollIntoView({ block: "nearest" }); } return ( <div> <button onClick={handleAdd}>Add an order</button> <ul ref={listRef}> {orders.map((order) => ( <li key={order}>{order}</li> ))} </ul> </div> );}Before the fix, line 9 runs before React adds Order 4, so it logs Scrolled to Order 3. Inside flushSync, React renders and commits the new order before the call returns, so the fixed version logs Scrolled to Order 4.
Click Change the amount: the console shows a TypeError. What is quantityRef.current when you click? Fix QuantityInput.jsx so the button focuses the box and selects its number.
import { useRef } from "react";import QuantityInput from "./QuantityInput.jsx"; export default function App() { const quantityRef = useRef(null); function handleEdit() { quantityRef.current.focus(); quantityRef.current.select(); } return ( <div> <QuantityInput label="Cups of chai" ref={quantityRef} /> <button onClick={handleEdit}>Change the amount</button> </div> );}export default function QuantityInput({ label, ref }) { return ( <label> {label} <input ref={ref} type="number" defaultValue={1} min={1} /> </label> );}QuantityInput now reads ref from its props and puts it on the <input>, so quantityRef.current in App is the input's node. Before the fix it stayed null, and focus() threw a TypeError.
App wants to call listRef.current.scrollToBottom(), but MessageList doesn't share anything yet. Give it a handle with only that method. After your fix, which key does the console list for the handle?
import { useRef } from "react";import MessageList from "./MessageList.jsx"; const messages = ["Hi!", "Is the chai sweet?", "A little.", "Great, one please.", "Coming up.", "Thanks!"]; export default function App() { const listRef = useRef(null); function handleJump() { listRef.current.scrollToBottom(); console.log("The handle has:", Object.keys(listRef.current)); } return ( <div> <button onClick={handleJump}>Jump to the newest</button> <MessageList messages={messages} ref={listRef} /> </div> );}import { useImperativeHandle, useRef } from "react"; export default function MessageList({ messages, ref }) { const listRef = useRef(null); useImperativeHandle(ref, () => ({ scrollToBottom() { listRef.current.lastChild.scrollIntoView({ block: "nearest" }); }, })); return ( <ul ref={listRef} className="messages"> {messages.map((message) => ( <li key={message}>{message}</li> ))} </ul> );}MessageList keeps its <ul> in a private ref and gives the parent a handle with one method. App's listRef.current is that handle, so line 10 scrolls to the newest message and line 11 logs The handle has: with one name in the list, scrollToBottom.
Check your understanding
Question 1 of 8What does this component log while it renders for the first time?
Read the code, then predictimport { useRef } from "react"; export default function App() { const nameRef = useRef(null); console.log(nameRef.current); return <input ref={nameRef} />; }Choose an answer to see the explanation.
Question 2 of 8You click the button once. What does the console show?
Read the code, then predictimport { useRef, useState } from "react"; export default function App() { const [likes, setLikes] = useState(5); const likesRef = useRef(null); function handleLike() { setLikes(likes + 1); console.log(likesRef.current.textContent); } return <button ref={likesRef} onClick={handleLike}>{likes}</button>; }Choose an answer to see the explanation.
Question 3 of 8You click the price. What does it log?
Read the code, then predictimport { useRef } from "react"; function PriceTag({ price }) { return <strong>${price}</strong>; } export default function App() { const tagRef = useRef(null); return ( <button onClick={() => console.log(tagRef.current)}> <PriceTag price={4} ref={tagRef} /> </button> ); }Choose an answer to see the explanation.
Question 4 of 8In React 19, how does a function component like
SearchBoxreceive the ref in<SearchBox ref={searchRef} />?Choose an answer to see the explanation.
Question 5 of 8
SearchBoxcallsuseImperativeHandle(ref, () => ({ focus, clear }))and puts its own ref on the<input>. What does the parent's ref hold?Choose an answer to see the explanation.
Question 6 of 8A handler calls
setMessages([...messages, text]), then scrolls tolistRef.current.lastChild. Why does it scroll to the message before the new one?Choose an answer to see the explanation.
Question 7 of 8You remove a
<p>that React rendered by calling.remove()on it. Then a state change tells React to stop rendering it. What happens?Choose an answer to see the explanation.
Question 8 of 8In development, with StrictMode, a new
<li>appears. Its ref callback logsattachand returns a cleanup that logscleanup. What do you see for that<li>?Choose an answer to see the explanation.
Key takeaways
ref={myRef}on a built-in tag makes React put the DOM node inmyRef.current.- Use refs for jobs JSX has no prop for: focus, scrolling, measuring, selecting text, playing media.
- For a list, pass a ref callback. It gets the node, and the cleanup it returns runs when the node goes (React 19).
- In React 19,
refis a prop for function components. Pass it on to a tag, or share a smaller handle withuseImperativeHandle.forwardRefis for older code. - React fills in refs during the commit. Read them in event handlers, not during render.
flushSynccommits an update at once when the next line needs it. - Never add, remove, or change the nodes React manages. Change state, or work inside an element React leaves empty.
A ref hands you a node; React still runs the page.
Read, focus, scroll, and measure through refs, and change what the page shows through state.
Coming next: Synchronizing with effects.