React Forwarding Refs
Table of Contents + −
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
TextInputcomponent 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} />, butmyRef.currentstaysnull. - 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.
inputRefis created fine, and we pass it asref={inputRef}on<TextInput>.- But
TextInputis a function component, so React does not pass thatrefalong as a normal prop. - Inside
TextInput, nothing ever attaches the ref to the real<input>. - So
inputRef.currentstaysnull, and clicking the button crashes because you can’t call.focus()onnull.
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 getprops, but this one also getsref.- Inside, we attach that
refto the real<input>withref={ref}. - Now the parent’s ref points straight at the input element, not at the component.
- So the
refis 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
inputRefand passes it asref={inputRef}on<TextInput>. - Because
TextInputis wrapped inforwardRef, it receives that ref as its second argument. - The child attaches it with
ref={ref}on the real<input>. - So
inputRef.currentis 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
innerRefon the real input. useImperativeHandle(ref, () => ({ ... }))decides what the parent’sref.currentwill be.- Now the parent can only call
ref.current.focus()orref.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 inputfunction TextInput(props) { return <input ref={props.ref} />; // props.ref does not exist}
// ✅ wrap in forwardRef and use the second argumentconst 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 useforwardRef. - Don’t forget to actually attach the
refto 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.currentisnulluntil 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
useImperativeHandleonly 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
- ✅
forwardRefwraps a component so it receivesrefas a second argument, afterprops - ✅ You attach that forwarded
refto a real element inside the child, like<input ref={ref} /> - ✅ The parent’s
ref.currentthen points straight at that element, so it can call.focus()and more - ✅
useImperativeHandlelets the child expose only chosen actions instead of the whole DOM node - ✅
refis special, so you never read it asprops.ref, and it’snulluntil the element mounts
Check Your Knowledge
Test what you learned. Pick an answer for each question, then click Check.
- 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
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
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
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.