React Forwarding Refs

In the last lesson you learned about React Storing Mutable Values with useRef, so you’re comfortable putting a value in a ref and reading it later. This lesson fixes a common wall: putting a ref on your own component, like <TextInput ref={inputRef} />, doesn’t work, and ref forwarding is how you make it work.

🤔 Why do we need ref forwarding?

Here’s the pain. A ref normally points at a real DOM element, but your reusable components are functions, so React has nowhere to attach it.

  • You build a nice reusable TextInput component and use it all over your app.
  • One day you want the parent to focus that input, like right after a form error.
  • You try <TextInput ref={myRef} />, but myRef.current stays null.
  • The input is hidden inside the child, so the parent can’t reach it.

So ref forwarding is the bridge. It lets the child say “here, attach the parent’s ref to this element inside me”.

🚫 What happens without forwarding

Let’s see the problem first. Here a parent makes a ref and tries to put it on a custom TextInput. Watch what breaks.

import { useRef } from "react";
function TextInput(props) {
// this is a normal component, NOT a DOM element
return <input type="text" placeholder={props.placeholder} />;
}
function Form() {
const inputRef = useRef(null);
function handleClick() {
// ❌ inputRef.current is null - the ref never reached the input
inputRef.current.focus();
}
return (
<div>
<TextInput ref={inputRef} placeholder="Your name" />
<button onClick={handleClick}>Focus the input</button>
</div>
);
}

Walk through why this fails.

  • inputRef is created fine, and we pass it as ref={inputRef} on <TextInput>.
  • But TextInput is a function component, so React does not pass that ref along as a normal prop.
  • Inside TextInput, nothing ever attaches the ref to the real <input>.
  • So inputRef.current stays null, and clicking the button crashes because you can’t call .focus() on null.

React even warns you

In older React, putting a ref on a function component without forwarding logs a warning like “Function components cannot be given refs”. It’s telling you the ref had nowhere to go.

🧩 The fix: wrap the component in forwardRef

The tool for this is forwardRef. You wrap your component with it, and now it receives a second argument, the ref, which you attach to the real element inside.

import { forwardRef } from "react";
const TextInput = forwardRef(function TextInput(props, ref) {
// ref now comes in as the SECOND argument
return <input type="text" ref={ref} placeholder={props.placeholder} />;
});

Read it carefully, because the shape is the key part.

  • forwardRef(...) wraps a function that takes (props, ref). Normal components only get props, but this one also gets ref.
  • Inside, we attach that ref to the real <input> with ref={ref}.
  • Now the parent’s ref points straight at the input element, not at the component.
  • So the ref is the second argument, never a normal prop.

ref is special, not a prop

You can’t read props.ref like a regular prop, because React treats ref specially. That’s exactly why forwardRef hands it to you as a separate, second argument instead.

💡 A simple working example

Now let’s make the focus button actually work. Same parent as before, but the child uses forwardRef, so the ref reaches the input.

import { useRef, forwardRef } from "react";
const TextInput = forwardRef(function TextInput(props, ref) {
return <input type="text" ref={ref} placeholder={props.placeholder} />;
});
function Form() {
const inputRef = useRef(null);
function handleClick() {
// ✅ now inputRef.current is the real <input>
inputRef.current.focus();
}
return (
<div>
<TextInput ref={inputRef} placeholder="Your name" />
<button onClick={handleClick}>Focus the input</button>
</div>
);
}

Here’s the full path the ref travels now.

  • The parent makes inputRef and passes it as ref={inputRef} on <TextInput>.
  • Because TextInput is wrapped in forwardRef, it receives that ref as its second argument.
  • The child attaches it with ref={ref} on the real <input>.
  • So inputRef.current is now the actual input, and .focus() works.

Output

(user clicks "Focus the input")
The cursor jumps into the text box, ready for typing.

🌍 Where ref forwarding is used

You reach for forwarding any time the parent needs to touch a real element that lives inside a reusable child. It shows up a lot in shared component libraries.

  • Reusable inputs and text areas, so a form can focus or select them.
  • Custom buttons, so a parent can measure or scroll to them.
  • Modal or dialog wrappers, so opening one can move focus inside it.
  • Any design-system component that needs to behave like a plain element to its users.

So if you ever publish components for other people to use, forwarding makes your component feel just like a normal <input> or <button> to them.

🎛️ Going further: useImperativeHandle

Sometimes you don’t want to hand over the whole DOM element, just a few safe actions. That’s what useImperativeHandle is for, since it lets the child choose exactly what the parent’s ref can do.

import { useRef, forwardRef, useImperativeHandle } from "react";
const FancyInput = forwardRef(function FancyInput(props, ref) {
const innerRef = useRef(null);
// tell the parent's ref: you may only call focus() and clear()
useImperativeHandle(ref, () => ({
focus: () => innerRef.current.focus(),
clear: () => {
innerRef.current.value = "";
},
}));
return <input type="text" ref={innerRef} placeholder={props.placeholder} />;
});

Notice the trade we’re making here.

  • The child keeps its own private innerRef on the real input.
  • useImperativeHandle(ref, () => ({ ... })) decides what the parent’s ref.current will be.
  • Now the parent can only call ref.current.focus() or ref.current.clear(), nothing else.
  • The raw DOM node stays hidden, so the parent can’t reach in and break things.

So plain forwardRef gives the parent the whole element, while useImperativeHandle gives only the small, safe menu of actions you allow.

Use it sparingly

Most of the time plain forwardRef is enough. Reach for useImperativeHandle only when you truly want to limit what the parent can do, like exposing just focus and clear. Don’t wrap everything in it by habit.

⚠️ Common Mistakes

A few small slips trip people up the most. Here’s the wrong way next to the right way.

// ❌ forgetting forwardRef - ref never reaches the input
function TextInput(props) {
return <input ref={props.ref} />; // props.ref does not exist
}
// ✅ wrap in forwardRef and use the second argument
const TextInput = forwardRef(function TextInput(props, ref) {
return <input ref={ref} />;
});

Keep these in mind so the ref actually lands.

  • Don’t read props.ref. There is no such prop, so the ref comes in as the second argument only after you use forwardRef.
  • Don’t forget to actually attach the ref to an element inside. Receiving it isn’t enough, you have to put it on a real node.
  • Don’t call methods on the ref too early. ref.current is null until the element mounts, so use it inside event handlers or effects, not during the first render.

✅ Best Practices

Some simple habits keep forwarding clean and predictable.

  • Use forwarding only for components that wrap a single, clear element, like an input or a button.
  • Give the wrapped function a name (forwardRef(function TextInput(...))). It shows a readable name in React DevTools instead of “Anonymous”.
  • Prefer plain props for normal behavior. Forwarding is for reaching a DOM element, not for everyday parent-child talking.
  • Reach for useImperativeHandle only when you want to expose a small, safe set of actions instead of the whole node.

Newer React, even simpler

In the latest React, function components can accept ref as a normal prop, so you may not need forwardRef at all. But tons of code and libraries still use forwardRef today, so it’s important to recognize and understand it.

🧩 What You’ve Learned

  • ✅ A ref put on a custom component does nothing by default, because the component is not a DOM element
  • forwardRef wraps a component so it receives ref as a second argument, after props
  • ✅ You attach that forwarded ref to a real element inside the child, like <input ref={ref} />
  • ✅ The parent’s ref.current then points straight at that element, so it can call .focus() and more
  • useImperativeHandle lets the child expose only chosen actions instead of the whole DOM node
  • ref is special, so you never read it as props.ref, and it’s null until the element mounts

Check Your Knowledge

Test what you learned. Pick an answer for each question, then click Check.

  1. 1

    Why does putting a ref on a custom function component do nothing by default?

    Why: A ref attaches to a real DOM node. A custom component is a function, not an element, so without forwarding the ref has nowhere to go and current stays null.

  2. 2

    After wrapping a component in forwardRef, how does it receive the ref?

    Why: forwardRef hands the ref to your component as a separate second argument, because React treats ref specially and does not pass it as a normal prop.

  3. 3

    What do you do with the forwarded ref inside the child?

    Why: Receiving the ref is not enough. You must attach it to an actual DOM element inside the child so the parent's ref.current points at that element.

  4. 4

    When is useImperativeHandle the right tool?

    Why: useImperativeHandle lets the child decide exactly what the parent's ref can do, exposing a small safe set of actions instead of the whole DOM node. Plain forwardRef is enough otherwise.

🚀 What’s Next?

Now you can pass a ref through a component and reach the element inside, which makes your reusable components feel just like plain elements. Next we switch from refs to performance, starting with a hook that remembers an expensive result so React doesn’t redo the work on every render.

React useMemo Hook

Share & Connect