Refs and the DOM
React builds and updates the DOM for you. You describe what the screen should look like, and the actual elements are React's job. A small number of tasks still need the element itself: focusing an input after a button is clicked, scrolling a message into view, measuring how wide a box turned out, playing a video. None of those can be expressed as a piece of JSX, because each one acts on a node instead of describing one. A ref is the escape hatch for them. It gives a component a way to hold onto a real DOM element and call methods on it directly.
Attaching a ref
A ref starts with the useRef hook, and you hand the result to an element through the ref attribute:
import { useRef } from 'react'
function SearchBar() {
const inputRef = useRef(null)
function handleFocus() {
inputRef.current.focus()
}
return (
<>
<input ref={inputRef} />
<button onClick={handleFocus}>Focus</button>
</>
)
}useRef(null) returns an object with a single property, current, holding whatever you passed as the initial value. You always pass one, and null is the conventional starting point for a DOM ref, since there is no node yet at that moment.
Writing ref={inputRef} is the instruction React needs. When React puts that <input> on the screen, it sets inputRef.current to the DOM node. From then on inputRef.current is the input element itself, with every method and property the browser gives it, so inputRef.current.focus() inside the click handler moves the cursor into the field. React keeps the assignment up to date for you: if the element is removed from the screen, React sets current back to null.
Scrolling a node into view
Focus is one job. Scrolling is the other one you will hit early, usually in something like a chat log that should jump to the newest message. That job runs after render, so it pairs with useEffect:
import { useRef, useEffect } from 'react'
function MessageList({ messages }) {
const bottomRef = useRef(null)
useEffect(() => {
bottomRef.current.scrollIntoView({ behavior: 'smooth' })
}, [messages])
return (
<div>
{messages.map(message => (
<p key={message.id}>{message.text}</p>
))}
<div ref={bottomRef} />
</div>
)
}The empty <div> at the bottom exists only as something to scroll to, which is a common and perfectly reasonable trick. The effect runs whenever messages changes, and by then React has already put the new messages on the screen and pointed bottomRef.current at that final div.
Changing a ref never triggers a render
State and refs both survive from one render to the next, and there the resemblance stops. Calling a state setter asks React to render the component again with the new value. Assigning to ref.current changes the value silently, and the screen stays exactly as it was.
That silence is the whole point. A ref holds something the UI does not display: a DOM node, a timer ID, a flag your rendering logic never reads. Re-rendering when one of those changes would cost you a redraw that could not change a single pixel.
It has a second consequence worth holding onto. Because nothing has to be scheduled, the value in ref.current is readable the instant after you assign it. A state variable is fixed for the whole of one render and only moves in the next one, as State covers. A ref behaves like a plain mutable box that you can read and write at any moment.
When a ref is the wrong instinct
The dividing line is whether the value ends up on the screen. Text in a heading, a number in a badge, whether a panel is open: all of that belongs in state, because the display has to change when the value does. Put it in a ref and the update goes unseen: the value moves, and the UI keeps showing whatever it rendered last.
Reaching into the DOM to change what is displayed is the same instinct one step further along. Setting node.textContent by hand, or toggling a class directly, appears to work, and then the next render undoes it, because React re-applies whatever your JSX describes. Refs cover two things: reading and commanding a node, meaning focus it, scroll it, measure it, play it, and holding values that survive across renders without ever being displayed. For anything the user reads off the screen, describe it in JSX and drive it with state.
Version note
Before React 19, function components could not receive a ref prop at all. Passing one through to an element inside required wrapping the component in forwardRef. React 19 delivers ref to function components as an ordinary prop, so forwardRef is unnecessary in new code and is deprecated. Further back, class components created refs with React.createRef() and read them from this. The useRef hook covers both roles in function components.
useRef(null), put the result on an element with ref={inputRef}, and React fills in inputRef.current with the actual DOM node once it is on the screen. Then you can do things to it, like inputRef.current.focus(). Reach for a ref when you need to focus, scroll, or measure something. Anything the person using your app reads off the screen belongs in state. Next up: Hooks, the wider family that useState, useEffect, and useRef all belong to.

