Command Palette

Search for a command to run...

Hectal
PHASE 8Intermediate React 19+ ~24 min· topic 3 of 6

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 ref in 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 forwardRef to 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.

Form.jsxwhole filejsx
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.

MyInput.jsxwhole filejsx
function MyInput({ ref, label, ...rest }) {
  return (
    <label>
      {label} <input ref={ref} {...rest} />
    </label>
  );
}
React 19: read ref from props and pass it ondiagram
Rendering diagram…

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.

MyInput.jsxwhole filejsx
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.

Observed.jsxwhole filejsx
<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.

Know when callbacks rundiagram
Rendering diagram…

06Type it in TypeScript

In React 19, React.ComponentProps<"input"> includes ref, so a wrapper accepts everything an input accepts.

MyInput.tsxwhole filetsx
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. 1

    Forget to pass the ref on

    In the first example, remove ref={ref} from the <input> (keep reading ref from props). Predict what happens when Edit name is clicked. (No warning in React 19: the ref is simply never attached, so nameRef.current stays null and focus() throws.)

  2. 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: do B cleanup / B attached still appear? (No. The function is the same every render, so React leaves the ref alone.)

  3. 3

    Rename the prop

    In the first example, call the prop inputRef instead: <MyInput inputRef={nameRef} /> and function MyInput({ inputRef }). It still works. Before React 19 this was a common trick to avoid forwardRef; the real ref prop is now the standard.

Code & diagrams

React 19: ref is just a prop New tab

`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

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

MyInput props: [ 'ref', 'label', 'id' ]
focused: <input#name>
The older way: forwardRef (still works) New tab

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

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

props keys: [ 'label', 'id' ] | ref is a object
focused: <input#name>
Callback refs: with and without a cleanup New tab

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

Press Run to see the page.

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 cleanup

Break 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.

terminal
$ npx tsc --noEmit
── what you'll see ──
error TS2322: Type '(el: HTMLDivElement | null) => HTMLDivElement | null' is not assignable to type 'Ref<HTMLDivElement> | undefined'.
Type '(el: HTMLDivElement | null) => HTMLDivElement | null' is not assignable to type '(instance: HTMLDivElement | null) => void | (() => VoidOrUndefinedOnly)'.
Type 'HTMLDivElement | null' is not assignable to type 'void | (() => VoidOrUndefinedOnly)'.

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.

terminal
$ Console (on click)
── what you'll see ──
Uncaught TypeError: Cannot read properties of null (reading 'focus')

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 ref into props for function components, which is why element.props.ref now exists and reading element.ref is deprecated. For class components, ref still points to the class instance, as before.

  • ▸

    forwardRef components are a special element type ($$typeof: Symbol(react.forward_ref)). Wrapping order matters with memo: React's own error says to use memo(forwardRef(...)), not forwardRef(memo(...)). With the ref prop, plain memo(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 a TextField, the <button> of a Button), and document it. For more than one target or a controlled API, use useImperativeHandle (Topic 8.4).

Remember this

  1. 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. 2

    **React 19: ref is 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 took ref out of the props, the component never saw it, and the development build warned ("Function components cannot be given refs…").

  3. 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. forwardRef still 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. 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 with null when the element was detached, so you had to check for null. 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 with null. It works like an effect's cleanup.

  5. 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 with useCallback (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. 6

    With TypeScript in React 19, React.ComponentProps<"input"> already includes ref, so a wrapper can accept every input prop plus ref and 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 returns el); write a block body ref={(el) => { saved = el; }}.

Explain it without notes

01

How do you let a parent get a ref to a DOM element inside your component in React 19? How was it done before?

02

What changed about callback refs in React 19?

03

Why does an inline callback ref run on every render, and when does that matter?

04

Why are a component's DOM nodes private by default?

Practice

01

Write a Button component that accepts every normal button prop plus ref (React 19 style), and a parent that focuses it on mount.

02

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 ref as a prop in a React 19 function component and pass it to an element.

  • I can read and write forwardRef code 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 null if the component doesn't attach it.