如果你在React中创建组件库和设计系统,你可能已经对组件内的DOM元素进行了fowarded Refs。
如果你将基本组件或叶子包裹在代理组件中,但想像你习惯的那样使用ref 属性,这就特别有用。
const Button = React.forwardRef((props, ref) => (
<button type="button" {...props} ref={ref}>
{props.children}
</button>
));
// Usage: You can use your proxy just like you use
// a regular button!
const reference = React.createRef();
<Button className="primary" ref={reference}>Hello</Button>
为React.forwardRef 提供类型通常是非常直接的。由@types/react 运送的类型有通用的类型变量,你可以在调用React.forwardRef 时设置。在这种情况下,明确地注解你的类型才是正确的做法!
type ButtonProps = JSX.IntrinsicElements["button"];
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
(props, ref) => (
<button type="button" {...props} ref={ref}>
{props.children}
</button>
)
);
// Usage
const reference = React.createRef<HTMLButtonElement>();
<Button className="primary" ref={reference}>Hello</Button>
到目前为止,一切都很好。但如果你有一个接受通用属性的组件,事情就有点麻烦了。看看这个组件,它产生一个列表项,你可以用button 元素选择每一行。
type ClickableListProps<T> = {
items: T[];
onSelect: (item: T) => void;
};
function ClickableList<T>(props: ClickableListProps<T>) {
return (
<ul>
{props.items.map((item) => (
<li>
<button onClick={() => props.onSelect(item)}>
Choose
</button>
{item}
</li>
))}
</ul>
);
}
// Usage
const items = [1, 2, 3, 4];
<ClickableList items={items}
onSelect={(item) => {
// item is of type number
console.log(item)
} } />
你想要额外的类型安全,所以你可以在你的onSelect 回调中使用类型安全的item 。假设你想为内部的ul 元素创建一个ref ,你该如何进行呢?让我们把ClickableList 组件改成一个内部函数组件,它接收一个ForwardRef ,并把它作为React.forwardRef 函数的参数。
// The original component extended with a `ref`
function ClickableListInner<T>(
props: ClickableListProps<T>,
ref: React.ForwardedRef<HTMLUListElement>
) {
return (
<ul ref={ref}>
{props.items.map((item, i) => (
<li key={i}>
<button onClick={(el) => props.onSelect(item)}>Select</button>
{item}
</li>
))}
</ul>
);
}
// As an argument in `React.forwardRef`
const ClickableList = React.forwardRef(ClickableListInner)
这样可以编译,但有一个缺点。我们不能为ClickableListProps 指定一个通用类型变量。它默认成为unknown 。与any 相比,这很好,但也有点令人讨厌。当我们使用ClickableList ,我们知道要传递哪些项目!我们想让它们被相应的类型化!那么,我们如何才能实现这一点呢?答案是很棘手的......你有几个选择。
选项1:类型断言#
一种选择是做一个类型断言,恢复原来的函数签名。
const ClickableList = React.forwardRef(ClickableListInner) as <T>(
props: ClickableListProps<T> & { ref?: React.ForwardedRef<HTMLUListElement> }
) => ReturnType<typeof ClickableListInner>;
类型断言有点让人皱眉头,因为它们看起来类似于其他编程语言中的类型转换。它们有一点不同,Dan很好地解释了原因。类型断言在TypeScript中有自己的位置。通常情况下,我的方法是让TypeScript从我的JavaScript代码中找出它自己能找出的一切。如果它不能,我就用类型注解来帮忙。在我比TypeScript知道得更多的地方,我会做一个类型断言。
这就是其中的一种情况,我知道我的原始组件接受通用道具
选项2:创建一个自定义的 ref / 封装组件#
虽然ref 是React组件的保留词,但你可以使用你自己的、自定义的props来模仿类似的行为。这样做的效果也不错。
type ClickableListProps<T> = {
items: T[];
onSelect: (item: T) => void;
mRef?: React.Ref<HTMLUListElement> | null;
};
export function ClickableList<T>(
props: ClickableListProps<T>
) {
return (
<ul ref={props.mRef}>
{props.items.map((item, i) => (
<li key={i}>
<button onClick={(el) => props.onSelect(item)}>Select</button>
{item}
</li>
))}
</ul>
);
}
不过,你引入了一个新的API。顺便说一下,也有可能使用一个包装组件,它允许你在内部组件中使用forwardRef,并向外部暴露一个自定义的ref属性。这个方法在网上流传,我只是觉得与前面的解决方案相比,没有什么明显的好处
function ClickableListInner<T>(
props: ClickableListProps<T>,
ref: React.ForwardedRef<HTMLUListElement>
) {
return (
<ul ref={ref}>
{props.items.map((item, i) => (
<li key={i}>
<button onClick={(el) => props.onSelect(item)}>Select</button>
{item}
</li>
))}
</ul>
);
}
const ClickableListWithRef = forwardRef(ClickableListInner);
type ClickableListWithRefProps<T> = ClickableListProps<T> & {
mRef?: React.Ref<HTMLUListElement>;
};
export function ClickableList<T>({
mRef,
...props
}: ClickableListWithRefProps<T>) {
return <ClickableListWithRef ref={mRef} {...props} />;
}
如果你想实现的唯一目的是传递引用,那么这两种解决方案都是有效的。如果你想有一个一致的API,你可以寻找其他的东西。
选项3:增强forwardRef#
这实际上是我最喜欢的解决方案。
TypeScript有一个功能叫 高阶函数类型推理的功能,它允许将自由类型的参数传播到外层函数上。
这听起来很像我们一开始想用forwardRef ,但由于某些原因,它在我们目前的类型中不起作用。原因是高阶函数类型推理只对普通函数类型起作用。forwardRef 内的函数声明也为defaultProps ,等等添加了属性。类组件时代的遗留问题。反正你可能不想使用的东西。
所以,如果没有这些额外的属性,应该可以使用高阶函数类型推理
而且,嘿!我们正在使用TypeScript,我们有可能自己重新声明和重新定义全局模块、命名空间和接口的声明。声明合并是一个强大的工具,我们要利用它。
// Redecalare forwardRef
declare module "react" {
function forwardRef<T, P = {}>(
render: (props: P, ref: React.Ref<T>) => React.ReactElement | null
): (props: P & React.RefAttributes<T>) => React.ReactElement | null;
}
// Just write your components like you're used to!
type ClickableListProps<T> = {
items: T[];
onSelect: (item: T) => void;
};
function ClickableListInner<T>(
props: ClickableListProps<T>,
ref: React.ForwardedRef<HTMLUListElement>
) {
return (
<ul ref={ref}>
{props.items.map((item, i) => (
<li key={i}>
<button onClick={(el) => props.onSelect(item)}>Select</button>
{item}
</li>
))}
</ul>
);
}
export const ClickableList = React.forwardRef(ClickableListInner);
这个解决方案的好处是,你可以再次编写常规的JavaScript,并且完全在类型层面上工作。另外,重新声明是模块范围的。不会干扰来自其他模块的任何forwardRef 。