There are times when we want to exert control over a specific HTML element in a React component from its parent.
This is where the forwardRef
function comes in handy, granting us the ability to directly pass a ref to a child component.
It's worth mentioning that forwardRef
is a helper tool for passing refs to components. Therefore, to use it properly in our app, we should follow the rules and patterns applicable to refs. In this article, we'll primarily cover refs in React before proceeding to forwardRef
.
Hope you enjoy the article.
Prerequisite
- Node and npm installed on your machine
- Fundamental knowledge of ReactJS
- Typescript ( good to know )
Although I will use TypeScript in the project, it is not essential to learn it for this article.
I'll also be using Tailwind CSS for styling. While it's not mandatory to learn it beforehand, I highly suggest you do so.
Table of content
TL;DR
To pass a ref to a child component, we can utilize the forwardRef utility function by passing the child component to it, as demonstrated below:
export const ChildComponent = forwardRef<Ref, ChildComponentProps>((props, ref) => {
return (
<div>
<input ref={ref} />
</div>
);
});
What is ref?
As its name suggests, a ref is indeed a reference. The React documentation explains it best on this this page, as shown below:
When you want a component to “remember” some information, but you don’t want that information to trigger new renders, you can use a ref.
In fact, refs help us store, update, and retrieve specific data without using state and triggering re-renders.
One of the most useful uses of refs is storing references to HTML elements, similar to using IDs in vanilla JavaScript.
useRef hook
To utilize refs in our components, we employ a React hook called useRef.
By invoking it at the top level of our component and assigning its value to a variable, we effectively create a space to store the desired data.
Although we can allocate an initial value to this space.
import { useRef } from "react";
function App() {
// Create a ref, name it buttonData, define its type as string or null, set its initial value to null
const buttonData = useRef<string | null>(null);
// Access ref value with current property of the ref
const printRef = () => {
console.log(buttonData.current);
};
// Access ref value with current property of the ref and set it to Sample data
const assignValueToRef = () => {
buttonData.current = "Sample data";
};
// Access ref value with current property of the ref and set it to null
const resetRefValue = () => {
buttonData.current = null;
};
return (
<div className="flex flex-col items-center justify-center h-screen">
<div className="flex flex-col items-center gap-2">
<button
className="bg-gray-400 hover:bg-gray-200 text-gray-800 p-2 rounded-sm font-semibold"
onClick={assignValueToRef}
>
Assign value
</button>
<button
className="bg-gray-400 hover:bg-gray-200 text-gray-800 p-2 rounded-sm font-semibold"
onClick={resetRefValue}
>
Reset value
</button>
<button
className="bg-gray-400 hover:bg-gray-200 text-gray-800 p-2 rounded-sm font-semibold"
onClick={printRef}
>
Print ref to the console
</button>
</div>
</div>
);
}
export default App;
You can see the result in the screenshot bellow:
- I clicked on the Print button
- I clicked on the Assign button
- I clicked on the Print button
- I clicked on the Reset button
- I clicked on the Print button again
Using ref to store HTML elements reference
First, why do we need to store references to elements?
With the reference at our fingertips, we can perform various actions on the desired element, such as focusing on an input or scrolling it to the view.
Also third party libraries sometimes want a reference to an element to use them, take React DnD as an example. This library helps you to have drag and drop functionality in your app.
This library needs to pass a ref as a prop to the component you want to assign as drop zone. According to This ref the library will take action when you drop something onto a div for example.
How to change focus state of an input with ref?
To accomplish this, we could simply create a ref and then pass it to the element we want to focus on (an input, in our case) as a prop.
import { useRef } from "react";
function App() {
// Create a ref to store element reference and define its type as HTMLInputElement
const inputRef = useRef<HTMLInputElement>(null);
// Focus on the input element
const focusOnInput = () => {
inputRef?.current?.focus();
};
return (
<div className="flex flex-col items-center justify-center h-screen gap-4">
<button
onClick={focusOnInput}
className="bg-gray-500 text-gray-900 hover:bg-gray-300 p-2 rounded-sm"
>
Focus on input
</button>
{/* Passing ref as prop to input element, notice that we pass the ref itself and not the current */}
<input
ref={inputRef}
className="px-2 py-3 rounded-sm bg-gray-200 text-gray-900 outline-none focus:outline-3 focus:outline-lime-600"
type="text"
placeholder="Focus on me..."
/>
</div>
);
}
export default App;
Ref rethink: Avoiding overuse refs for better performance
By reading the article up to this point, you might be wondering, "Why don't I use refs instead of state? What restricts me here?"
Using refs instead of state does offer advantages in certain situations, particularly when direct manipulation of DOM elements is necessary. However, there are scenarios in which you shouldn't use refs.
With refs, we store data between renders and across different parts of a component's lifecycle. It's beneficial to have this capability to address certain issues.
However, it's essential to respect React's reactivity and the underlying structures to achieve this behavior. When using refs and updating the value of a ref, we mutate stored data with relatively few limitations. This can potentially lead to inconsistencies in our application data.
React insists, do not write or read ref.current during rendering:
Do not write or read
ref.current
during rendering.React expects that the body of your component behaves like a pure function:
- If the inputs (props, state, and context) are the same, it should return exactly the same JSX.
- Calling it in a different order or with different arguments should not affect the results of other calls.
Reading or writing a ref during rendering breaks these expectations.
Accessing a ref during the render process violates React's purity rule, as it introduces side effects into what should be a deterministic rendering process. Let's break down how this violation occurs:
Impure Functionality: When React renders a component, it expects that the component's render function behaves like a pure function. A pure function always produces the same output for the same input and has no side effects. If a component accesses a ref during rendering, it introduces side effects because the value of the ref can change independently of the component's inputs (props, state, context).
Non-deterministic Behavior: Reading or writing to a ref during render introduces non-deterministic behavior into the component. This means that the component's output can vary depending on the current state of the ref, even if the props, state, and context remain the same. Non-deterministic behavior makes it difficult to reason about how a component will behave and can lead to bugs and inconsistencies in your application.
Unpredictable Rendering: React relies on the purity of component functions to optimize rendering performance. If a component's render function is impure due to reading or writing to a ref, React cannot guarantee that the component's output will remain consistent between renders. This can lead to unnecessary re-renders and performance issues in your application.
To elaborate further on the third point, React utilizes memoization techniques to optimize rendering by caching the result of a component's render function and reusing it if the component's inputs haven't changed.
However, if a component accesses a ref during rendering, React cannot accurately determine if the component's output has changed. This uncertainty results in unnecessary re-renders, even when the component's inputs remain the same.
forwardRef
Consider a scenario where you've written a custom input component with a label. You import it like other components and modules in your file and place it somewhere in the return value of a functional component.
Now, you want to focus on the input element inside that component. Based on what we've learned, we should pass a ref to the input element and call the focus()
method of the current property of the ref when clicking the button.
However, the input element is inside that child custom component we imported, and we don’t have direct access to it.
Our solution lies in the hands of forwardRef
.
As its name suggests, forwardRef
forwards the ref to a component from its direct parent. Essentially, it adds the possibility for a child component to access the ref.
Let's see forwardRef
in action.
Creating a custom input element
Let's start by creating a custom input component that consists of a label and a simple HTML input element. We'll then place it below a button in our App component.
// CustomInput.tsx
import { InputHTMLAttributes } from "react";
// Define type of receiving props, our props should be input element attributes and a custom label prop.
type CustomInputProps = InputHTMLAttributes<HTMLInputElement> & {
label?: string;
};
export default function CustomInput({ label, ...rest }: CustomInputProps) {
const { id } = rest;
return (
<div className="flex gap-3 items-center w-full">
<label htmlFor={id}>{label}</label>
<input
className="px-2 py-3 rounded-sm bg-gray-200 text-gray-900 outline-none focus:outline-3 focus:outline-lime-600 flex-grow"
{...rest}
/>
</div>
);
}
Let's import the CustomInput component into the App component and pass any necessary props:
// App.tsx
import CustomInput from "./components/CustomInput";
function App() {
return (
<div className="flex flex-col items-center justify-center h-screen">
<div className="w-1/2 flex flex-col gap-3 items-center">
<button className="bg-gray-500 text-gray-900 hover:bg-gray-300 w-full p-2 rounded-sm">
Focus
</button>
<CustomInput
label="Custom Input"
id="custom-input"
name="custom-input"
type="text"
placeholder="Type something"
/>
</div>
</div>
);
}
export default App;
Use forwardRef to accept ref as a prop in our CustomInput
If you were to pass a ref to our CustomInput component in its current state, you would immediately encounter the following error in your console:
To enable the component to receive the ref object, we need to modify our CustomInput to create this possibility in our component to get the ref object:
// CustomInput.tsx
import { InputHTMLAttributes, forwardRef } from "react";
type CustomInputProps = InputHTMLAttributes<HTMLInputElement> & {
label?: string;
};
// Define ref type the component accepts
type Ref = HTMLInputElement;
// Give previous input component to the forwardRef utility function as parameter
export const CustomInput = forwardRef<Ref, CustomInputProps>(
({ label, ...rest }, ref) => {
const { id } = rest;
return (
<div className="flex gap-3 items-center w-full">
<label htmlFor={id}>{label}</label>
{/* Pass ref as prop to the input */}
<input
ref={ref}
className="px-2 py-3 rounded-sm bg-gray-200 text-gray-900 outline-none focus:outline-3 focus:outline-lime-600 flex-grow"
{...rest}
/>
</div>
);
}
);
// Export return value of the forwardRef function as default export
export default CustomInput;
React forwardRef function accepts a render function, as you can see we pass it as an arrow function.
The render function itself has the following structure:
Parameters:
- props
- ref
Returns:
Based on React documentation:
forwardRef
returns a React component that you can render in JSX. Unlike React components defined as plain functions, the component returned byforwardRef
is able to take aref
prop.
Typescript note: to define your component type, you should follow this structure:
const Component = forwardRef<Ref,PropsType>(props,ref)
Ref: type of the ref we want to pass to the component
PropsType: Type of the props the component accepts
Pass ref to our CustomInput component
Now that our component is capable of accepting a ref, we should pass our desired ref to it:
// App.tsx
import { useRef } from "react";
import CustomInput from "./components/CustomInput";
function App() {
// Define the ref with the desired type
const inputRef = useRef<HTMLInputElement>(null);
// Declare a function to focus on the input element
const focusOnInput = () => {
inputRef.current?.focus();
};
return (
<div className="flex flex-col items-center justify-center h-screen">
<div className="w-1/2 flex flex-col gap-3 items-center">
<button
onClick={focusOnInput}
className="bg-gray-500 text-gray-900 hover:bg-gray-300 w-full p-2 rounded-sm"
>
Focus
</button>
{/* Pass the ref to the component */}
<CustomInput
ref={inputRef}
label="Custom Input"
id="custom-input"
name="custom-input"
type="text"
placeholder="Type something"
/>
</div>
</div>
);
}
export default App;
As you can see, I also declared a function to focus on the input element and passed it as an onClick handler to the button.
Here is the result:
Summary
In this article, we explored the concept of refs in React, one of its amazing and useful features.
Refs are references to data similar to states, but unlike states, we can mutate them. They allow us to store references to HTML elements and access them to perform actions like focusing on or scrolling them into view.
Third-party libraries often require access to an element, necessitating the passing of refs to them. Refs act similarly to regular HTML id attribute.
To use refs properly and maintain our application's performance, we should adhere to certain patterns, as discussed in the article.
When we need to pass a ref to a component, we utilize a React utility function called forwardRef
. The return value of this function is JSX that we can use in our app, allowing us to pass our desired ref as a prop.
Thank you for reading, and I hope you found this article helpful.
Top comments (2)
Thank you for sharing.
Your welcome, thank you for reading <3