engineering

What is forwardRef in React?

What is forwardRef in React?

Learn about forwarding refs in React, forwardRef function, how does it work, its code examples, limitations, and alternatives!

Kaushal Joshi

Kaushal Joshi

Dec 13, 2024 10 min read

If you’ve been working with React, you’ve likely come across the concept of forwardRef. You know what forwarding ref in React means, why we need to forward refs in React, and how to forward refs in React. You’ve seen its use cases and might even know its drawbacks. Perhaps you’ve seen some code examples of forwardRef.

But it can be challenging to remember everything clearly when it’s time to implement or explain it—whether in frontend interviews or while writing code.

If that’s you, don’t worry! You’ve found the right guide. This article is the ultimate resource on forwardRef in React. We’ll explore its basics, dive deep into its advanced use cases, and provide interview questions and code examples to help you solidify your understanding.

Prerequisites and Assumptions

This article will cover an advanced topic in React.js. While writing it, I've already assumed that you're well familiar with JavaScript, TypeScript, and React.js. This is what you need to know to grasp this article smoothly:

  1. React.js: components architecture, passing down props, the useRef hook

  2. JavaScript: DOM APIs like .focus(), etc.

  3. TypeScript: Typefacing HTML DOM elements.

Out of all, familiarity with the useRef hook is a must. If you want to brush up on the useRef hook, I'd highly recommend reading this article. It covers everything that you need to know about the hook.

With that in mind, let's get started.

Why Can't You Forward Refs Like Any Other Props?

My first intuition when I came across forwardRef() for the first time was "why don't we pass refs like any other prop". While researching about this, I came across an interesting thing in React.

React has a set of keywords that you cannot use as prop names. You know, and have used one such keyword, key. React internally uses this to interact with underlying DOM or class components. If you pass a prop named ref, it would conflict with React's internal mechanisms for handling refs. React is designed to "consume" this prop in a specific way, and allowing it as a custom prop would create conflicts in how the virtual DOM is reconciled with the real DOM.

And hence, we cannot use ref as a prop name in React. To overcome this limitation, React introduced a function called fowardRef() that allows passing refs to child components.

How forwardRef works?

React still provides a way to explicitly pass a ref through a component to one of its child DOM nodes. That's where the forwardRef function comes into the pictrue.

You can import it like the following:

import { forwardRef } with "react";

The forwardRef method accepts a functional component that returns JSX. This function takes two arguments, props and ref.

  1. props: The props you pass to a child component.

  2. ref: The ref object passed down from the parent component. It can hold a reference to DOM nodes or React components and enable direct interaction with it.

The JSX returned by the forwardRef() will be the output of your component. You can use it just like any React functional component you use. There is just one difference — the component now receives ref prop!

Code example of forwardRef

Let’s see a simple example of forwardRef:

import React, { forwardRef } from "react";

type InputProps = React.InputHTMLAttributes<HTMLInputElement>;

const CustomInput = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
  return <input ref={ref} {...props} />;
});

export default CustomInput;

Inside the parent component:

import React, { useRef } from "react";
import CustomInput from "@/components/ui/custom-input";

const App = () => {
  const inputRef = useRef<HTMLInputElement>(null);

  const handleFocus = () => {
    inputRef.current?.focus();
  };

  return (
    <div>
      <CustomInput ref={inputRef} placeholder="Type something..." />
      <button onClick={handleFocus}>Focus Input</button>
    </div>
  );
};

export default App;

The snippet might confuse you if you’re seeing this for the first time. So let’s understand the code:

  • The CustomInput component uses forwardRef to pass the ref from the parent component to the underlying <input> element.

  • A function with two arguments viz. props and ref is passed to the forwardRef function. This is the React component we already use everywhere else. The only difference is that it has a second argument along with prop — ref.

  • forwardRef is explicitly typed with HTMLInputElement for the ref and InputProps for props. React.InputHTMLAttributes<HTMLInputElement> ensures the component supports all standard <input> attributes.

  • The parent component uses useRef to create a reference to the input element.

  • The handleFocus function demonstrates how the parent can control the input element using the ref.

It might feel complicated at first glance, so make sure you read it thoroughly and understand it better.

Real-World Use Cases of Forwarding Refs

Building reusable UI components

If you're building a component library, forwarding refs to the UI component that wraps a single DOM element (Like <input>, <table>, <td>, etc. is very crucial. This enables developers to access the underlying element at its fullest capacity.

Shadcn UI is the best example of this. The component uses forwardRef() to wrap the UI component, hence providing ability to create a ref in parent component and attach to the UI component.

Stimulating class component patterns in functional components

In class components, refs are attached to the instance of the class. Hence you can access all its internal properties and methods from the parent component. However, as functional components don't create its instances, we cannot access internal methods. Let me explain.

Suppose we want to expose internal methods and group them in one ref. In class components we can simply write our methods and attach it to our components which will then have the instance of an entire class.

As there are no instances of a functional component, this doesn't work for functional components. So we've to attach all those methods manually with the ref with useEffeect or useImeerativeHandle.

const FunctionalComponent = forwardRef((props, ref) => {
	const firstInternalMethod = () => console.log("First internal method")
	const secondInternalMethod = () => console.log("Second internal method")

	useEffect(() => {
		ref.current = {
			firstInternalMethod,
			secondInternalMethod
		}
	}, [])

	return <p>I am a functional component</p>
})

In this case it makes sense to call it ref and nothing else as it is going to be attached to the whole component and not a particular DOM node.

This is a very niche use case, and you might never come across a scenario where you need to use this. Still it's a good thing to know, as you can always get an extra mark during an interview ;)

Drawbacks of forwardRef

The forwardRef function has many drawbacks and limitations which makes it a hard choice to consider. Let’s look at some points that make forwardRef painful to work with.

You can't pass multiple refs

forwardRef allows passing only a single ref. If you need to manage multiple DOM nodes or component instances within a single child, additional logic is required to manage multiple references.

Debugging in React DevTools is difficult

When using arrow functions as an argument to forwardRef, React DevTools displays an automatically created a component name. This complicates debugging a lot.

const Input = React.forwardRef((props, ref) => <input ref={ref} {...props} />);

This is what you'll see inside the React DevTools:

React's official docs uses named functions instead of arrow functions.

const Input = React.forwardRef(function Input(props, ref) {
  <input ref={ref} {...props} />
});

Although this works, you have to write similar code twice. And most importantly, you must remember about this problem as well as its solution while you are coding.

Another simple workaround is setting the displayName property of the component with the name you wish to see in the DevTools.

const Input = React.forwardRef((props, ref) => <input ref={ref} {...props} />);
Input.displayName = "Input with forwardRef";

export default Input;

This is React DevTools shows such components:

I learned this while exploring Shadcn UI components. I believe this is a good solution to resolve this problem. But still, you’ve to write similar code twice.

Ambiguous naming

While passing the ref from the parent component, a generic name like ref provides no context about what the ref attaches to. If the child component has multiple similar elements (two input elements inside the same components), there's no way to figure out which element the ref would be attached to.

TypeScript challenges

Forwarding refs with generics can lead to type inference issues, especially when dealing with generic components. Without proper typing, the ref type may default to unknown.

const RenderData = forwardRef(function RenderData<T>({
  items,
  onSelect
}: {
  items: T[],
  onSelect: (item: T) => void
}) {
  return (
	<>
		{
			items.map(item => (
				<div onClick={() => onSelect(item)}>{JSON.stringify(item)}</div>
			))
		}
	</>
  )
})

Inside a parent component, let’s define a simple mock data of array of objects and pass it down to the child component.

type Data = {
  country: string;
  capital: string;
}

const ParentComponent = () => {
  const data: Data[] = [
	{ country: "India", capital: "New Delhi" },
	{ country: "USA", capital: "Washington, D.C." },
	{ country: "Japan", capital: "Tokyo" },
  ];

  return (
	<>
	  <RenderData
		items={data}
		onSelect={(country) => console.log(country)}
	  />
    </>
  )
}

As the <RenderData /> component is wrapped inside forwardRef, you cannot determine types properly.

How to Overcome Limitations of forwardRef

Dan Abramov proposed a flexible approach to avoid many limitations of forwardRef. Instead of using ref, you can pass refs down as regular props with descriptive names.

After all, a ref is just a JavaScript object with a property called current. Therefore, you can pass it down as a regular prop with a descriptive name.

import { ElementRef, RefObject} from "react";

type FormProps = {
	inputRef: RefObject<ElementRef<"input">>;
	buttonRef: RefObject<ElementRef<"button">>;
};

const Form = ({ inputRef, buttonRef }: FormProps) => {
	return (
		<form>
			<input ref={inputRef} placeholder="Enter text" />
			<button ref={buttonRef} type="submit">
				Submit
			</button>
		</form>
	);
};

export default Form;

And this is how you can call it from a parent component:

import React, { useRef } from "react";
import Form from "@components/ui/form";

const App = () => {
  const inputRef = useRef<HTMLInputElement>(null);
	const buttonRef = useRef<HTMLButtonElement>(null);

	const handleFocus = () => {
		inputRef.current?.focus();
	};

	const handleClick = () => {
		buttonRef.current?.click();
	};

	return (
		<div>
			<Form inputRef={inputRef} buttonRef={buttonRef} />
			<button onClick={handleFocus}>Focus Input</button>
			<button onClick={handleClick}>Click Submit from Parent</button>
		</div>
	);
};

export default App;

This approach avoids the complexity of forwardRef while still achieving the desired functionality. It solves all the problems we discussed in the previous section:

  1. You can pass down multiple refs to the child components.

  2. You don't have to use named functions, or displayName property to see the component name while debugging.

  3. Prop names are descriptive, hence increasing DX and code quality.

  4. TypeScript can understand types of props better.

Interview questions

By now, you must have a great understanding of the forwardRef function. Let's revise our learnings by preparing with some interview questions:

  1. Explain what happens when you pass a ref to native HTML element, class component and a functional component.

  2. Why do we need forwardRef? Why can't we forward it like any other props?

  3. What are the real-world use cases of forwarding refs to child components?

  4. What arguments does the forwardRef function take? And what does it return?

  5. What are the limitations and drawbacks of using forwardRef? How to overcome them?

References

While researching for this article, I came across some amazing resources that helped me get deeper understanding of the whole topic. I'd like to share them here:

  1. Never Forget React forwardRef Again by Coding in Public: A great video to understand the nuances of forwardRef.

  2. Goodbye, forwardRef by UI Engineering: This video explains the problems with forwardRef and provides a simpler approach in a very in-depth manner.

  3. TypeScript + React: Typing Generic forwardRefs by Stefan Baumgartner: This blog discusses working with generic type and forwardRefs.

  4. useRef blog would help you get a deeper understanding of the useRef hook

Wrapping up

I really hope you found this comprehensive deep dive useful. If you did, do share this with your friends and peers as well!

And hey, if you're on the lookout for a Frontend Developer Job, or want to share an exciting project you built, check out Peerlist!

Create Profile

or continue with email

By clicking "Create Profile“ you agree to our Code of Conduct, Terms of Service and Privacy Policy.