mcpField
R
react-composable-components

react-composable-components

github.com/jonmumm開発245 回インストール4 日前に更新
リポジトリ

react-composable-components について

小さく再利用しやすいコンポーネントを設計するための React スキルです。複合コンポーネントや Context、柔軟なプロパティの使い方を示し、不要な再レンダリングの抑制も扱います。

主な機能

  1. 01複合コンポーネントのパターン
  2. 02React Context による状態共有
  3. 03HTML 属性と className の透過
  4. 04clsx と twMerge によるクラス統合
  5. 05ref の転送
  6. 06インライン描画ロジックの分割

活用シーン

  1. 01再利用可能なタブやアコーディオンの設計
  2. 02大規模な React コンポーネントの分割
  3. 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 コンポーネントに分割して、レンダリングを最適化する。

コメント