本章内容将一步一步从一个无类型的列表组件完善成能接收 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 }[]
常用的有两种方式
- 直接给 props 添加类型;
- 通过 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>;
使用时 props和ref都可以接受泛型了。
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>
</>
);
};