Reactコンポジションパターン
React componentの利用側が意図を読み取れ、機能追加でboolean propsが組み合わせ爆発を起こさないAPIを設計する。 小さく安定したcomponentへ、必要以上の抽象化を導入しない。
適用を判断する
次の兆候がある場合に使う。
- 一つのcomponentに表示や動作を切り替えるboolean propsが増えている。
- 特定のprop組み合わせだけが有効で、無効な状態を型で防げない。
- 親子や兄弟componentが同じstateとactionを共有する。
- 利用側が内部stateの持ち方へ依存している。
- design systemのcomponent APIを複数の利用箇所へ公開する。
単一用途で分岐が少ないcomponentは、そのまま保つ。
boolean propsを増やさない
isCompact、hasFooter、isDismissibleのようなflagを追加する前に、利用者が選んでいる概念を名前にする。 意味の異なる構成は、明示的なvariant componentまたはchildren compositionへ分ける。
<CheckoutDialog />
<UpgradeDialog />
これを、多数のflagを持つ一つのDialogより優先する。 ただし、disabledやrequiredのようにplatform上で意味が安定した状態まで機械的にvariantへ分けない。
compound componentを使う
複数の部品が同じ状態を共有し、利用側が構成順を選ぶ必要がある場合にcompound componentを使う。
<Select.Root value={value} onValueChange={setValue}>
<Select.Trigger />
<Select.Content>
<Select.Item value="a">A</Select.Item>
</Select.Content>
</Select.Root>
RootまたはProviderが状態とactionを所有する。- 子componentは狭い責務を持つ。
- DOM構造とkeyboard behaviorの不変条件は利用側へ丸投げしない。
- contextが存在しない誤用は、早く理解できるerrorにする。
state interfaceを分離する
Providerが公開するcontextを次の三つへ分けて考える。
- state:現在値
- actions:更新操作
- meta:識別子、validation、statusなどの補助情報
利用側がuseState、reducer、server stateのどれを使うか知らなくてよいinterfaceにする。 制御componentと非制御componentの違いもProvider境界で吸収する。
stateを必要な位置へ持ち上げる
兄弟componentが同じ状態を読む場合、共通祖先のProviderへ移す。 ただし、画面全体のglobal stateへ移す前に、featureまたはcomponent tree内で閉じられるか確認する。 stateの共有範囲を利用範囲より広くしない。
childrenを優先する
静的な構成を渡すだけなら、renderHeaderやrenderFooterよりchildrenまたはnamed subcomponentを使う。 render propは、利用側へruntime stateを渡して描画判断を委ねる必要がある場合に残す。
React versionを確認する
- React 19では
refを通常のpropとして扱えるため、新規APIでforwardRefが必要か確認する。 - React 19固有APIをReact 18へ持ち込まない。
- projectのReact version、compiler、framework制約を確認してから変換する。
完了条件
- component名と構造から利用意図が分かる。
- 無効なprop組み合わせを作りにくい。
- stateの保存方法を利用側が知る必要がない。
- keyboard、focus、semanticsの責務がcomponent側に残る。
- 既存の利用箇所を段階的に移行できる。
出典
- [vercel-labs/agent-skills: composition-patterns](https://github.com/vercel-labs/agent-skills/tree/main/skills/composition-patterns)
- MIT License
- 本文は公式skillを日本語の設計判断へ再構成した派生文書である。