Refs
React renders descriptions of browser elements:
const field = <input aria-label="Search" />;
The browser also provides imperative operations. It can focus an input, scroll a node into view, measure a rectangle, play a video, or hand a DOM node to a library outside React's control.
That leaves the next hole:
if rendering is declarative, how do we safely ask a browser element to perform an imperative operation such as focus?
Trying the JSX value #
Perhaps the JSX value already gives us the input:
const field = <input aria-label="Search" />;
console.log('element type:', field.type);
field.focus();
root.render(field);
Waiting to run
Not run yet.
The value has an element type and represents a React element, as we saw with JSX. The renderer uses this description later, then the browser-created host object provides methods such as focus.
In this order, the code attempts to focus before React has even rendered the description.
This example gives us two constraints:
- the DOM node and the React element are different values
- the DOM node only exists after React commits it
That narrows the hole:
what stable value can persist across renders and receive the host node during commit?
A retained mutable container #
useRef creates an object with one mutable property named current:[1]
import { useRef } from 'react';
function Search() {
const inputRef = useRef(null);
// inputRef is { current: null } on this first render
}
React retains the same ref object across later renders of the same component. The initial value is used for the first render; later calls return the object already stored at that Hook position.[3]
That gives us a stable container. Passing the object through the special ref prop tells React which host element to place inside it:
function Search() {
const inputRef = useRef(null);
return <input ref={inputRef} aria-label="Search" />;
}
When React commits the input, it assigns the DOM node to inputRef.current. When that node leaves the committed tree, React restores inputRef.current to null.[2]
render commit removal
inputRef.current = null inputRef.current = input inputRef.current = null
The useRef call owns the durable container. The ref prop tells React which committed value should occupy it.
Using the node after commit #
An event handler runs after the input has been committed, so it can use the browser API through current:
const { useRef } = React;
function Search() {
const inputRef = useRef(null);
function handleFocus() {
inputRef.current.focus();
console.log(
'input focused:',
document.activeElement === inputRef.current,
);
}
return (
<section>
<input ref={inputRef} aria-label="Search" />
<button onClick={handleFocus}>Focus search</button>
</section>
);
}
root.render(<Search />);
setTimeout(() => {
mountNode.querySelector('button').click();
}, 20);
Waiting to run
Not run yet.
The returned tree describes the input and button. At click time, the handler asks the committed input to perform the imperative focus operation.
This fills the first access hole:
attach a ref to a host element, then use its current node from code that runs after commit
Usually that code is an event handler. Commit-driven operations can synchronize through the ref in an Effect. Layout-sensitive measurement may require useLayoutEffect, because its timing differs around browser paint.[2]
Testing render-time access #
The ref object is available during rendering. Perhaps we can read its node immediately:
const { useRef, useState } = React;
function Search() {
const [query, setQuery] = useState('first');
const inputRef = useRef(null);
console.log('during render:', {
pendingValue: query,
committedValue: inputRef.current?.value ?? null,
});
return (
<section>
<input ref={inputRef} value={query} readOnly />
<button onClick={() => setQuery('second')}>
Change value
</button>
</section>
);
}
root.render(<Search />);
setTimeout(() => {
mountNode.querySelector('button').click();
}, 20);
Waiting to run
Not run yet.
The first render logs a pending value of first and a committed value of null. The click starts another render with a pending value of second, while current.value still reads first from the previous commit. The ref therefore represents the latest committed UI; props, state, and context describe the pending UI.
Render must remain a calculation from props, state, and context. A DOM ref belongs to the commit side of that boundary:
access DOM refs from event handlers or commit-side Effects
The render phase supports a narrow initialization exception for predictable, one-time values stored in refs. Host-node refs follow the attachment and detachment lifecycle owned by React.[3]
Trying a ref as rendered state #
The ref container can hold values other than DOM nodes. Try using one as a counter:
const { useRef } = React;
function Counter() {
const countRef = useRef(0);
function handleClick() {
countRef.current += 1;
console.log('stored count:', countRef.current);
}
return (
<button onClick={handleClick}>
Rendered count: {countRef.current}
</button>
);
}
root.render(<Counter />);
setTimeout(() => {
const button = mountNode.querySelector('button');
button.click();
button.click();
console.log('button text:', button.textContent);
}, 20);
Waiting to run
Not run yet.
The stored count reaches two while the button continues to display zero. Assigning ref.current is an immediate JavaScript mutation outside React's update scheduling.[1]
This separation from rendering makes refs useful:
| Requirement | Store it in |
|---|---|
| a value that changes the rendered description | state |
| a mutable value used only outside rendering | a ref |
Timer IDs, third-party instances, and previous event data can fit in refs when the screen stays the same as they change. A count displayed in JSX belongs in state.
The counter exposes the constraint:
if changing a value should change the UI, that value must participate in rendering
Refs retain mutable information between renders while state drives the rendered description.
Testing conditional attachment #
The focus example keeps its input mounted. Conditional rendering introduces a lifetime hole:
what does
currentcontain after the committed tree removes the host node?
Try hiding the input:
const { useRef, useState } = React;
function Editor() {
const [visible, setVisible] = useState(true);
const inputRef = useRef(null);
function toggle() {
setVisible(!visible);
setTimeout(() => {
console.log(
'after commit:',
inputRef.current === null ? 'detached' : 'attached',
);
}, 0);
}
return (
<section>
{visible && <input ref={inputRef} defaultValue="Draft" />}
<button onClick={toggle}>
{visible ? 'Hide editor' : 'Show editor'}
</button>
</section>
);
}
root.render(<Editor />);
setTimeout(() => {
mountNode.querySelector('button').click();
}, 20);
Waiting to run
Not run yet.
After the hide commit, current is null. If the input appears again, React creates or reuses the appropriate node and attaches the ref during that later commit.
Code that uses a ref must respect this lifetime. An event handler can often use optional chaining when the target may be absent:
inputRef.current?.focus();
The null state is meaningful. It marks the current absence of a committed host value for that tree position.
Crossing a component boundary #
Attaching a ref directly to <input> exposes its DOM node. In the following example, the parent renders a component that owns the input:
const { useRef } = React;
function SearchField() {
return <input aria-label="Search" />;
}
function Toolbar() {
const inputRef = useRef(null);
function handleFocus() {
console.log('exposed value:', inputRef.current);
inputRef.current?.focus();
}
return (
<section>
<SearchField ref={inputRef} />
<button onClick={handleFocus}>Focus search</button>
</section>
);
}
root.render(<Toolbar />);
setTimeout(() => {
mountNode.querySelector('button').click();
}, 20);
Waiting to run
Not run yet.
The component received the ref and left it unattached. This exposes the component-boundary hole:
how can the child choose which of its possible host nodes the parent can reach?
In React 19, ref is available to a function component as a prop. SearchField can explicitly pass it to the input it chooses to expose:[a][4]
const { useRef } = React;
function SearchField({ ref }) {
return <input ref={ref} aria-label="Search" />;
}
function Toolbar() {
const inputRef = useRef(null);
function handleFocus() {
inputRef.current.focus();
console.log('exposed node:', inputRef.current.tagName);
}
return (
<section>
<SearchField ref={inputRef} />
<button onClick={handleFocus}>Focus search</button>
</section>
);
}
root.render(<Toolbar />);
setTimeout(() => {
mountNode.querySelector('button').click();
}, 20);
Waiting to run
Not run yet.
The parent now reaches the inner INPUT. This is explicit capability forwarding: the child decides whether and where the parent's ref crosses its boundary.
Exposing the complete node also exposes every operation on it. Sometimes the parent only needs one narrow action. That leaves a smaller capability hole:
how can the child expose the required operation while keeping its DOM node private?
Exposing an imperative handle #
useImperativeHandle lets a component choose the value assigned to a received ref.[4] The component can keep the real DOM node private and expose a small object:
const { useImperativeHandle, useRef } = React;
function SearchField({ ref }) {
const inputRef = useRef(null);
useImperativeHandle(ref, () => ({
focus() {
inputRef.current.focus();
},
}), []);
return <input ref={inputRef} aria-label="Search" />;
}
function Toolbar() {
const searchRef = useRef(null);
function handleFocus() {
searchRef.current.focus();
console.log('DOM tag exposed:', searchRef.current.tagName);
}
return (
<section>
<SearchField ref={searchRef} />
<button onClick={handleFocus}>Focus search</button>
</section>
);
}
root.render(<Toolbar />);
setTimeout(() => {
mountNode.querySelector('button').click();
}, 20);
Waiting to run
Not run yet.
The parent received the narrow handle { focus() }, so the focus operation works and tagName is undefined.
The handle creates a deliberate boundary:
parent ref -> { focus() } -> private input ref -> DOM input
This keeps the component free to change its internal markup while preserving the small imperative contract its parent needs. Methods such as focus, scrollToStart, or selectText can be reasonable. A handle containing open and close is usually a sign that an isOpen prop would express the UI state more clearly.[4]
This fills the component-boundary hole:
forward a ref when the DOM node is the intended public capability; expose an imperative handle when the public capability should be smaller
Managing multiple nodes #
An object ref represents one current value, while a dynamic list may need a node for each item. Hooks preserve a stable top-level call order, which gives us the collection hole:
how can one component retain several committed nodes through a stable number of Hook calls?
A callback ref can manage attachment directly. React calls it with the node during commit, and React 19 lets the callback return cleanup for detachment:[5]
function Results({ results }) {
const nodesRef = useRef(new Map());
return results.map((result) => (
<article
key={result.id}
ref={(node) => {
nodesRef.current.set(result.id, node);
return () => {
nodesRef.current.delete(result.id);
};
}}
>
{result.title}
</article>
));
}
The stable IDs already used as keys can also index the node map. Setup records the node; cleanup removes exactly that record.[b]
Callback refs are useful when attachment itself needs bookkeeping. For one host node, the object returned by useRef remains the smaller tool.
The mutation boundary #
Refs now provide access to host nodes and capabilities. That access opens the final ownership hole:
which imperative operations preserve React's control of the rendered structure?
React retains ownership of a DOM node reached through a ref and expects the committed DOM to match the rendered tree.
Imperative operations that preserve that structure are usually safe:
- focus or select text
- scroll a node
- measure position or size
- call a media method
- connect a library to a dedicated container
Removing, inserting, or rewriting children that React manages can make the real DOM disagree with React's previous committed tree. A later reconciliation may update the wrong structure or fail because the node it expects has disappeared.[2]
The useful ownership rule is:
use a ref to request a host capability while leaving React in charge of the rendered tree
Describe structural DOM changes with props or state. Reserve direct mutation for browser behavior, measurement, and isolated integration points beyond the component tree's vocabulary.
Filling the hole #
The smallest working focus path has three parts:
import { useRef } from 'react';
function Search() {
const inputRef = useRef(null);
return (
<>
<input ref={inputRef} />
<button onClick={() => inputRef.current?.focus()}>
Focus search
</button>
</>
);
}
Each part owns one responsibility:
useRef(null)retains a mutable container across rendersref={inputRef}asks React to manage its value as the host node attaches and detaches during commit- the click handler uses the current node only when the interaction requires its imperative API
When the target lives inside another component, that component can forward the ref to a chosen node or replace the node with a deliberately limited imperative handle.
Final definition #
A ref is a stable, mutable container retained by React whose current value stays outside the rendering process. A host ref prop lets React attach the corresponding DOM node during commit. Across a component boundary, the child can expose either that node or a custom imperative handle.
Refs are escape hatches. They connect a declarative component to a host capability while React remains the sole rendering system.
Summary #
Refs fill the imperative-access hole:
- a React element describes the host node that the browser creates later
useRef(initialValue)returns a stable object with a mutablecurrentproperty- changing
currenthappens outside React's render scheduling - state holds values that affect JSX; refs hold mutable values used outside rendering
- passing a ref to a host element lets React attach its DOM node during commit
- React clears the ref when that node detaches
- event handlers and commit-side Effects are the appropriate places to read DOM refs
- a component must explicitly decide whether a parent's ref crosses its boundary
- React 19 makes
refavailable to function components as a prop useImperativeHandlecan expose a focused capability through a narrow handle- callback refs support attachment bookkeeping for collections and other dynamic targets
- direct DOM work should use host capabilities while preserving the structure React owns
Notes
- The live demos in this article use the React 19.2.5 and react-dom 19.2.5 development modules. React 19 makes `ref` available to function components as a prop. React 18 and earlier require `forwardRef` for that boundary. · Back
- Strict Mode may run an additional setup-and-cleanup cycle for callback refs in development. A callback ref with complete cleanup remains correct under that probe. · Back
References
- React: Referencing Values with Refs (opens in a new tab) · Back
- React: Manipulating the DOM with Refs (opens in a new tab) · Back
- React `useRef` API reference (opens in a new tab) · Back
- React `useImperativeHandle` API reference (opens in a new tab) · Back
- React DOM: Common components and the `ref` prop (opens in a new tab) · Back