react-composition について
React開発者向けのコンポーネント設計スキルです。設定用のプロパティを増やすより、小さな要素の組み合わせを重視します。複合コンポーネント、コンテキスト設計、React 19 APIを扱います。
主な機能
- 01ブール型プロパティより合成を優先
- 02明示的なコンポーネントバリアント
- 03childrenによるコンテンツ構成
- 04状態と操作を分けたコンテキスト
- 05React 19のrefとuse API
活用シーン
- 01ブール型プロパティの多いコンポーネントの再設計
- 02異なるモードを持つコンポーネントAPIの設計
- 03兄弟コンポーネント間での状態共有
- 04React 19 APIを使うコードへの更新
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ s-hiraoku/ synapse-a2a --skill react-compositionインストール後、Claude Code で/react-compositionを入力して読み込まれたか確認します。
詳細
React のコンポジション
設定ではなくコンポジションによって、柔軟なコンポーネント API を構築します。
基本原則
設定よりコンポジション。 コンポーネントに新しい動作が必要なとき、答えが「ブール値の props を追加する」ことはほとんどありません。代わりに、小さな要素を組み合わせます。
// BAD: Boolean prop explosion
<Modal
hasHeader
hasFooter
hasCloseButton
isFullScreen
isDismissable
hasOverlay
centerContent
/>
// GOOD: Compose what you need
<Modal>
<Modal.Header>
<Modal.Title>Settings</Modal.Title>
<Modal.Close />
</Modal.Header>
<Modal.Body>...</Modal.Body>
<Modal.Footer>
<Button onClick={save}>Save</Button>
</Modal.Footer>
</Modal>
パターン 1:複合コンポーネント
コンテキストを通じて暗黙的な状態を共有します。各サブコンポーネントは、それぞれ独立した意味を持ちます。
// 1. Define shared context
interface TabsContextValue {
activeTab: string;
setActiveTab: (tab: string) => void;
}
const TabsContext = createContext<TabsContextValue | null>(null);
function useTabs() {
const ctx = use(TabsContext); // React 19
if (!ctx) throw new Error('useTabs must be used within <Tabs>');
return ctx;
}
// 2. Root component owns the state
function Tabs({ defaultTab, children }: { defaultTab: string; children: React.ReactNode }) {
const [activeTab, setActiveTab] = useState(defaultTab);
return (
<TabsContext value={{ activeTab, setActiveTab }}>
<div role="tablist">{children}</div>
</TabsContext>
);
}
// 3. Sub-components consume context
function TabTrigger({ value, children }: { value: string; children: React.ReactNode }) {
const { activeTab, setActiveTab } = useTabs();
return (
<button
role="tab"
aria-selected={activeTab === value}
onClick={() => setActiveTab(value)}
>
{children}
</button>
);
}
function TabContent({ value, children }: { value: string; children: React.ReactNode }) {
const { activeTab } = useTabs();
if (activeTab !== value) return null;
return <div role="tabpanel">{children}</div>;
}
// 4. Attach sub-components
Tabs.Trigger = TabTrigger;
Tabs.Content = TabContent;
パターン 2:明示的なバリアント
コンポーネントに異なるモードがある場合は、ブール値の切り替えではなく、明示的なバリアントコンポーネントを作成します。
// BAD: Boolean modes
<Input bordered />
<Input underlined />
<Input ghost />
// GOOD: Explicit variants
<Input.Bordered placeholder="Name" />
<Input.Underlined placeholder="Name" />
<Input.Ghost placeholder="Name" />
// Implementation: shared base, variant-specific styles
function createInputVariant(className: string) {
return forwardRef<HTMLInputElement, InputProps>((props, ref) => (
<InputBase ref={ref} className={cn(className, props.className)} {...props} />
));
}
Input.Bordered = createInputVariant('border border-gray-300 rounded-md px-3 py-2');
Input.Underlined = createInputVariant('border-b border-gray-300 px-1 py-2');
Input.Ghost = createInputVariant('bg-transparent px-3 py-2');
パターン 3:レンダープロップより children
コンポジションには children を使用します。子コンポーネントが親からデータを必要とする場合にのみ、レンダープロップを使用します。
// BAD: Render prop when children would work
<Card renderHeader={() => <h2>Title</h2>} renderBody={() => <p>Content</p>} />
// GOOD: Children composition
<Card>
<Card.Header><h2>Title</h2></Card.Header>
<Card.Body><p>Content</p></Card.Body>
</Card>
// ACCEPTABLE: Render prop when child needs parent data
<Combobox>
{({ isOpen, selectedItem }) => (
<>
<Combobox.Input />
{isOpen && <Combobox.Options />}
{selectedItem && <Badge>{selectedItem.label}</Badge>}
</>
)}
</Combobox>
パターン 4:コンテキストインターフェースの設計
状態、アクション、メタデータを明確に分けたコンテキストインターフェースを設計します。
interface FormContext<T> {
// State (read-only from consumer perspective)
values: T;
errors: Record<string, string>;
touched: Record<string, boolean>;
// Actions (stable references)
setValue: (field: keyof T, value: T[keyof T]) => void;
setTouched: (field: keyof T) => void;
validate: () => boolean;
submit: () => Promise<void>;
// Metadata
isSubmitting: boolean;
isDirty: boolean;
isValid: boolean;
}
状態のリフトアップ
兄弟コンポーネントから状態にアクセスする必要がある場合は、状態をプロバイダーに移します。
// BAD: Prop drilling
function Parent() {
const [selected, setSelected] = useState<string | null>(null);
return (
<>
<Sidebar selected={selected} onSelect={setSelected} />
<Detail selected={selected} />
</>
);
}
// GOOD: Shared context
function Parent() {
return (
<SelectionProvider>
<Sidebar />
<Detail />
</SelectionProvider>
);
}
React 19 API
forwardRef をなくす
React 19 では、ref が通常の prop として渡されます。
// Before (React 18)
const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => (
<input ref={ref} {...props} />
));
// After (React 19)
function Input({ ref, ...props }: InputProps & { ref?: React.Ref<HTMLInputElement> }) {
return <input ref={ref} {...props} />;
}
useContext() の代わりに use()
// Before
const ctx = useContext(ThemeContext);
// After (React 19) — works in conditionals and loops
const ctx = use(ThemeContext);
判断ガイド
| 状況 | パターン |
|---|---|
| コンポーネントにブール値のレイアウト用 props が 3 つ以上ある | 複合コンポーネント |
| 同じコンポーネントに複数の表示モードがある | 明示的なバリアント |
| 柔軟な子のレイアウトで親のデータが必要 | レンダープロップ |
| 兄弟コンポーネントが状態を共有する | コンテキストプロバイダー + 状態のリフトアップ |
| スロットを簡単にカスタマイズする | children prop |
| コンポーネントに命令型 API が必要 | useImperativeHandle |
アンチパターン
| 避けるもの | 理由 | 代わりに使うもの |
|---|---|---|
<Component isX isY isZ /> | 組み合わせが爆発的に増え、相互作用が分かりにくくなる | 複合コンポーネントまたは明示的なバリアント |
renderHeader、renderFooter | 親の API が子の構造に依存する | children + スロットコンポーネント |
| 深くネストされたコンテキストプロバイダー | パフォーマンスとデバッグが悪化する | 状態を利用側の近くに配置し、コンテキストを分割する |
注入に React.cloneElement を使う | 脆弱で、ラッパーを使うと壊れる | コンテキストベースのコンポジション |
| すべての状態を単一の巨大なコンテキストにまとめる | 変更のたびにすべての利用側が再レンダリングされる | StateContext + ActionsContext に分割する |
コメント
Markdown 対応