iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🧩

How to Use TypeScript Generics with React.forwardRef

に公開

In this article, I will introduce how to handle the issue where React.forwardRef cannot work with Props that have TypeScript generic types.

(Using React 18, TypeScript 4.9)

Background

While implementing UI components, I needed to forward refs, but I ran into a problem where I couldn't handle Props with TypeScript generic types.

For example, consider a case where you want to call Component1 from Component2, where Component1 needs to be passed Props with a generic type like this:

const Component2 = <T,>(props: Component1Props<T>) => (
  <Component1<T> {...props} />
);

Since I also needed to pass a ref, I tried to implement it using React.forwardRef, but I couldn't pass generics to Component1Props.

const Component2 = React.forwardRef<HTMLDivElement, Component1Props>(
  (props, ref) => <Component1 ref={ref} {...props} />,
);

This is because React.forwardRef is defined as follows and cannot be extended:

function forwardRef<T, P = {}>(
  render: ForwardRefRenderFunction<T, P>,
): ForwardRefExoticComponent<PropsWithoutRef<P> & RefAttributes<T>>;

Solutions

There are three ways to handle this issue. I will explain the advantages and disadvantages of each.

1. Casting

First, I will explain how to use casting.

const Component2 = React.forwardRef<HTMLDivElement, Component1Props<any>>(
  (props, ref) => <Component1 ref={ref} {...props} />,
) as <T>(
  p: Component1Props<T> & { ref?: React.Ref<HTMLDivElement> },
) => JSX.Element;
<Component2<number> ref={ref} value={2} />

This approach has the advantage of being easy to understand as a program and easy for users to comprehend.

On the other hand, it is fragile to changes and carries the risk that Type Errors may no longer occur, meaning you don't fully benefit from TypeScript.

2. Not using forwardRef

Next is the method of forwarding the ref through a separate prop instead of using forwardRef. Before forwardRef was implemented, refs were passed as props. This method still works in the current version and is also introduced in the React official documentation.

const Component2 = <T,>(
  props: Component1Props<T> & { customRef?: Ref<HTMLDivElement> },
): JSX.Element => <Component1<T> ref={customRef} {...props} />;
<Component2<number> customRef={ref} value={2} />

This has a significant advantage of being the simplest in terms of code and resistant to changes.

On the other hand, since it requires passing the ref through a customRef prop instead of the widely known ref prop naming in React, it may cause confusion for the users of the component.

3. Overriding the forwardRef type to enable type inference

Next is the method utilizing higher-order function inference.

TypeScript 3.4 introduced higher-order function inference. This allows the generics we want to pass to propagate through the props.

// @types/react.d.ts
import React from "react";

declare module "react" {
  function forwardRef<T, P = {}>(
    render: (props: P, ref: ForwardedRef<T>) => JSX.Element | null,
  ): (props: P & RefAttributes<T>) => JSX.Element | null;
}
interface Component1Props<T> {
  value: T;
}

const Component2Wrapped = <T, P = {}>(
  props: Component1Props<P>,
  ref?: React.ForwardedRef<T>,
): JSX.Element => <Component1 ref={ref} {...props} />;

const Component2 = React.forwardRef(Component2Wrapped);
<Component2 ref={ref} value={2} />

StackOverflow Answered by ford04 (CC BY SA 4.0)

At first glance, it looks the same as the original forwardRef type. However, according to TypeScript architect Anders Hejlsberg, higher-order function type inference only works for pure functions with a single call signature and no other members [1].

The original type was not a pure function, so higher-order function inference was not working. By replacing it with this type, it becomes a pure function, and higher-order function inference starts working.

The merit of this method is that it only requires a fix in one place, keeping the program simple and resistant to changes. Users can also pass the ref using the ref prop, so they don't need to be aware of the internal implementation.

On the other hand, a disadvantage is that type definitions using declare module can cause trouble in long-term maintenance due to issues like loading order or further overrides.

Summary

There is an issue where React.forwardRef cannot handle props with generic types, and I have introduced three ways to address this.

Since we are providing this as a UI component library, we decided to use the "1. Casting" method to minimize confusion for the users.

While it is best to implement components without using refs, if you absolutely must use them, I believe using the "2. Not using forwardRef" method after providing proper documentation is a better approach in terms of maintainability.

脚注
  1. https://github.com/microsoft/TypeScript/issues/30650#issuecomment-486680485 ↩︎

Discussion