🍣
react childrenへのprops付与
概要
コンポーネントを作るときにchildrenに対してprops要素をつけたい時があるかと思います。
以下のようにchildrenの先頭に例えばdate-state
のようなpropsを渡したいんよなという時です。あるいは親コンポーネントのdivをコンポーネントに付けたくないけど仕方なしに使っている場合があるかと思います。
<div data-state='open'>
{children}
</div>
最終的に作るコンポーネント
以下のようなコンポーネントを作ります。
<Primitive.div asChild data-state={'hoge'} className={'flex justify-center'}>
<div className='text-red-100'>
<div className='mr-5'>
aaa
</div>
<div>
bbb
</div>
</div>
</Primitive.div>
実際のDOMは以下のようになります。Primitive.divのdivのelementが消え、そこについていたpropsが子コンポーネントのdivに付与されるようになります。
<div class="flex justify-center text-red-100" data-state="hoge">
<div class="mr-5">aaa</div>
<div>bbb</div>
</div>
設計
今回参考になったのは radix-uiというHeadless uiです。そのコードを必要最低限だけ切り抜いて作ってみました。登場人物は2つのコンポーネントです。
- Primitive
- div,a,タグのようなelementを持つコンポーネント。propsに通常のelementのpropsに加えて、
asChild
を持ちます。これがある場合は子コンポーネントに対してpropsを継承させます。
- div,a,タグのようなelementを持つコンポーネント。propsに通常のelementのpropsに加えて、
- Slot
- 親コンポーネントのpropsを子コンポーネントに渡す役目のコンポーネント
Primitive
コードは以下の通りです。雰囲気でコードを読んでもらえたらと思いますが、asChildがfalseの時はforwardrefを持っているelementができるというだけです。
import { Slot } from '@/components/Headless/Slot/Slot'
const NODES = [
'a',
'button',
'div',
'form',
'h2',
'h3',
'img',
'input',
'label',
'li',
'nav',
'ol',
'p',
'span',
'svg',
'ul',
] as const
type PrimitivePropsWithRef<E extends React.ElementType> = React.ComponentPropsWithRef<E> & {
asChild?: boolean
}
// forwardRefをしたコンポーネントの型
type PrimitiveForwardRefComponent<E extends React.ElementType> = React.ForwardRefExoticComponent<PrimitivePropsWithRef<E>>
type Primitives = { [E in typeof NODES[number]]: PrimitiveForwardRefComponent<E> }
export const Primitive = NODES.reduce((primitive, node) => {
const Node = React.forwardRef((props: PrimitivePropsWithRef<typeof node>, ref) => {
const { asChild, ...primitiveProps } = props
const Comp: any = asChild ? Slot : node
return <Comp {...primitiveProps} ref={ref}/>
})
Node.displayName = `Primitive.${node}`
return { ...primitive, [node]: Node }
}, {} as Primitives)
// 使い方
// 通常のdivエレメントと変わりがない
<Primitive.div> </Primitive.div>
// asChildがある場合は子コンポーネントの`<div></div>`に対して`data-state`が付与される
<Primitive.div asChild data-state='open'><div></div></Primitive.div>
Slot
コードは以下の通りです。
type SlotProps = React.HTMLAttributes<HTMLElement> & {
children?: React.ReactNode
}
export const Slot = React.forwardRef<HTMLElement, SlotProps>(
(props, forwardedRef) => {
const { children, ...slotProps } = props
// childrenがReactElementである場合
if(React.isValidElement(children)) {
return React.cloneElement<any>(children, {
//Primitiveのpropsとchildrenのpropsをがっちゃんこします。
...mergeProps(slotProps, children.props),
// Primitiveのrefとchildrenに対するrefがある場合、がっちゃんこします。
// composeRefsはがっちゃんこさせる関数です。
ref: forwardedRef ? composeRefs(forwardedRef, (children as any).ref) : (children as any).ref,
})
}
return null
})
Slot.displayName = 'Slot'
// がっちゃんこ
const mergeProps = (slotProps: AnyProps, childProps: AnyProps) => {
const overrideProps = { ...childProps }
for (const propName in childProps) {
const slotPropValue = slotProps[propName]
const childPropValue = childProps[propName]
//on要素についてはPrimitiveのonと子コンポーネントのonの両方を実行させる
const isHandler = /^on[A-Z]/.test(propName)
if(isHandler) {
if (slotPropValue && childPropValue) {
// slot及び子コンポーネントにhandlerがある場合は両方実行
overrideProps[propName] = (...args: unknown[]) => {
childPropValue(...args)
slotPropValue(...args)
}
} else if (slotPropValue) {
overrideProps[propName] = slotPropValue
}
}
// デザイン系は両方を付与させる
else if (propName === 'style') {
overrideProps[propName] = { ...slotPropValue, ...childPropValue }
} else if (propName === 'className') {
overrideProps[propName] = [slotPropValue, childPropValue].filter(Boolean).join(' ')
}
}
return { ...slotProps, ...overrideProps }
}
type PossibleRef<T> = React.Ref<T> | undefined
const setRef = <T,>(ref: PossibleRef<T>, node: T) => {
if (typeof ref === 'function') {
ref(node)
} else if (ref !== null && ref !== undefined) {
(ref as React.MutableRefObject<T>).current = node
}
}
// 複数のrefがある場合、そのrefに対してそのelementをいれる
export const useComposedRefs = <T,>(...refs: PossibleRef<T>[]) => {
return React.useCallback((node: T) => refs.forEach((ref) => setRef(ref, node)), refs)
}
最終的な使い方は最初のページにある通り使うことができます。使い道はいろいろできますが、基本的にはdivのような要素を必ずしも使いたくないんだよな、というときに重宝します。
まとめ
子コンポーネントに対してpropsを付与できるものを作成しました。refのあたりは若干テクニカルですが、慣れるとこのコンポーネントは使いやすいかと思います。
Discussion