react-composition-2026
2025/2026年向けのモダンなReactコンポジションパターンを習得できるスキルです。コンポーネントAPIの設計、共有UIライブラリの構築、またはpropsが肥大化したコンポーネントのリファクタリングを行う際に活用してください。
description の原文を見る
Teaches modern React composition patterns for 2025/2026. Use when designing component APIs, building shared UI libraries, or refactoring prop-heavy components.
SKILL.md 本文
モダン React コンポジションパターン
目次
スケーラブルで柔軟性と保守性に優れた React コンポーネントを構築するためのコンポジションパターン。これらのパターンは、boolean プロップの乱増、硬直したコンポーネント API、複雑に絡み合った状態管理を、組み合わせ可能で明示的な設計に置き換えます。
使用する時期
以下のような場合にこれらのパターンを参考にしてください:
- コンポーネントが 3〜4 個以上の boolean プロップでその動作を制御している
- 再利用可能な UI コンポーネントやコンポーネントライブラリを構築している
- 拡張しづらいコンポーネントをリファクタリングしている
- 他のチームが利用するコンポーネント API を設計している
- コンポーネントアーキテクチャの柔軟性と保守性をレビューしている
説明
- コンポーネント設計、コード生成、レビュー中にこれらのパターンを適用します。boolean プロップの蓄積または硬直したコンポーネント API を見つけた場合は、適切なコンポジションパターンを提案してください。
詳細
概要
コアの原則: 設定より構成(composition over configuration)。すべてのバリエーションに対応するために boolean プロップと条件分岐を追加する代わりに、より小さく焦点を絞ったコンポーネントを一緒に組み合わせます。これにより、人間にとっても AI エージェントにとっても、コンポーネントが理解しやすく、テストしやすく、拡張しやすくなります。
1. Boolean プロップをコンポジションで置き換える
影響: HIGH — 組み合わせの爆発を防ぎ、意図を明示的にします。
Boolean プロップは複雑性を増加させます: 4 つの boolean = 16 の可能な状態で、ほとんどがテストされていません。それらを組み合わせ可能な children で置き換えます。
避けるべき — boolean プロップの蓄積:
<Card
showHeader
showFooter
collapsible
bordered
withShadow
headerAction="close"
size="large"
/>
推奨 — 明示的なコンポジション:
<Card variant="bordered" shadow="md">
<Card.Header>
<h3>Title</h3>
<Card.CloseButton />
</Card.Header>
<Card.Body collapsible>
<p>Content here</p>
</Card.Body>
<Card.Footer>
<Button>Save</Button>
</Card.Footer>
</Card>
各要素が明示的で、テスト可能で、独立してオプション化できます。
2. Context を使用したコンパウンドコンポーネントの構築
影響: HIGH — プロップドリリングなしで暗黙の状態を共有します。
コンパウンドコンポーネントは一緒に機能するコンポーネントのグループで、プロップではなく context を通じて状態を共有します。親が状態を所有し、子がそれを消費します。
避けるべき — 親がプロップを通じてすべてを管理:
<Select
options={options}
value={value}
onChange={onChange}
renderOption={(opt) => <span>{opt.icon} {opt.label}</span>}
renderSelected={(opt) => <b>{opt.label}</b>}
placeholder="Choose..."
clearable
searchable
maxHeight={300}
/>
推奨 — コンパウンドコンポーネント:
const SelectContext = createContext<SelectState | null>(null)
function Select({ children, value, onChange }: SelectProps) {
const [open, setOpen] = useState(false)
const ctx = useMemo(() => ({ value, onChange, open, setOpen }), [value, onChange, open])
return (
<SelectContext.Provider value={ctx}>
<div className="select-root">{children}</div>
</SelectContext.Provider>
)
}
function Trigger({ children }: { children: React.ReactNode }) {
const { open, setOpen } = useSelectContext()
return <button onClick={() => setOpen(!open)}>{children}</button>
}
function Options({ children }: { children: React.ReactNode }) {
const { open } = useSelectContext()
if (!open) return null
return <ul role="listbox">{children}</ul>
}
function Option({ value, children }: OptionProps) {
const { value: selected, onChange, setOpen } = useSelectContext()
return (
<li
role="option"
aria-selected={value === selected}
onClick={() => { onChange(value); setOpen(false) }}
>
{children}
</li>
)
}
Select.Trigger = Trigger
Select.Options = Options
Select.Option = Option
使用例:
<Select value={color} onChange={setColor}>
<Select.Trigger>Pick a color</Select.Trigger>
<Select.Options>
<Select.Option value="red">Red</Select.Option>
<Select.Option value="blue">Blue</Select.Option>
</Select.Options>
</Select>
3. 明示的なバリアントコンポーネントの作成
影響: MEDIUM — 各モードが明確で焦点を絞ったコンポーネントになります。
コンポーネントが異なる「モード」を持つ場合(dialog vs drawer、inline vs modal、card vs list-item)、プロップでの切り替えではなく、明示的なバリアントコンポーネントを作成します。
避けるべき — モードプロップを持つ単一のコンポーネント:
function MediaDisplay({ type, src, title, showControls, autoPlay, loop }: Props) {
if (type === 'video') {
return <video src={src} controls={showControls} autoPlay={autoPlay} loop={loop} />
}
if (type === 'audio') {
return <audio src={src} controls={showControls} />
}
return <img src={src} alt={title} />
}
推奨 — 明示的なバリアント:
function VideoPlayer({ src, controls, autoPlay, loop }: VideoProps) {
return <video src={src} controls={controls} autoPlay={autoPlay} loop={loop} />
}
function AudioPlayer({ src, controls }: AudioProps) {
return <audio src={src} controls={controls} />
}
function Image({ src, alt }: ImageProps) {
return <img src={src} alt={alt} />
}
各バリアントは必要なプロップのみを持ち、不可能な状態がなく、未使用のプロップがありません。
4. Render Props より Children をコンポジションに使用する
影響: MEDIUM — よりシンプルな API、より良い可読性。
Render props(renderHeader、renderItem)は Hooks 以前には必須でしたが、今日は children がほとんどのケースでより清潔なコンポジションを提供します。
避けるべき — render props の乱増:
<DataTable
data={users}
renderHeader={() => <h2>Users</h2>}
renderRow={(user) => <UserRow user={user} />}
renderEmpty={() => <EmptyState />}
renderFooter={() => <Pagination />}
/>
推奨 — children コンポジション:
<DataTable data={users}>
<DataTable.Header>
<h2>Users</h2>
</DataTable.Header>
<DataTable.Body>
{users.map(user => <UserRow key={user.id} user={user} />)}
</DataTable.Body>
<DataTable.Empty>
<EmptyState />
</DataTable.Empty>
<DataTable.Footer>
<Pagination />
</DataTable.Footer>
</DataTable>
Render props は親がレンダラーにデータを提供する必要があるケース(例:仮想化されたリストアイテム)に限定してください。
5. 状態実装を UI から分離する
影響: MEDIUM — コンポーネントを変更せずに状態管理を交換できます。
状態形状(value、actions、metadata)の汎用インターフェースを定義し、プロバイダーがそれを実装させます。コンポーネントは実装ではなく、インターフェースを消費します。
インターフェースを定義:
interface CounterState {
count: number
increment: () => void
decrement: () => void
isLoading: boolean
}
const CounterContext = createContext<CounterState | null>(null)
function useCounter() {
const ctx = useContext(CounterContext)
if (!ctx) throw new Error('useCounter must be used within a CounterProvider')
return ctx
}
ローカル状態で実装:
function LocalCounterProvider({ children }: { children: React.ReactNode }) {
const [count, setCount] = useState(0)
const value = useMemo(() => ({
count,
increment: () => setCount(c => c + 1),
decrement: () => setCount(c => c - 1),
isLoading: false,
}), [count])
return <CounterContext.Provider value={value}>{children}</CounterContext.Provider>
}
コンシューマーを変更せずに API バックアップ状態に切り替え:
function ApiCounterProvider({ children }: { children: React.ReactNode }) {
const { data, mutate } = useSWR('/api/counter', fetcher)
const value = useMemo(() => ({
count: data?.count ?? 0,
increment: () => mutate(patch('/api/counter', { delta: 1 })),
decrement: () => mutate(patch('/api/counter', { delta: -1 })),
isLoading: !data,
}), [data, mutate])
return <CounterContext.Provider value={value}>{children}</CounterContext.Provider>
}
useCounter() のコンシューマーは変わりません。
6. 状態をプロバイダーコンポーネントにリフトアップする
影響: MEDIUM — プロップスレッディングなしでシブリング通信を可能にします。
2 つのシブリングコンポーネントが共有状態を必要とする場合、親を通じたコールバックスレッディングではなく、プロバイダーに状態をリフトアップします。
避けるべき — 親がシブリングに状態をスレッド化:
function Page() {
const [selected, setSelected] = useState<string | null>(null)
return (
<div>
<Sidebar selected={selected} onSelect={setSelected} />
<Detail selected={selected} />
</div>
)
}
推奨 — プロバイダーが共有状態を管理:
function SelectionProvider({ children }: { children: React.ReactNode }) {
const [selected, setSelected] = useState<string | null>(null)
return (
<SelectionContext.Provider value={{ selected, setSelected }}>
{children}
</SelectionContext.Provider>
)
}
function Page() {
return (
<SelectionProvider>
<Sidebar />
<Detail />
</SelectionProvider>
)
}
Sidebar と Detail の両方が直接 useSelection() を消費します。
7. 柔軟な要素のためにポリモルフィック as プロップを使用する
影響: MEDIUM — 1 つのコンポーネント、任意の基礎要素またはコンポーネント。
as プロップパターンは、コンシューマーがレンダリングされた要素を制御できるようにしながら、コンポーネントのスタイルと動作を保持します。
type BoxProps<C extends React.ElementType = 'div'> = {
as?: C
children: React.ReactNode
} & Omit<React.ComponentPropsWithoutRef<C>, 'as' | 'children'>
function Box<C extends React.ElementType = 'div'>({
as,
children,
...props
}: BoxProps<C>) {
const Component = as || 'div'
return <Component {...props}>{children}</Component>
}
使用例:
<Box>Default div</Box>
<Box as="section">A section</Box>
<Box as="a" href="/about">A link</Box>
<Box as={Link} to="/about">Router link</Box>
8. React 19: forwardRef を削除し、ref をプロップとして使用する
影響: MEDIUM — よりシンプルなコンポーネント定義。
React 19 は ref を通常のプロップとして渡します。もう forwardRef ラッパーは不要です。
React 18(非推奨パターン):
const Input = forwardRef<HTMLInputElement, InputProps>(function Input(props, ref) {
return <input ref={ref} {...props} />
})
React 19:
function Input({ ref, ...props }: InputProps & { ref?: React.Ref<HTMLInputElement> }) {
return <input ref={ref} {...props} />
}
同様に、use() は promise または context の両方を読み取ることができ、条件付きで呼び出すことができます:
import { use } from 'react'
function Panel({ themePromise }: { themePromise: Promise<Theme> }) {
const theme = use(themePromise) // promise をアンラップ
const user = use(UserContext) // 条件付き context 読み取り
return <div className={theme.bg}>{user.name}</div>
}
9. レイアウトコンポーネント向けスロットパターン
影響: MEDIUM — render props なしの名前付き挿入ポイント。
複数のコンテンツエリアを持つレイアウトコンポーネントの場合、子型検出または名前付きサブコンポーネント基のスロットパターンを使用します。
function AppLayout({ children }: { children: React.ReactNode }) {
const slots = React.Children.toArray(children)
const header = slots.find(
(child): child is React.ReactElement => React.isValidElement(child) && child.type === AppLayout.Header
)
const content = slots.filter(
(child) => !React.isValidElement(child) || child.type !== AppLayout.Header
)
return (
<div className="app-layout">
<header>{header}</header>
<main>{content}</main>
</div>
)
}
AppLayout.Header = function Header({ children }: { children: React.ReactNode }) {
return <>{children}</>
}
使用例:
<AppLayout>
<AppLayout.Header>
<Logo />
<Nav />
</AppLayout.Header>
<Dashboard />
</AppLayout>
10. 最大の柔軟性のためのヘッドレスコンポーネント
影響: HIGH — レンダリングに関する意見なしのロジック。
ヘッドレスコンポーネントは、マークアップなしで動作(状態、キーボード処理、ARIA 属性)を提供します。コンシューマーがレンダリングを提供します。
function useToggle(initial = false) {
const [on, setOn] = useState(initial)
const toggle = useCallback(() => setOn(o => !o), [])
const buttonProps = {
'aria-pressed': on,
onClick: toggle,
role: 'switch' as const,
}
return { on, toggle, buttonProps }
}
使用例 — コンシューマーがすべてのレンダリングを制御:
function DarkModeSwitch() {
const { on, buttonProps } = useToggle(false)
return (
<button {...buttonProps} className={on ? 'dark' : 'light'}>
{on ? 'Dark' : 'Light'} Mode
</button>
)
}
Radix UI、Headless UI、React Aria などのライブラリはこのパターンに従います。デザイン柔軟性が必要な場合は、完全スタイルコンポーネントライブラリより、これを優先してください。
出典
パターンは patterns.dev より — より広い React コミュニティ向けのコンポジション指針。
ライセンス: MIT(寛容ライセンスのため全文を引用しています) · 原本リポジトリ
詳細情報
- 作者
- patternsdev
- リポジトリ
- patternsdev/skills
- ライセンス
- MIT
- 最終更新
- 不明
Source: https://github.com/patternsdev/skills / ライセンス: MIT
関連スキル
doubt-driven-development
重要な判断はすべて、本番環境への展開前に新しい視点から対抗的レビューを実施します。速度より正確性が重要な場合、不慣れなコードを扱う場合、本番環境・セキュリティに関わるロジック・取り消し不可の操作など影響度が高い場合、または後でバグを修正するよりも今検証する方が効率的な場合に活用してください。
apprun-skills
TypeScriptを使用したAppRunアプリケーションのMVU設計に関する総合的なガイダンスが得られます。コンポーネントパターン、イベントハンドリング、状態管理(非同期ジェネレータを含む)、パラメータと保護機能を備えたルーティング・ナビゲーション、vistestを使用したテストに対応しています。AppRunコンポーネントの設計・レビュー、ルートの配線、状態フローの管理、AppRunテストの作成時に活用してください。
desloppify
コードベースのヘルスチェックと技術負債の追跡ツールです。コード品質、技術負債、デッドコード、大規模ファイル、ゴッドクラス、重複関数、コードスメル、命名規則の問題、インポートサイクル、結合度の問題についてユーザーが質問した場合に使用してください。また、ヘルススコアの確認、次の改善項目の提案、クリーンアップ計画の作成をリクエストされた際にも対応します。29言語に対応しています。
debugging-and-error-recovery
テストが失敗したり、ビルドが壊れたり、動作が期待と異なったり、予期しないエラーが発生したりした場合に、体系的な根本原因デバッグをガイドします。推測ではなく、根本原因を見つけて修正するための体系的なアプローチが必要な場合に使用してください。
test-driven-development
テスト駆動開発により実装を進めます。ロジックの実装、バグの修正、動作の変更など、あらゆる場面で活用できます。コードが正常に動作することを証明する必要がある場合、バグ報告を受けた場合、既存機能を修正する予定がある場合に使用してください。
incremental-implementation
変更を段階的に実施します。複数のファイルに影響する機能や変更を実装する場合に使用してください。大量のコードを一度に書こうとしている場合や、タスクが一度では完結できないほど大きい場合に活用します。