Skip to content
React Course1.0.0 beta
CSEN
React Course1.0.0 beta
CSEN
🏠 Home🧭 Where to start?🔁 Review📄 Cheat sheets📰 What's newℹ️ About the course
0. JavaScript0/22

Basics

  • 1. A quick review of the basics0/3
  • 2. State and events (useState)0/3

Hooks in depth

  • 3. Effects (useEffect)0/3
  • 4. Refs (useRef)0/3
  • 5. More complex state (useReducer)0/3
  • 6. Context0/3
  • 7. Custom hooks0/3

Forms

  • 8. Forms and React 19 Actions0/3

Performance

  • 9. Performance and rendering0/3

Patterns

  • 10. Component design patterns0/3

Ecosystem

  • 11. Where the type goes (TypeScript syntax)0/3
  • 12. TypeScript with React0/3
  • 13. Routing (React Router)0/3
  • 14. Data fetching0/3
  • 15. Application state management0/3

Project

  • 16. Final project: Kanban0/2

Summary

  • 17. Summary: the principles of React

Your account

Privacy

Lesson 4 · Hooks in depth

Refs (useRef)

Access to the DOM, values without re-rendering, ref as a prop and useImperativeHandle.

Loading lesson…

💬 Found a mistake, something unclear, or have an idea? Let me know.
← Effects (useEffect)More complex state (useReducer) →

Before we start: the board and the drawer

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 key to a particular door

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.

ℹ️ What's in this lesson
  1. A ref as memory without a render – and when to use it instead of state.
  2. A ref to an element on the page: focus, measuring, scrolling.
  3. A ref to your own component and a small "remote control" through useImperativeHandle.

1. A ref as memory without a render

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 it
const intervalRef = useRef<number | null>(null) // the drawer – a technical detail
function 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.
  • You can write to intervalRef.current at any time. React won't find out and re-renders nothing.
  • When you then click Stop, the handler reaches into the drawer and finds the ID from the click on Start – even though several renders have happened in between.

Why not some other way?

Where to store the IDWhat happens
let idThe variable is created again on every render (the cook without a memory). After the first tick the ID is lost and Stop stops nothing.
useStateIt 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.
useRefIt remembers and doesn’t re-render. Correct.
useState (the board)useRef (the drawer)
A change causes a renderYesNo
The value in a handlerA snapshot from the renderAlways current (.current)
How it changesOnly through the setterBy writing directly to .current
Use it forData shown in the UITimer IDs, page elements, library instances, previous values
Demo

A ref vs. state

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:

  1. Click ref + 1 five times. Nothing changes on the screen, not even the number of renders. Yet the value really does grow – if you open the browser console (F12), you'll see it there.
  2. Click state + 1 once. The component re-renders and suddenly shows ref = 5 too. The ref remembered all along, it just didn't tell anyone.
  3. Notice the number of renders: after clicking state + 1 it jumps by 2. In development mode React calls render twice on purpose to reveal impure components – that's why you shouldn't write to a ref during render (here we do it only for the demo).

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…

❓ Wouldn't an ordinary variable outside the component be enough (let id above the function)?

For a single use it would work, but a variable above the function is one for the whole app. If you put two stopwatches on the page, both will write to the same variable and one stopwatch will stop the other. Every occurrence of a component has its own ref, just like state: every stopwatch has its own drawer.

❓ Why is the interval's type ReturnType<typeof setInterval> and not number?

setInterval returns a number in the browser but an object in Node.js. TypeScript in a project sometimes knows both worlds and doesn't know which one applies. ReturnType<typeof setInterval> means "exactly the type setInterval returns", whatever it is. It works everywhere. The alternative is to call window.setInterval, which always returns a number (as in the example above).

⚠️ Don't reach into the drawer while cooking
Don't read or write ref.current during render (a one-off initialisation is the exception). The result would depend on how many times React called the component, and rendering would no longer be pure. Refs belong in handlers and effects. And a simple rule: what should show on the screen belongs on the board – in state.

2. A ref to an element on the page

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:

  1. Render: React calls Search(). At this moment the <input> element doesn't exist yet, inputRef.current is null.
  2. Writing to the page: React creates the real <input> and sees ref={inputRef} on it. It puts the element into inputRef.current.
  3. A click: the handler reaches into the ref and finds the ready element there. It calls its focus() method – exactly the one every <input> in the browser has.
  4. Removal: when the input disappears from the page, React sets the ref back to 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.

Demo

Focus, measuring and scrolling

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:

  1. Type a few words into the field, click outside it and then on Focus. The cursor returns to the field. The Select the text button selects all the text in it.
  2. Resize the "Resize me by the corner" box by dragging its bottom-right corner and click Measure. The ref gave access to the element its real size was read from.
  3. Click → Tom and then → Tiger. The list of cats scrolls to that item.

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…

❓ What is HTMLInputElement and how do I find out which type to write?

Every HTML tag has its own object type in the browser: <input> is HTMLInputElement, <div> is HTMLDivElement, <button> is HTMLButtonElement, <video> is HTMLVideoElement. The pattern is HTML + Name + Element. The type in useRef<…> says which methods you may call on the element: every element has focus(), only an input has select(), only a video has play().

The easiest way is to leave it to your editor: write <input ref={x} /> and hover over ref, and the editor shows the type.

And the ?. in inputRef.current?.focus() is optional chaining: "if the left side is null, do nothing, otherwise call focus()". It's there because the ref is null until the element exists.

❓ What are scrollTop, scrollHeight and clientHeight (the Chat challenge)?

Three properties of every element that can scroll. All of them are in pixels:

  • scrollHeight = the height of the whole content, including the hidden part.
  • clientHeight = the height of the visible window (how much you can see at once).
  • scrollTop = how many pixels of content are scrolled up, out of the window. It can also be written, which scrolls.
el.scrollTop = el.scrollHeight // scroll all the way down
const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 10 // am I (almost) at the bottom?

The second line: from the total height I subtract what's above the window and what's visible. The rest is the content below the window. When it's less than 10 px, the user is at the bottom.

📍 Where to use it
  • focus, select, scroll (automatic focus in a modal, scrolling to an error in a form)
  • measuring size and position (tooltips, popovers, virtualised lists)
  • controlling media: video.play(), canvas.getContext()
  • integrating libraries outside React (maps, charts, editors) that need a page element
✨ Keys to many doors: a callback ref
In the list of cats you can't call useRef for every cat – hooks mustn't be in a loop. Instead of an object you pass a function to ref: React calls it with the element when it's created. You then store the elements in a Map by key. In React 19 such a function can return a cleanup, which is called when the element disappears.
<li ref={(node) => {
itemsRef.current.set(cat, node) // the element was created
return () => itemsRef.current.delete(cat) // the element disappeared
}}>

3. A ref to your own component

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 code
const MyInput = forwardRef<HTMLInputElement, Props>((props, ref) => <input ref={ref} {...props} />)

A remote control instead of a screwdriver

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: () => void
shake: () => void
}
function FancyInput({ ref }: { ref?: Ref<FancyInputHandle> }) {
const inputRef = useRef<HTMLInputElement>(null) // its own, private ref
useImperativeHandle(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()
  • The component has its own ref to the <input> and gives it to nobody.
  • useImperativeHandle says: "into the ref the parent sent me, don't put the page element, put this object".
  • So the parent can call only focus() and shake(). You can rework the inside of the component any time and the parent won't notice a thing.
Demo

A limited API through useImperativeHandle

What you see: The FancyInput component from the example above. The parent has two buttons that call methods from its "remote control".

Try it:

  1. Click focus(). The cursor jumps into the field – the parent called a method the component itself offered.
  2. Click shake(). The field shakes and briefly turns red. That's a typical action without lasting state – it can't be expressed well with a prop.
  3. Open the code and find 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…

⚠️ The remote control is the last resort
If something can be expressed with a prop (isOpen, value), do it that way – that's still "a description of what the page should look like". Imperative methods are only for one-off actions: focus(), scrollToIndex(), play(), shake().

Check yourself

Quiz Where should you store the ID from setInterval that you need in a 'Stop' handler?
Quiz Why is inputRef.current null during the first render?
Quiz A component shows the number of clicks on a button. Where should you store it?

Warm-up

Challenge

Focus the field

Warm-up

Connect the ref to the input and move the cursor into it when the button is clicked.

Task (the same text is in the comment at the top of the file)
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 click
🧩 Where do the things not defined here come from
CSS classes like row, 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…

Challenges

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.

Challenge

A chat with scrolling and focus

Medium Bonus challenge

A combination of a ref to a page element, a ref as memory without a render and an effect that syncs the scroll.

🔒 Bonus challenge

Medium and hard challenges are a bonus for signed-in readers. Signing up is free – just an e-mail, no password.

How we handle your data
Challenge

A countdown controlled through a ref

Hard Bonus challenge

Design a component with a three-button remote control (start, pause, reset) – exactly how video players in UI libraries work.

🔒 Bonus challenge

Medium and hard challenges are a bonus for signed-in readers. Signing up is free – just an e-mail, no password.

How we handle your data

Summary

  • State is the board, a ref is the drawer. Both remember between renders, but only a change of state re-renders.
  • Use a ref for things the user doesn't see: timer IDs, previous values, library instances.
  • A ref to a page element for focus, measuring, scrolling and libraries outside React. The element is in it only after rendering.
  • Don't read or write ref.current during render – only in handlers and effects.
  • React 19: ref is an ordinary prop; you don't need forwardRef any more.
  • useImperativeHandle gives the parent a remote control instead of the whole element – only for actions that can't be expressed with a prop.