Reactでコンポーネントを使う時に、表示される要素を切り替えたい。
そんな時に asChild パターンが利用できる。
asChild パターンとは何か
asChild
が true
のときにコンポーネントの子要素としてカスタムコンポーネントを渡すパターン。
asChild
false
のときデフォルトとして指定したコンポーネントをレンダリングする
asChild
true
のとき渡した子要素をレンダリングする
Radix UIでの採用が有名らしい。
asChild パターンの実装
<a>
タグや <Link>
タグを子要素として描画できる Buttonコンポーネントを作ってみる。
型定義
Propsの型を定義する。
1
2
3
4
5
6
7
8
9
|
type AsChildProps<DefaultElementProps> =
| ({ asChild?: false } & DefaultElementProps)
| { asChild: true; children: React.ReactNode }
type ButtonProps = AsChildProps<
React.ButtonHTMLAttributes<HTMLButtonElement>
> & {
style?: React.CSSProperties
}
|
AsChildProps
型はジェネリクスとして DefaultElementProps
を受け取る。
asChild
が true
であれば、 React.Node
型のプロパティ children
をもち、そうでない場合は引数として渡された DefaultElementProps
型のプロパティをもつ。
ButtonProps
において、ジェネリクスに React.ButtonHTMLAttributes<HTMLButtonElement>
を渡しているので、 asChild
が false
の場合は、HTMLのButton要素と同等のpropsが期待されるようになる。
また、交差型(Intersection Types)で optionalな style
をマージしている(つまり、期待されるpropsは「HTMLのButton要素」+ style
となる)
Slot
Slotコンポーネントを定義する。Slotではpropsを直接の子に対してマージする。
子は1つだけ設定できる。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
function Slot({
children,
...props
}: React.HTMLAttributes<HTMLElement> & {
children?: React.ReactNode
}) {
// propsを統合したReactElementを生成
if (React.isValidElement(children)) {
return React.cloneElement(children, {
...props,
...children.props,
})
}
// 複数のchildrenは許可しない
if (React.Children.count(children) > 1) {
React.Children.only(null)
}
return null
}
|
JSX
型定義とSlotを利用し、Buttonコンポーネントを記述する。
1
2
3
4
5
6
7
|
export function Button({ asChild, ...props }: ButtonProps) {
const Component = asChild ? Slot : "button"
return (
<Component {...props} />
)
}
|
asChild
の値に応じて、Slotをレンダリングするか、通常のButton要素をレンダリングするか切り替える。
受け取ったpropsを全て流し込む。
呼び出し側
以下のパターンで呼び出してみた。
- ただのButtonとして扱う
- aタグを渡す
- asChildでaタグを渡す
- RemixのLinkコンポーネントを渡す
- asChildでRemixのLinkコンポーネントを渡す
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
export default function AschildSample() {
return (
<div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.8", margin: '100px' }}>
<h1>aschild Sample</h1>
<h2>ただのButtonとして扱う</h2>
<Button onClick={() => {alert('Clicked!!!')}}>
Click Me!
</Button>
<h2>aタグを渡す</h2>
<Button style={{color: 'green'}}>
<a href="https://google.com">Link to Google</a>
</Button>
<h2>asChildでaタグを渡す</h2>
<Button style={{color: 'green'}} asChild>
<a href="https://google.com">Link to Google</a>
</Button>
<h2>RemixのLinkコンポーネントを渡す</h2>
<Button style={{color: 'orange'}}>
<Link to='/sample'>Link to Sample Page</Link>
</Button>
<h2>asChildでRemixのLinkコンポーネントを渡す</h2>
<Button style={{color: 'orange'}} asChild>
<Link to='/sample'>Link to Sample Page</Link>
</Button>
</div>
);
}
|
ただのButtonとして扱う場合、onClick
でクリック時の挙動を渡すことができる。
aタグ・Linkコンポーネント共に問題なく渡すことができた。
asChild
を設定していないときは、buttonタグとしてレンダリングされ、設定した時はaタグとしてレンダリングされた。
参考