React多态性允许你动态的修改渲染的 HTML,最常见的场景为 LinkButton

同一个按钮,你可以选择其使用 button 标签还是 a 标签,实现这种的方法有两种: asasChild

as 模式

export function Button(props: any) {
  const Tag = props.as || "button";
  return (
    <Tag className="button" {...props} />
  );
}
 
<Button>button 按钮</Button>
<Button as="a">a 按钮</Button>

as 模式是利用了 JSX 中的一个非常特殊的属性:如果一个元素的首字母大写,React 将把它解释为元素类型。然后 React 将根据变量的值渲染元素类型

TypeScript 中使用

import React, { ElementType, ComponentPropsWithoutRef } from 'react';
 
type ButtonProps<T extends ElementType> = {
	as?: T
} & ComponentPropsWithoutRef<T>;
 
export function Button<T extends ElementType = 'button'>(props: ButtonProps<T>) {
	const Component = props.as || "button";
	return <Component className="button" {...props}/>
}

asChild

asChild 提供了一种替代的多态性方法,使用子元素来定义父元素的元素类型*

// as 模式
<Button as="a">link button</Button>
 
// asChild 模式
<Button asChild>
	<a>link button</a>
</Button>

Radix 组件库中能够经常看到这种实现。

asChild 模式的实现可以使用 React.cloneElement API 来实现

export function Button({ asChild, children, ...props }) {
	return asChild ? React.cloneElement(children, 
		{ className: 'button', ...props }
	) : <button className="button">{children}</button>
}

Radix 中封装了一个 Slot 组件来实现这一点Slot – Radix Primitives

使用 asChild 模式的好处

  1. **明确的角色分配:**通过 asChild,处理样式和功能的元素之间的区别变得更加清晰。按钮控制样式,而标签负责导航方面。
  2. 灵活性:提供了灵活性,可以更改子元素,例如从一个 <a> 标签切换到一个 <span>标签,而不会丢失 Button 提供的样式
  3. 避免复杂性: 这种方法避免了直接支持多个元素类型可能带来的复杂性,采用 Button 方法,很容易需要支持过多的版本,使得代码难以阅读。