Introduction
Understanding why forwardRef
exists and whether it’s truly necessary in React has been a complex journey. After identifying seven significant issues with forwardRef
and recognising a simpler and superior alternative, it became evident that forwardRef
could be removed from React without much impact. In fact, there’s already an open RFC to remove it.
Understanding ref
in React
When we pass a ref
to a native HTML element like an <input>
, it attaches automatically to the DOM node. We gain access to the native DOM API for this node.
import React, { useRef, useEffect } from 'react';
const App: React.FC = () => {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(()=>{
if (inputRef.current) {
// Accessing the DOM node
inputRef.current.focus();
}
}, []);
return <input ref={inputRef} />;
};
For class components, the ref
attaches to the instance of the class, allowing us to access its internal properties and methods. However, for functional components, the ref
results in a null
value and a warning. To make ref
work with functional components, we wrap them in the forwardRef
API:
import React, { forwardRef, useRef } from 'react';
const Child = forwardRef<HTMLDivElement, {}>((props, ref) => (
<div ref={ref}>Child Component</div>
));
const App: React.FC = () => {
const childRef = useRef<HTMLDivElement>(null);
return <Child ref={childRef} />;
};
The Problems with forwardRef
-
Lack of Support for Multiple Refs:
forwardRef
only allows one argument, making it cumbersome to handle multiple refs without workarounds. For example:
import React, { forwardRef, Ref, useImperativeHandle, useRef } from 'react';
interface FormHandles {
inputRef1: Ref<HTMLInputElement>;
inputRef2: Ref<HTMLInputElement>;
}
const Form = forwardRef<FormHandles>((props, ref) => {
const inputRef1 = useRef<HTMLInputElement>(null);
const inputRef2 = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => ({
inputRef1,
inputRef2,
}));
return (
<form>
<input ref={inputRef1} />
<input ref={inputRef2} />
</form>
);
});
const App: React.FC = () => {
const formRef = useRef<FormHandles>(null);
return <Form ref={formRef} />;
};
-
Anonymous Functions in Dev Tools: Using arrow functions with
forwardRef
results in anonymous functions in Dev Tools unless you name the function twice:
const NamedComponent = forwardRef<HTMLDivElement>((props, ref) => (
<div ref={ref}>Named Component</div>
));
const NamedComponent = forwardRef<HTMLDivElement>(function NamedComponent(props, ref) {
return <div ref={ref}>Named Component</div>;
});
- Extra Boilerplate: We need to use additional API and imports, making our code more complex and less readable.
- Nested Components: Passing refs through multiple layers of components adds unnecessary complexity.
const InnerComponent = forwardRef<HTMLDivElement>((props, ref) => (
<div ref={ref}>Inner Component</div>
));
const OuterComponent = forwardRef<HTMLDivElement>((props, ref) => (
<InnerComponent ref={ref} />
));
const App: React.FC = () => {
const outerRef = useRef<HTMLDivElement>(null);
return <OuterComponent ref={outerRef} />;
};
Non-Descriptive Prop Names: Generic
ref
names likeref
are not descriptive, making it unclear where theref
is being attached.Typing Issues with Generics:
forwardRef
breaks TypeScript generics, making type inference harder and less reliable. You can find more information here.Potential Performance Issues: Wrapping components in
forwardRef
can slow down rendering, especially in stress tests with a large number of components. You can find more information here.
The Simpler Alternative
A simpler and better alternative exists: using custom ref
props. Instead of ref
, we can use any other prop name like firstInputRef
. This pattern works automatically with functional components, solving all the issues mentioned:
import React, {Ref, useRef, useEffect } from 'react';
interface ChildProps {
firstInputRef: Ref<HTMLInputElement>;
}
const Child: React.FC<ChildProps> = ({ firstInputRef }) => (
<div>
<input ref={firstInputRef} />
</div>
);
const App: React.FC = () => {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (inputRef.current) {
console.log(inputRef.current); // <input />
}
}, []);
return <Child firstInputRef={inputRef} />;
};
Conclusion
For most cases, using custom ref
props is a better solution than forwardRef
. It simplifies our code, improves readability, and avoids many issues. forwardRef
is only necessary in specific scenarios like single element proxy components or when simulating instance refs. With the new RFC potentially removing forwardRef
, we can look forward to a simpler, more intuitive way of handling refs in React.
Image by u_vplf3ftkcz from Pixabay.
Was that helpful?
Top comments (0)