React forwardRef 组件中使用泛型

756 阅读3分钟

本章内容将一步一步从一个无类型的列表组件完善成能接收 props 泛型的 forwardRef 组件。

一. 无泛型普通 UI 组件

一个普通 ui 组件,props 也没有类型声明。

// List.tsx
const List = (props) => {
  const { data } = props;
  return (
    <ul>
      {data.map((item, index) => (
        <li key={index}>{item.id}</li>
      ))}
    </ul>
  );
};

二. 给 props 增加类型

现在我们给 data 的增加类型,将其改为 data: { id: number, value: string }[]
常用的有两种方式

  1. 直接给 props 添加类型;
  2. 通过 React.FC 给 props 添加类型;
// List.tsx
interface ListProps {
  data: { id: number, value: string }[];
}
// 直接给 props 添加类型;
const List = (props: ListProps) => {
  const { data } = props;
  return (
    <ul>
      {data.map((item, index) => (
        <li key={index}>{item.id}</li>
      ))}
    </ul>
  );
};
// 通过 React.FC 给 props 添加类型;
const List: React.FC<ListProps> = (props) => {
  const { data } = props;
  return (
    <ul>
      {data.map((item, index) => (
        <li key={index}>{item.id}</li>
      ))}
    </ul>
  );
};

在组件使用的时候,value 的类型只能传为 string,如果将其改为别的类型就会报错

// App.tsx
const App = () => {
  const data = [
    { id: 1, value: "a" },
    { id: 2, value: "b" },
    { id: 3, value: "c" },
  ];
  return <List data={data} />;
};

三. 给 props 增加泛型

在实际使用时发现 value 并不一定都是 string类型,这时候就需要通过泛型来决定 value使用时具体该是什么了。那我们就需要给 props 添加泛型接收,这时候 React.FC 的方式就不再适用了。

// List.tsxÏ
interface ListProps<T> {
  data: { id: number, value: T }[];
}

const List = <T, >(props: ListProps<T>) => {
  const { data } = props;
  return (
    <ul>
      {data.map((item, index) => (
        <li key={index}>{item.id}</li>
      ))}
    </ul>
  );
};

组件使用时,将 number 类型传递给 List组件。

// App.tsx

const App = () => {
  const data = [
    { id: 1, value: 1 },
    { id: 2, value: 2 },
    { id: 3, value: 3 },
  ];
  return <List<number> data={data} />;
};

四. 给 List 组件扩展 forwardRef

在平时使用组件时往往需要暴露一些方法供外部调用,比如我们给 List 组件扩展一个 getData方法供外部使用,就可以通过 forwardRef搭配 useImperativeHandle来实现。

// List.tsx
interface ListProps {
  data: { id: number; value: string }[];
}

interface ListRef {
  getData: () => { id: number; value: string }[];
}

const List = forwardRef<ListRef, ListProps>((props, ref) => {
  const { data } = props;

  useImperativeHandle(ref, () => ({
    getData: () => data,
  }));

  return (
    <ul>
      {data.map((item, index) => (
        <li key={index}>{item.id}</li>
      ))}
    </ul>
  );
});

App.tsx中使用 ref 来调用 getData:

// App.tsx
const App = () => {
  const listRef = useRef<ListRef>(null);
  const data = [
    { id: 1, value: '1' },
    { id: 2, value: '2' },
    { id: 3, value: '3' },
  ];
  
  return (
    <>
      <List ref={listRef} data={data} />
      <button onClick={() => listRef.current?.getData()}>get Data</button>
    </>
  );
};

由于 forwardRef组件不能接收泛型,使用之后会发现泛型丢失了。

五. forwardRef 添加泛型支持

以下部分参考文章: https://fettblog.eu/typescript-react-generic-forward-refs/ ,有需求可以阅读原文。

1. 类型断言

// List.tsx
interface ListProps<T> {
  data: { id: number; value: T }[];
}

interface ListRef<T> {
  getData: () => { id: number; value: T }[];
}

const ListOrigin = <T,>(props: ListProps<T>, ref: ForwardedRef<ListRef<T>>) => {
  const { data } = props;

  const getData = () => data;

  useImperativeHandle(ref, () => ({
    getData,
  }));

  return (
    <ul>
      {data.map((item, index) => (
        <li key={index}>{item.id}</li>
      ))}
    </ul>
  );
};

const List = forwardRef(ListOrigin) as <T>(
  props: ListProps<T> & { ref?: ForwardedRef<ListRef<T>> }
) => ReturnType<typeof ListOrigin>;

使用时 propsref都可以接受泛型了。

App.tsx

const App = () => {
  const listRef = useRef<ListRef<string>>(null);
  const data = [
    { id: 1, value: "1" },
    { id: 2, value: "2" },
    { id: 3, value: "3" },
  ];

  return (
    <>
      <List<string>
        ref={listRef}
        data={data}
      />
      <button onClick={() => listRef.current?.getData()}>get Data</button>
    </>
  );
};

2. 创建自定义 ref / 包装组件

ListOrigin 没有任何变化,将类型断言部分去掉,增加 ListWithRef 组件来接收泛型,增加 myRef 为了避免和原本的 ref 重名。

// List.tsx

const List = forwardRef(ListOrigin);

type ListWithRefProps<T> = ListProps<T> & { myRef: ForwardedRef<ListRef<T>> };

const ListWithRef = <T,>(props: ListWithRefProps<T>) => {
  return <List {...props} />;
};

App.tsx改为使用包装后的组件

// App.tsx

const App = () => {
  const listRef = useRef<ListRef<string>>(null);
  const data = [
    { id: 1, value: "1" },
    { id: 2, value: "2" },
    { id: 3, value: "3" },
  ];

  return (
    <>
      <ListWithRef<string>
        myRef={listRef}
        data={data}
      />
      <button onClick={() => listRef.current?.getData()}>get Data</button>
    </>
  );
};

3. 扩展 forwardRef

我们可以重新声明和重新定义全局模块、命名空间以及接口声明。声明合并(Declaration Merging)是一个强大的工具,而我们将利用它来解决这个问题。
扩展 forwardRef类型

// typings.d.ts
declare module "react" {
  function forwardRef<T, P = {}>(
    render: (props: P, ref: React.Ref<T>) => React.ReactNode | null
  ): (props: P & React.RefAttributes<T>) => React.ReactNode | null;
}

修改 List.tsx组件,ListOrigin没有任何变化,使用 forwardRef将其包裹即可。

// List.tsx
const List = React.forwardRef(ListOrigin);

App.tsx 中使用。

// App.tsx

const App = () => {
  const listRef = useRef<ListRef<string>>(null);
  const data = [
    { id: 1, value: "1" },
    { id: 2, value: "2" },
    { id: 3, value: "3" },
  ];

  return (
    <>
      <List<string>
        ref={listRef}
        data={data}
      />
      <button onClick={() => listRef.current?.getData()}>get Data</button>
    </>
  );
};

参考

https://fettblog.eu/typescript-react-generic-forward-refs/