If you’re exploring the lesser-used features of React, you might come across forwardRef. It’s a method used to pass refs between parent and children components. In this article, we’ll talk through what refs are, forwardRef, and how to type forwardRef using generic type parameters.

What are refs?

In React, “refs” are a feature that allows developers to access and interact with individual DOM elements, or React components, directly. Refs provide a way to reference elements or components within a React application, making it possible to access their properties, methods, and trigger imperative actions. They’re a kind of “escape hatch” in React, bypassing the typical data flow in React and letting you directly manipulate specific elements or components as needed.

What is forwardRef?

The forwardRef higher-order component lets you expose a DOM element in a child, by passing a ref from the parent component to the child. Here’s what it looks like:

const MyComponent = forwardRef(function MyComponent(props, ref) {
    //render method
});

One important thing to notice is how the ref gets passed as another function parameter, not as part of the props.

The difference might get confusing when you’re using destructuring to break up your props object, e.g. you might be used to doing this:

function MyVideo(props) {}
//⬇️
function MyVideo({ videoSrc, videoAlt}) {}

Which might lead you to do this to set up forwardRef:

const MyVideo = forwardRef(function MyVideo({ videoSrc, videoAlt, ref }) {});

But that’s wrong, and here’s what you should be doing:

const MyVideo = forwardRef(function MyVideo({ videoSrc, videoAlt }, ref) {});

TypeScript

With all that being said, let’s take a look at how to introduce TypeScript into this. forwardRef is a generic function, allowing you to pass types in there for your ref, and optionally the props for your component.

You can provide a type for your props as usual, by typing the prop parameter in your function:

const MyComponent = forwardRef<HTMLDivElement, MyComponentProps>(function MyComponent(
    props: MyComponentProps,
    ref
) {
    return <div ref={ref}></div>;
});
//or
const MyComponent = forwardRef<HTMLDivElement, MyComponentProps>(
    function MyComponent(props, ref) {
        return <div ref={ref}></div>;
    }
);

Example

Now let’s take a look at a real world use case for this. I have a simple page to display a video:

Built up from a video component:

type MyVideoProps = {
    src: string;
};

function MyVideo({ src }: MyVideoProps) {
    return <video className="w-1/2 rounded" src={src} controls />;
}

And our parent page component:

export default function ForwardRefPage() {
    return (
        <main className="flex h-screen w-full items-center justify-center bg-neutral-900">
            <MyVideo src="/cats.mp4" />
        </main>
    );
}

Just a minimal page, with a video element, and some styling from TailwindCSS.

I like the native video controls, but why don’t we get rid of them and replace them with our own controls. These are accessible by providing our video element with a ref, to access the underlying DOM element:

function MyVideo({ src }: MyVideoProps) {
    const ref = useRef<HTMLVideoElement>(null);
    if (ref.current) {
        ref.current.play();
        ref.current.pause(); //etc

    }
    return <video className="w-1/2 rounded" src={src} controls ref={ref}/>;
}

That’s great, and we could leave it here and just build our controls inside this component, but I want to raise this logic up to the parent.

Firstly we’ll update the parent to include a pause/play button. When we click this button, it will call your togglePlaying function. We’ll fill this in shortly, after we have the code to be able to access the video element inside MyVideo.

export default function ForwardRefPage() {
    const [playing, setPlaying] = useState(false);
    const togglePlaying = () => {};
    return (
        <main className="flex h-screen w-full flex-col items-center justify-center gap-12 bg-neutral-900">
            <MyVideo src="/cats.mp4" />
            <div className="flex w-1/2 items-center justify-center">
                <button
                    className="flex items-center justify-center rounded bg-neutral-50 p-12 text-7xl"
                    onClick={togglePlaying}>
                    {playing ? <FaPause /> : <FaPlay />}
                </button>
            </div>
        </main>
    );
}

Here’s what that looks like:

Next, we’ll change our video component to use forwardRef:

const MyVideo = forwardRef<HTMLVideoElement, MyVideoProps>(function MyVideo({ src }, ref) {
    return <video className="w-1/2 rounded" src={src} ref={ref}/>;
});

Then we need to update our parent:

export default function ForwardRefPage() {
    const [playing, setPlaying] = useState(false);
    const ref = useRef<HTMLVideoElement>(null);

    const togglePlaying = () => {
        if (playing) {
            console.log('Pause');
            ref.current?.pause();
            setPlaying(false);
        } else {
            console.log('Play');
            ref.current?.play().then(() => setPlaying(true));
        }
    };

    return (
        <main className="flex h-screen w-full flex-col items-center justify-center gap-12 bg-neutral-900">
            <MyVideo src="/cats.mp4" ref={ref} />
            <div className="flex w-1/2 items-center justify-center">
                <button
                    className="flex items-center justify-center rounded bg-neutral-50 p-12 text-7xl"
                    onClick={() => togglePlaying()}>
                    {playing ? <FaPause /> : <FaPlay />}
                </button>
            </div>
        </main>
    );
}

The key changes here are:

  • We set up our ref in the parent now, and since our element is a video element, the ref should be a HTMLVideoElement type. There are built in types for any DOM nodes you might need, and the names are self-explanatory.
  • I’ve added in a state variable to keep in sync with when the video is playing.
    • This is to set up some conditional rendering to change our play/pause button, since changing refs doesn’t cause a re-render

So:

  • Our component renders
  • Our video ref gets created, and passed on to our MyVideo component, which uses that ref to store the native video element inside it

Then whenever we click our play button, within the parent we can access the video controls to play/pause our video.

But Why?

You might be wondering why this API exists, and why can’t we just pass the ref directly. By using forwardRef, you make it clear that the child component expects to receive a ref and intends to forward it to a specific component. This helps ensure that the ref is properly handled and avoids any ambiguity or confusion.

Conclusion

Thanks for reading! forwardRef is a slightly more niche, but still useful feature in React. The addition of TypeScript helps keep the possibly confusing code clear, readable, and free of errors. If you liked this article, or if you’re having any difficulties, feel free to leave a comment below!

Avatar photo
👋 Hey, I'm Omari Thompson-Edwards
Hey, I'm Omari! I'm a full-stack developer from the UK. I'm currently looking for graduate and freelance software engineering roles, so if you liked this article, reach out on Twitter at @marile0n

💬 Leave a comment

Your email address will not be published. Required fields are marked *

We will never share your email with anyone else.