高阶组件(Higher-Order Component, HOC) 是 React 中的一种高级技巧。但是在 TypeScript 中写高阶组件可能会遇到一点障碍。
高阶组件
如果你忘了或还未了解过高阶组件,下面是个最简单的例子:
const withBorder = (Component, color) => {
return (props) => {
return (
<div style={{ border: `solid 1px ${color}` }}>
<Component {...props} />
</div>
);
};
};
React 中约定高阶组件名应当以 'with' 开头。
这是一个能给任何组件加上边框的高阶函数,它的返回值也是个组件。比如我们给下面的 Welcome
加上红色边框,则可以:
const Welcome = ({ name }) => {
return <div>Hello {name}!</div>;
};
// 带有红色边框的 Welcome
const RedBorderedWelcome = withBorder(Welcome, 'red');
const View = () => <RedBorderedWelcome name="Talaxy" />;
TypeScript 中的高阶组件
可以在 TypeScript 中这么写上面的 withBorder
高阶组件:
// 如果在 function 声明中定义范型,则可以不需要 `extends {}`
const withBorder = <T extends {}>(
Component: ComponentType<T>,
color: string,
) => {
return (props: T) => {
return (
<div style={{ border: `solid 1px ${color}` }}>
<Component {...props} />
</div>
);
};
};
我们需要定一个范型
T
(且限定为空对象的扩展),指代传入组件的属性类型;在返回的组件中,需要标注上 props 的类型为
T
。
稍微...复杂一点?
其实写这篇之初,是我在实际项目中给页面加 AB 测试遇到了困难。
项目中的 AB 测试是从 API 请求的,其实在页面里就可以做这个请求,但是考虑到别的一些页面也会用的 AB 测试,因此我想到把 AB 测试写在高阶组件里:
function withABTest<T>(
Component: ComponentType<T>,
category: ABTestCategory, // 枚举类型,指定实际需求里 AB 测试的类别
) {
return (props: T) => {
const [testCase, setTestCase] = useState<string>();
useEffect(() => {
fetchABTest(category).then(setTestCase);
}, []);
if (!testCase) return null;
return <Component {...props} testCase={testCase} />;
};
}
const Page = (props: { testCase: string }) => {
return <div>我是{props.testCase}测试集。</div>;
};
const PageWithABTest = withABTest(Page, ABTestCategory.SomeCategory);
第一眼看上去可能感觉这个高阶组件没什么问题。但实际则是错误地使用了 TypeScript 中的范型:
Component
需要testCase
参数,但未在声明里进行限定;高阶组件中的
props
应当比Component
少一个testCase
,而不是共同指向了T
。
因此,我们需要这样定义类型:
function withABTest<P>(
Component: ComponentType<P & { testCase: string }>,
category: ABTestCategory,
) {
return (props: P) => {
const [testCase, setTestCase] = useState<string>();
useEffect(() => {
fetchABTest(category).then(setTestCase);
}, []);
if (!testCase) return null;
return <Component {...props} testCase={testCase} />;
};
}