Topic 8.3
Passing Refs to Your Components
In one line
Since React 19 a function component receives ref as an ordinary prop and can pass it on to a DOM element; older code wraps the component in forwardRef instead, and ref callbacks can now return a cleanup function.
Think of it like this
A parcel addressed to "the person in flat 4" arrives at the building's front desk. The desk doesn't keep it; it passes it up to flat 4. When a parent writes <MyInput ref={inputRef} />, your MyInput component is the front desk: it receives the ref and must pass it on to the real <input> inside, so the ref ends up holding the real element.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Ref as a prop
- React 19 behaviour: a function component receives
refin its props like any other value. - Forwarding a ref
- Passing a ref a component received on to an element inside it, so the parent reaches that element.
- forwardRef
- The older API (React 16.3–18) that wraps a component so it receives a ref as a second argument. Still works; will be deprecated.
- Callback ref
- A function passed as
ref. React calls it with the node when the element is attached. - Ref cleanup
- React 19: a function returned from a callback ref. React calls it when the element is detached.
- Attach / detach
- React connecting a ref to a node after commit, and disconnecting it when the element is removed or the ref changes.
- Codemod
- A script that rewrites code automatically, for example from
forwardRefto the ref prop.
Step by step
01A ref on your own component
The parent wants to focus a text box that lives inside your MyInput component. It creates a ref and puts it on MyInput.
const nameRef = useRef(null);
<MyInput ref={nameRef} label="Name" />
<button onClick={() => nameRef.current.focus()}>Edit</button>02React 19: read ref from props and pass it on
MyInput reads ref like any prop and gives it to the <input>. Now nameRef.current in the parent is the real <input> node.
function MyInput({ ref, label, ...rest }) {
return (
<label>
{label} <input ref={ref} {...rest} />
</label>
);
}03The same component in older code: forwardRef
Before React 19 you wrapped the component. The ref comes as the second argument. You'll meet this in libraries and older apps; it still works in React 19.
import { forwardRef } from "react";
const MyInput = forwardRef(function MyInput({ label, ...rest }, ref) {
return (
<label>
{label} <input ref={ref} {...rest} />
</label>
);
});04Callback refs and their cleanup
A callback ref gets the node. In React 19 it can return a cleanup function, which React calls when the node is detached. Use it to undo whatever you did on attach: remove a listener, disconnect an observer, delete the node from a Map.
<div
ref={(node) => {
const observer = new ResizeObserver(() => console.log("resized"));
observer.observe(node);
return () => observer.disconnect(); // React 19 ref cleanup
}}
/>05Know when callbacks run
After each commit where the callback function changed (an inline function changes every render), React first detaches the old callback, then attaches the new one. On unmount it only detaches.
06Type it in TypeScript
In React 19, React.ComponentProps<"input"> includes ref, so a wrapper accepts everything an input accepts.
type Props = React.ComponentProps<"input"> & { label: string };
export function MyInput({ label, ref, ...rest }: Props) {
return (
<label>
{label} <input ref={ref} {...rest} />
</label>
);
}Try it yourself
- 1
Forget to pass the ref on
In the first example, remove
ref={ref}from the<input>(keep readingreffrom props). Predict what happens when Edit name is clicked. (No warning in React 19: the ref is simply never attached, sonameRef.currentstaysnullandfocus()throws.) - 2
Stabilise a callback ref
In the callback example, move B's callback out of
App(a function at the top of the module that doesn't use any state). Re-render: doB cleanup/B attachedstill appear? (No. The function is the same every render, so React leaves the ref alone.) - 3
Rename the prop
In the first example, call the prop
inputRefinstead:<MyInput inputRef={nameRef} />andfunction MyInput({ inputRef }). It still works. Before React 19 this was a common trick to avoidforwardRef; the realrefprop is now the standard.
Code & diagrams
`MyInput` receives `ref` in its props and passes it to the `<input>`. The parent's ref ends up holding the real input, so Edit name can focus it.
Preview
Console
Nothing logged yet.Expected console output
MyInput props: [ 'ref', 'label', 'id' ]
focused: <input#name>Same result, older API. React 19.2 prints no warning for `forwardRef`; it is on the path to deprecation, so prefer the ref prop in new code.
Preview
Console
Nothing logged yet.Expected console output
props keys: [ 'label', 'id' ] | ref is a object
focused: <input#name>Inline callbacks are new functions every render, so a re-render detaches and re-attaches both. All detaching happens before any attaching. When the boxes are removed, A is called with `null` and B's cleanup runs.
Preview
Console
Nothing logged yet.Expected console output
A got <p.a>
B attached <p.b>
--- re-render ---
A got null
B cleanup
A got <p.a>
B attached <p.b>
--- hide ---
A got null
B cleanupBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Return a value from a callback ref (TypeScript)
In a .tsx file write <div ref={(el) => (saved = el)} />, an arrow with an expression body.
Break #2
Pass a ref to a component that ignores it
Render <Fancy ref={r} /> where function Fancy() { return <input />; } never reads ref. Then call r.current.focus() from a button.
Myth vs fact
Myth
You always need forwardRef to pass a ref to a component.
Fact
Only before React 19. In React 19 function components receive ref as a prop. forwardRef still works but is on the way to deprecation.
Myth
A ref on a component gives you the component's DOM.
Fact
It gives you whatever the component chooses to attach it to. If the component ignores the prop, the ref stays null; it can also expose a custom object instead (Topic 8.4).
Myth
Callback refs are called once.
Fact
An inline callback is detached and re-attached after every commit, because it's a new function each render. React 19's StrictMode also runs attach → cleanup → attach once extra on mount in development.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
React 19 put
refinto props for function components, which is whyelement.props.refnow exists and readingelement.refis deprecated. For class components,refstill points to the class instance, as before. - ▸
forwardRefcomponents are a special element type ($$typeof: Symbol(react.forward_ref)). Wrapping order matters withmemo: React's own error says to usememo(forwardRef(...)), notforwardRef(memo(...)). With the ref prop, plainmemo(Component)just works. - ▸
Ref cleanup timing matches effect cleanup timing in the commit's layout phase: detach happens before attach, so moving a ref from one element to another never leaves both attached. If a callback returns a cleanup, React does not also call it with
null. - ▸
Design rule for component libraries: forward the ref to the one element a consumer most likely needs (the
<input>of aTextField, the<button>of aButton), and document it. For more than one target or a controlled API, useuseImperativeHandle(Topic 8.4).
Remember this
- 1
A ref on a built-in element (
<input ref={r} />) gets the DOM node. A ref on your own component (<MyInput ref={r} />) is different: your component has no DOM node of its own, it only returns other elements. So React needs you to decide which inner element (if any) the parent may reach. By default, none: a component's DOM is private, which keeps components independent. - 2
**React 19:
refis a regular prop.** A function component reads it from its props like any other prop,function MyInput({ ref, label }), and passes it to an element,<input ref={ref} />. That's all. Before React 19 this didn't work: React tookrefout of the props, the component never saw it, and the development build warned ("Function components cannot be given refs…"). - 3
**
forwardRef(the older way).** In React 16.3 to 18 you wrapped the component:const MyInput = forwardRef(function MyInput(props, ref) { ... }). The ref arrives as a second argument, not in props.forwardRefstill works in React 19 with no warning, so you'll see it in lots of existing code and libraries, but the React team has said it will be deprecated in a future version. New code should use the ref prop; the React 19 upgrade codemods can convert old code. - 4
Callback refs. Instead of a ref object you can pass a function:
ref={(node) => { ... }}. React calls it with the DOM node when the element is attached. Before React 19, React called it again withnullwhen the element was detached, so you had to check fornull. React 19 adds ref cleanup: if your callback returns a function, React calls that function when the element is detached, instead of calling your callback withnull. It works like an effect's cleanup. - 5
Inline callbacks run again on every render. A function written inline in JSX is a new function each render, so React detaches the old one (cleanup, or a call with
null) and attaches the new one (a call with the node) after each commit. That's usually harmless. If the callback does expensive work, keep it stable withuseCallback(Topic 16.3) so it runs only when the element really changes. In StrictMode (development only), React 19 also runs attach → cleanup → attach once extra on mount to check your cleanup. - 6
With TypeScript in React 19,
React.ComponentProps<"input">already includesref, so a wrapper can accept every input prop plusrefand spread them on. Because a ref callback may now return a cleanup, the types reject callbacks that return anything else:ref={(el) => (saved = el)}is a type error (it returnsel); write a block bodyref={(el) => { saved = el; }}.
Explain it without notes
How do you let a parent get a ref to a DOM element inside your component in React 19? How was it done before?
What changed about callback refs in React 19?
Why does an inline callback ref run on every render, and when does that matter?
Why are a component's DOM nodes private by default?
Practice
Write a Button component that accepts every normal button prop plus ref (React 19 style), and a parent that focuses it on mount.
Convert this to React 19 style: const Search = forwardRef((props, ref) => <input ref={ref} type="search" {...props} />);
Trade-offs
- ↔
Forwarding a ref makes the inner element part of your component's public API: if you later change the markup, users' code that relies on the node may break. Forward deliberately, to one obvious element.
- ↔
Libraries that still support React 18 must keep
forwardRef, because the ref prop doesn't work there. Apps on React 19 can drop it.
Done when you can
I can accept
refas a prop in a React 19 function component and pass it to an element.I can read and write
forwardRefcode and know it is the pre-React 19 way.I can write a callback ref that returns a cleanup function.
I can explain why inline callback refs run after every render.
I know a ref on a component stays
nullif the component doesn't attach it.