Refs and forwardRef
Most of React is declarative: you describe what the UI should look like for a given state, and React figures out the DOM changes. But sometimes you need to step outside that model and imperatively reach into the DOM — to focus an input, measure an element’s size, scroll something into view, or store a value that changes over time without ever causing a re-render. That is exactly what useRef is for. And when the thing you need to reach into lives inside a custom component instead of a plain HTML tag, forwardRef is the tool that lets a ref pass through that component to whatever it wraps. Together they form React’s escape hatch for the handful of problems that props and state alone can’t solve.
Overview / How it works
useRef(initialValue) returns a plain JavaScript object of the shape { current: initialValue }. React creates this object once, on the component’s first render, and returns the exact same object on every subsequent render of that component instance — it is never recreated and never reset. This is fundamentally different from useState: updating ref.current does not schedule a re-render, because React’s reconciler never looks at ref values when deciding whether to re-render or what to diff. A ref is simply a mutable box that happens to survive across renders, attached to the component’s internal fiber node rather than to the render output.
The most common use of a ref is to hold a reference to a real DOM node. When you write <input ref={inputRef} /> on a host element (a lowercase JSX tag like div, input, or button), React automatically sets inputRef.current to the actual DOM node during the commit phase — after React has updated the DOM to match the new render output, but before the browser paints. That timing matters: you cannot read a DOM ref during rendering (the node doesn’t exist yet), only afterward, inside event handlers or inside useEffect. On unmount, React sets the ref back to null before the element is removed.
Custom function components, however, don’t automatically accept a ref prop the way host elements do. If you try <MyInput ref={someRef} /> on a plain function component, React (in versions prior to React 19) strips the ref out and warns that function components cannot be given refs, because there’s no single DOM node for React to attach it to — a component might render many elements or none. forwardRef solves this: it wraps your component’s render function so it explicitly receives (props, ref) as two arguments, and you decide which inner element (or which custom object) that incoming ref should be attached to. This is the standard pattern for building reusable, wrapped versions of native elements — a styled Input, a Button, a Modal — that still let a parent grab the real underlying DOM node when needed. (React 19 also allows passing ref as a normal prop directly to function components without forwardRef, but forwardRef remains the widely used, backward-compatible pattern you’ll see in almost every existing codebase and library.)
A related hook, useImperativeHandle, lets a forwardRef component customize exactly what value the parent’s ref receives, instead of exposing the raw DOM node. This is useful when you want to offer a small, intentional imperative API (like focus() and clear()) while keeping the internal DOM structure private, so the parent can’t accidentally mutate things it shouldn’t.
Syntax
const myRef = useRef(initialValue);
const MyComponent = forwardRef(function MyComponent(props, ref) {
return <input ref={ref} {...props} />;
});
useImperativeHandle(ref, () => ({
someMethod() { /* ... */ }
}), [dependencies]);
| Piece | Meaning |
|---|---|
useRef(initialValue) |
Creates a mutable { current } object that persists across renders without causing re-renders when changed. |
ref={myRef} |
Attaches the ref to a host DOM element; React sets myRef.current to that DOM node after commit. |
forwardRef(renderFn) |
Wraps a component so its render function receives (props, ref), letting it forward the ref to an inner element. |
useImperativeHandle(ref, factory, deps) |
Inside a forwardRef component, replaces what the parent’s ref sees with a custom object returned by factory. |
Examples
Example 1: Focusing a plain input
import { useRef } from "react";
function FocusInput() {
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus();
}
return (
<div>
<input ref={inputRef} type="text" placeholder="Click the button to focus me" />
<button onClick={handleClick}>Focus the input</button>
</div>
);
}
export default FocusInput;
This renders a text field and a button. On mount, inputRef.current is null until React commits the DOM, after which it points to the real <input> element. Clicking the button runs handleClick, which calls the native .focus() method directly on that DOM node — something no amount of props or state could do, because focus is an imperative browser API, not a piece of renderable UI.
Example 2: Storing a mutable value that shouldn’t trigger renders
import { useState, useEffect, useRef } from "react";
function Stopwatch() {
const [seconds, setSeconds] = useState(0);
const [isRunning, setIsRunning] = useState(false);
const intervalRef = useRef(null);
useEffect(() => {
if (isRunning) {
intervalRef.current = setInterval(() => {
setSeconds((s) => s + 1);
}, 1000);
}
return () => clearInterval(intervalRef.current);
}, [isRunning]);
return (
<div>
<p>Seconds: {seconds}</p>
<button onClick={() => setIsRunning(true)}>Start</button>
<button onClick={() => setIsRunning(false)}>Stop</button>
</div>
);
}
export default Stopwatch;
Renders “Seconds: 0” plus Start and Stop buttons. Clicking Start sets isRunning to true, which reruns the effect and starts an interval; the interval’s ID is stored in intervalRef so the cleanup function can clear it later. The counter itself lives in state (because the number displayed must trigger a re-render every second), while the interval ID lives in a ref (because storing it in state would cause a pointless extra re-render every time it’s set, and the UI never needs to display the ID itself). Clicking Stop sets isRunning to false, the effect’s cleanup runs and clears the interval, and the count freezes at its current value.
Example 3: forwardRef on a custom component
import { forwardRef, useRef } from "react";
const FancyInput = forwardRef(function FancyInput(props, ref) {
return <input ref={ref} className="fancy-input" {...props} />;
});
function Form() {
const inputRef = useRef(null);
function handleFocusClick() {
inputRef.current.focus();
}
return (
<div>
<FancyInput ref={inputRef} placeholder="Type your name" />
<button onClick={handleFocusClick}>Focus FancyInput</button>
</div>
);
}
export default Form;
Renders a styled input (via the fancy-input class) and a button. Without forwardRef, attaching ref={inputRef} directly to <FancyInput /> would fail silently or warn, because FancyInput is a custom component, not a DOM tag. Because FancyInput is wrapped in forwardRef, the ref passed by the parent flows into FancyInput‘s render function as its second argument and gets attached to the real <input> inside it. Clicking the button calls .focus() on inputRef.current, which is now the actual DOM node rendered by FancyInput, so focus moves into the field exactly as in Example 1 — the wrapping component is transparent to the ref.
Example 4: Exposing a custom API with useImperativeHandle
import { forwardRef, useRef, useImperativeHandle } from "react";
const FancyInput = forwardRef(function FancyInput(props, ref) {
const inputRef = useRef(null);
useImperativeHandle(ref, () => ({
focus() {
inputRef.current.focus();
},
clear() {
inputRef.current.value = "";
}
}));
return <input ref={inputRef} {...props} />;
});
function Form() {
const fancyRef = useRef(null);
return (
<div>
<FancyInput ref={fancyRef} placeholder="Name" />
<button onClick={() => fancyRef.current.focus()}>Focus</button>
<button onClick={() => fancyRef.current.clear()}>Clear</button>
</div>
);
}
export default Form;
Renders an input and two buttons. Here, fancyRef.current in the parent is no longer the raw <input> DOM node — it’s the object returned by useImperativeHandle‘s factory function, exposing only focus and clear. Clicking “Focus” calls fancyRef.current.focus(), which internally calls the real DOM node’s .focus(); clicking “Clear” resets the input’s value. The parent gets a small, deliberate API instead of full access to the DOM node, which keeps FancyInput‘s internal markup free to change later without breaking anything that consumes it.
How it works step by step / Under the hood
- On mount: React renders the component tree, builds the DOM, and inserts it into the page. Only after this commit does React walk the tree attaching refs — setting
ref.currentto each host DOM node (or to theuseImperativeHandleobject, forforwardRefcomponents that define one). This happens beforeuseEffectcallbacks run, so a DOM ref is always safe to read insideuseEffect. - On update: If a re-render doesn’t unmount the element the ref points to, the ref object itself is untouched — it still points at the same DOM node. Only if React needs to replace the node entirely (e.g. its type changes, or its
keychanges in a list) does React detach the old ref and reattach it to the new node. - On unmount: React sets the ref back to
nullbefore removing the node from the DOM, running any effect cleanup functions around the same phase so subscriptions and timers stored in refs can be torn down safely. - Why mutating
.currentnever re-renders: refs live outside the state/props system entirely. React’s render function is only re-invoked in response to a state update (via a state setter) or a re-render of a parent; changingref.currentis just a plain object mutation that React’s scheduler has no reason to notice.
Common Mistakes
Mistake 1: Expecting a ref update to reflect in the UI
function Counter() {
const countRef = useRef(0);
function handleClick() {
countRef.current = countRef.current + 1;
console.log(countRef.current);
}
return (
<div>
<p>Count: {countRef.current}</p>
<button onClick={handleClick}>Increment</button>
</div>
);
}
This looks reasonable but is broken: clicking the button does increment countRef.current (the console logs 1, 2, 3…), but the <p> never updates on screen, because mutating a ref doesn’t schedule a re-render. React has no way of knowing the value changed. The fix is to use useState for anything the UI needs to display:
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount((c) => c + 1);
}
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>Increment</button>
</div>
);
}
Rule of thumb: if a value’s change should be visible in the rendered output, it belongs in state, not a ref.
Mistake 2: Forgetting forwardRef on a wrapped component
function Input(props, ref) {
return <input ref={ref} {...props} />;
}
// <Input ref={someRef} /> logs a warning and someRef.current stays null
A plain function component only ever receives props as its argument; React does not pass a second ref argument unless the component is created with forwardRef. Without it, the ref attribute is dropped, React (pre-19) warns that function components cannot be given refs, and someRef.current never gets set. The fix is to wrap the component:
const Input = forwardRef(function Input(props, ref) {
return <input ref={ref} {...props} />;
});
Best Practices
- Use refs only for values or nodes that don’t need to appear in the rendered UI — DOM handles, timer IDs, previous-value tracking, or third-party library instances. If it should show up on screen, use state instead.
- Never read or write
ref.currentduring rendering; only touch it inside event handlers,useEffect, oruseImperativeHandle, where the DOM is guaranteed to already exist. - Reach for
forwardRefmainly on reusable, low-level wrapper components (inputs, buttons, custom dialogs) that legitimately need to expose a DOM node to their parent. - Prefer
useImperativeHandleover exposing the raw DOM node when you want to control exactly what a parent can and can’t do to a child, keeping internal markup free to change. - Give the inner function passed to
forwardRefa name (as inforwardRef(function Input(...))) so DevTools shows a useful component name instead of “ForwardRef” or “Anonymous”. - Treat refs as an escape hatch, not a default tool — if a problem can be solved declaratively with props and state, prefer that first.
Practice Exercises
- Build a
SearchBoxcomponent with a text input and a “Select All” button that uses a ref to call the native.select()method on the input, highlighting its current text. - Create a
forwardRef-wrappedPasswordFieldcomponent that usesuseImperativeHandleto expose a singlefocusmethod. In a parent form, call that method to move focus back to the field whenever a validation check fails. - Build a
VideoPlayercomponent that wraps a native<video>element withforwardRefanduseImperativeHandle, exposingplay()andpause()methods to a parent that renders its own custom play/pause buttons outside the component.
Summary
useRef(initialValue)returns a stable{ current }object that persists across renders without ever causing a re-render when mutated.- Refs attached to host elements (
ref={myRef}on<div>,<input>, etc.) give you the actual DOM node, available after commit — read it in effects or event handlers, never during render. - Plain function components don’t accept a
refprop automatically;forwardReflets a component receive(props, ref)and forward that ref to an inner element. useImperativeHandlelets aforwardRefcomponent expose a custom, minimal API instead of the raw DOM node.- Use refs for values that shouldn’t trigger re-renders (timers, DOM handles, previous values); use state for anything that must appear in the UI.
