Learn how to use the forwardRef function in React to expose DOM nodes inside a component to its parent component.
In React, refs are used for storing values that don’t trigger a re-render when updated. We can also assign refs to DOM elements so that we can reference the ref to manipulate the DOM element assigned to the ref.
Refs can also be assigned components, but we need to do one extra step. We need to forward the ref to the DOM element we want to assign or set the value we want to return when the ref is referenced with forwardRef
.
In this article, we will look at how to use the forwardRef
function to expose DOM nodes that are inside a component.
We call the forwardRef
function with a render function. A render function returns a group of DOM elements or components.
forwardRef
is needed to expose a DOM node in a component to its parent component.
For instance, we write:
import { forwardRef, useRef } from "react";
const CustomInput = forwardRef((props, ref) => {
const { label } = props;
return (
<>
<label>{label}</label>
<input ref={ref} />
</>
);
});
export default function App() {
const inputRef = useRef();
return (
<div>
<CustomInput label="Name" ref={inputRef} />
<button onClick={() => inputRef.current.focus()}>focus</button>
</div>
);
}
to define the CustomInput
component.
We allow the CustomInput
component to be assigned a ref by calling forwardRef
with the render function. The function takes the props
of the component and the ref
.
We assign the ref
parameter value as the value of the input’s ref
prop so that the input will be returned as the value of inputRef.current
when we reference it in App
.
props
is the props for the CustomInput
component. Therefore, the label
prop’s value would be Name
.
In App
, we create a ref with useRef
and assign that as the value of CustomInput
's ref
prop.
Then we assign the onClick
prop of the button to call inputRef.current.focus()
, where inputRef.current
is the input element within the CustomInput
component.
Therefore, when we click the focus button, the input will have focus.
We can customize the value of the ref with the useImperativeHandle
hook. useImperativeHandle
takes the ref, a callback that returns the value we want for the ref, and an array of dependencies to watch for to trigger the callback to call as arguments.
For instance, we write:
import { forwardRef, useImperativeHandle, useRef } from "react";
import { v4 as uuidv4 } from "uuid";
const CustomInput = forwardRef((props, ref) => {
const { label } = props;
const inputRef = useRef(null);
useImperativeHandle(
ref,
() => {
return {
focus() {
inputRef.current.focus();
},
scrollIntoView() {
inputRef.current.scrollIntoView();
}
};
},
[]
);
return (
<>
<label>{label}</label>
<div>
<input ref={inputRef} />
</div>
</>
);
});
export default function App() {
const inputRef = useRef();
return (
<div>
<div>
<button onClick={() => inputRef.current.focus()}>focus</button>
<button onClick={() => inputRef.current.scrollIntoView()}>
scroll into view
</button>
</div>
{Array(100)
.fill()
.map((_, i) => {
const key = uuidv4();
return (
<CustomInput
key={key}
label={`Name ${i + 1}`}
ref={i === 99 ? inputRef : undefined}
/>
);
})}
</div>
);
}
to change CustomInput
to call the useImperativeHandle
hook.
It takes the ref
we want to forward as the first argument. The ref
will then be set to the value returned by the callback in the second argument.
We choose to return the focus
and scrollIntoView
method. focus
calls focus
on the input element we assigned the inputRef
to by setting the ref
prop of the input to inputRef
.
And in scrollIntoView
, we call the input’s scrollIntoView
method to scroll the page until the input is visible on the screen.
The third argument is an empty array since we don’t want to return a new value when anything with a reactive value changes, including props and states.
Next, in App
, we render 100 CustomInput
components. We assign the inputRef
in the App
component as the ref
prop’s value if index i
in 99. Otherwise, we don’t assign a ref to it.
Therefore, the last CustomInput
rendered, which is the bottommost one, would be assigned inputRef
and the rest has no ref assigned to them.
Afterward, we add two buttons. The first button calls inputRef.current.focus()
to call focus
on the CustomInput
that has been assigned App
's inputRef
.
Likewise, the second button calls scrollIntoView
with the same CustomInput
. As a result, when we click scrollIntoView
, we scroll to the bottom of the page. And when we click focus, the last input is focused on.
Refs can be forwarded through multiple components.
For instance, if we write:
import { forwardRef, useRef } from "react";
const CustomInput = forwardRef((props, ref) => {
const { label } = props;
return (
<>
<label>{label}</label>
<div>
<input ref={ref} />
</div>
</>
);
});
const InputGroup = forwardRef((props, ref) => {
const { label } = props;
return (
<>
<CustomInput ref={ref} label={label} />
<select>
<option>foo</option>
<option>bar</option>
</select>
</>
);
});
export default function App() {
const inputRef = useRef();
return (
<div>
<div>
<button onClick={() => inputRef.current.focus()}>focus</button>
</div>
<InputGroup label="Name" ref={inputRef} />
</div>
);
}
to define the InputGroup
component, which renders a CustomInput
component.
InputGroup
is created by calling the forwardRef
function so that it can accept the ref
prop.
Then we pass the ref
to CustomInput
by assigning the ref
parameter of its render function to CustomInput
's ref
prop.
And in CustomInput
's render function, we assign its ref
parameter to the input’s ref
prop.
As a result, the App
component’s inputRef
value is the input element in CustomInput
since that’s where the ref is ultimately forwarded to.
John Au-Yeung is a frontend developer with 6+ years of experience. He is an avid blogger (visit his site at https://thewebdev.info/) and the author of Vue.js 3 By Example.