react-composable-components について
小さく再利用しやすいコンポーネントを設計するための React スキルです。複合コンポーネントや Context、柔軟なプロパティの使い方を示し、不要な再レンダリングの抑制も扱います。
主な機能
- 01複合コンポーネントのパターン
- 02React Context による状態共有
- 03HTML 属性と className の透過
- 04clsx と twMerge によるクラス統合
- 05ref の転送
- 06インライン描画ロジックの分割
活用シーン
- 01再利用可能なタブやアコーディオンの設計
- 02大規模な React コンポーネントの分割
- 03利用側でのコンポーネントレイアウトのカスタマイズ
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ jonmumm/ skills --skill react-composable-componentsインストール後、Claude Code で/react-composable-componentsを入力して読み込まれたか確認します。
詳細
React のコンポーザブルコンポーネント
小さく、役割が明確で、高い合成性を備えた React コンポーネントを作成するためのパターンです。
基本的な考え方
多くの状態を管理し、複数の異なる UI 領域をレンダリングする大きなコンポーネントは、可読性、再利用性、レンダリングパフォーマンスが低下します。
避けること: 多数の props(hasHeader、showFooter など)を受け取る、巨大でモノリシックなコンポーネント。
推奨すること: UI を、HTML 属性を透過的に渡し、カスタマイズしやすいようにユーティリティクラスをマージする、アトミックで合成可能なパーツ(複合コンポーネント)に分割します。
1. インラインのレンダリングメソッドを抽出する
コンポーネントに renderStoryScreen() のようなメソッドがある場合は、独立した子コンポーネントとして抽出します。こうすることで、状態の小さな変化によって巨大なコンポーネント全体が不要に再レンダリングされるのを防げます。
// Prefer returning separate components instead of calling inline logic blocks
function App() {
const [screen, setScreen] = useState('START');
return (
<main>
{screen === 'START' && <StartScreen onStart={() => setScreen('STORY')} />}
{screen === 'STORY' && <StoryScreen />}
</main>
);
}
2. 設定 props より複合コンポーネントを使う
複雑なオブジェクトを単一のコンポーネントに渡す代わりに(<MonolithCard footerButtons={<button>} />)、サブコンポーネントを公開します。これにより、利用側はカスタムコンテンツを挿入したり、パーツを完全に省略したりできます。
export function App() {
return (
<Card className="border-blue-500">
<CardHeader>
<CardTitle>Adventure Begins</CardTitle>
</CardHeader>
<CardContent>
{/* Consumer fully controls the layout and sub-components! */}
<img src="/hero.png" alt="Hero" className="w-full h-auto" />
</CardContent>
</Card>
);
}
3. 共有状態には Context を使う
Tabs や Accordions のような複雑な複合コンポーネントを作るときは、ローカルな React Context を使って内部で状態を共有します。これにより、利用側で props を手動でバケツリレーする必要がなくなります(isOpen={isOpen} setIsOpen={setIsOpen})。
import { createContext, useContext, useState } from "react";
const TabsContext = createContext<{ activeTab: string; setActiveTab: (v: string) => void } | null>(null);
export function Tabs({ defaultValue, children }) {
const [activeTab, setActiveTab] = useState(defaultValue);
return <TabsContext.Provider value={{ activeTab, setActiveTab }}>{children}</TabsContext.Provider>;
}
export function Tab({ value, children }) {
const ctx = useContext(TabsContext);
const isActive = ctx?.activeTab === value;
return <button onClick={() => ctx?.setActiveTab(value)} className={isActive ? "font-bold" : ""}>{children}</button>;
}
export function TabContent({ value, children }) {
const ctx = useContext(TabsContext);
return ctx?.activeTab === value ? <div>{children}</div> : null;
}
使用例:
<Tabs defaultValue="home">
<Tab value="home">Home</Tab>
<Tab value="settings">Settings</Tab>
<TabContent value="home">Home Content!</TabContent>
<TabContent value="settings">Settings Content!</TabContent>
</Tabs>
4. props の透過性と className のマージ
合成可能なコンポーネントは、ネイティブの HTML 要素のように動作するべきです。className と ...props を受け取ることで、コンポーネントの API を肥大化させずに、利用側が完全に制御できるようにします。
import { forwardRef } from "react";
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs));
export const Button = forwardRef<HTMLButtonElement, React.ButtonHTMLAttributes<HTMLButtonElement>>(
({ className, variant = "primary", ...props }, ref) => (
<button
ref={ref}
className={cn(
"rounded-md px-4 py-2 transition-colors",
variant === "primary" ? "bg-blue-500 text-white" : "bg-gray-200 text-gray-900",
className
)}
{...props}
/>
)
);
Button.displayName = "Button";
5. react-twc でコンポーネントを簡潔にする
forwardRef と className のマージを使ったラッパーコンポーネントの作成は、定型コードが多くなりがちです。スタイルを props に対応付ける場合は、react-twc を使うと、forwardRef と className のマージを自動的に処理する、styled-components のような API を利用できます。
import { twc } from "react-twc";
export const Title = twc.h1`text-4xl font-extrabold tracking-tight`;
// Perfect for building Compound Components concisely:
export const Card = twc.div`rounded-xl border bg-card shadow`;
export const CardHeader = twc.div`flex flex-col space-y-1.5 p-6`;
export const CardTitle = twc.h3`font-semibold leading-none tracking-tight`;
export const CardContent = twc.div`p-6 pt-0`;
まとめのチェックリスト
- 再利用可能なすべての UI コンポーネントで
classNameと...propsを受け取る。 -
twMerge(clsx(...))やreact-twcのようなユーティリティを使い、デフォルトのクラスと利用側による上書きを自然にマージする。 - 大量の設定 props オブジェクトに頼るのではなく、
Header、Title、Contentのような 構造的なパーツを公開する。 - 親コンポーネントが基盤となる DOM 要素を直接操作できるよう、
refの転送(React.forwardRef)を提供する。 - 内部で状態を共有する必要がある複合コンポーネントには、ローカル Context を使う。
- インラインの
renderSomething()メソッドを実際の React コンポーネントに分割して、レンダリングを最適化する。