DEV Community

Cover image for Ref forwarding with React function components and Typescript
Jonathan Experton
Jonathan Experton

Posted on • Updated on • Originally published at jexperton.dev

Ref forwarding with React function components and Typescript

While ref forwarding is well explained in the official documentation, it can be confusing to type it correctly with function components.

1. Typing the reference to be forwarded

The reference is created with the createRef function.

Unlike the useRef hook, which creates a MutableRefObject whose .current property might be initialized with an undefined value, createRef returns an immutable RefObject so you don't need to bother with the initial value type being different than the final value type.

const refTitle: React.RefObject<Text> = React.createRef<Text>();
// In real life, you don't need to explicitly type refTitle 
Enter fullscreen mode Exit fullscreen mode

In this example the reference will point to a Text component therefore the Text component is passed to createRef as a type argument to explicitly type the referenced value.

2. Forwarding the reference to the child component

With the ref attribute of the child component:

const ParentComponent: React.FC = () => {
  const refTitle = React.createRef<Text>();
  return (
    <ChildComponent ref={refTitle} title="Hello, World!" />
  );
}
Enter fullscreen mode Exit fullscreen mode

3. Assigning the forwarded ref inside the child component

As said in the documentation:

By default, you may not use the ref attribute on function components because they don’t have instances.

If you want to allow people to take a ref to your function component, you can use forwardRef

Then with the forwardRef function:

interface ChildComponentProps { 
  title: string; 
}

const ChildComponent = React.forwardRef<Text, ChildComponentProps>(
  (
    { title }: ChildComponentProps,
    ref: React.ForwardedRef<Text>,
  ): JSX.Element => {
    return (
      <Text ref={ref}>{title}</Text>
    );
  },
);

// Name the component for debugging purpose and prevent eslint warning.
ChildComponent.displayName = 'ChildComponent';
Enter fullscreen mode Exit fullscreen mode

Now there's an immutable RefObject available in the ParentComponent that points to the Text component inside the ChildComponent.

Concrete example

Here is an example with React Native where you can use ref forwarding to focus the device's screen reader on a text once the whole parent component is ready:

Photo by JC Gellidon on Unsplash

Discussion (3)

Collapse
roblevintennis profile image
Rob Levin • Edited on

You should hear from the ether that this helped me :) I looked at many examples and was fiddling quite a while until this worked for me. I used the 3rd pattern.

Fwiw, I found the return type of ReactElement to work as the index.d.ts I had didn't appear to export JSX (anymore because I've definitely used that in the past).

I find it interesting the number of hoops I have to jump through just to implement a keyboard navigation handler in react + TS but I guess that's the world of modern FE development ¯_(ツ)_/¯

Collapse
jexperton profile image
Jonathan Experton Author • Edited on

the @types/react module should declare the global JSX namespace here:

declare global {
    namespace JSX {
        interface Element extends React.ReactElement<any, any> { }
Enter fullscreen mode Exit fullscreen mode

As you can see JSX.Element is an alias of React.ReactElement<any, any> so that's probably why it works for you.

I sometimes find myself working harder on typing rather than on the code itself, but if you see typing as the only standardized, accurate and up-to-date documentation you can enforce (with a linter and git hooks), it worth the effort.

Then you still have to deal with the most difficult problem in programming: picking good names.

Collapse
roblevintennis profile image
Rob Levin • Edited on

Yeah, I think I was doing something wrong to not be able to use JSX.Element but I forget the exact error.

Regarding it being worth the effort I'm still on the fence myself. I think Typescript is a great place to apply the Pareto Principle. I barely use generics and I'm going to let that happen naturally (or not), but I'm not willing to invest as much time as it would take to learn a new language just to please Typescript. But yeah, I like the type safety overall. Applying what you described for React.forwardRef today on my project:

export const TabButton = React.forwardRef<HTMLButtonElement, TabButtonProps>(
  (
    {
...bunch of props
    }: TabButtonProps,
    tabRef: React.ForwardedRef<HTMLButtonElement>,
  ): ReactElement => {...}
Enter fullscreen mode Exit fullscreen mode

is about as terse as I'm willing to go for now. Maybe I'll surprise myself and get more fancy later down the road :-)