mcpField
R
react-patterns

react-patterns

github.com/josiahsiegel開発5976 回インストール20 週間前に更新
リポジトリ

react-patterns について

再利用可能なインターフェースを設計する開発者向けの、Reactコンポーネント設計パターンを解説するスキルです。状態やロジックの共有、柔軟な描画、アクセシブルなAPIを扱います。

主な機能

  1. 01複合コンポーネント
  2. 02レンダープロップ
  3. 03高階コンポーネント
  4. 04カスタムフックとプロバイダー
  5. 05制御・非制御入力
  6. 06プロップゲッターと状態リデューサー

活用シーン

  1. 01再利用可能なコンポーネントAPIの構築
  2. 02関連コンポーネント間の状態共有
  3. 03コンポーネント間での状態ロジック再利用
  4. 04柔軟なフォーム入力の設計

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/josiahsiegel/claude-plugin-marketplace --skill react-patterns

インストール後、Claude Code で/react-patternsを入力して読み込まれたか確認します。

同梱ファイルreferences/hoc-hooks-provider-controlled-patterns.md

詳細

クイックリファレンス

パターンユースケース例
複合コンポーネント状態を共有する関連コンポーネント<Tabs><Tab /><Panel /></Tabs>
Render Propsロジックを共有した動的なレンダリング<Mouse>{({x,y}) => ...}</Mouse>
HOC横断的な関心事withAuth(Component)
カスタムフック状態を持つロジックの再利用useToggle(), useDebounce()
Provider パターングローバル/共有状態<CartProvider>...</CartProvider>
制御/非制御フォーム入力の柔軟性value と defaultValue
Prop Getterアクセシブルなコンポーネント APIgetButtonProps()
State Reducerカスタマイズ可能な状態ロジックuseReducer(customReducer)

このスキルを使う場面

React コンポーネントのアーキテクチャに使用します。

  • 柔軟で再利用可能なコンポーネント API の構築
  • 複合コンポーネントパターンの実装
  • 動的なコンテンツ向けの Render Props の作成
  • ロジックを再利用するためのカスタムフックの開発
  • 制御入力と非制御入力の管理
  • アクセシブルなコンポーネントインターフェースの設計

状態管理については、react-state-managementを参照してください。


React コンポーネントパターン

複合コンポーネント

基本的な複合コンポーネント
import { createContext, useContext, useState, ReactNode } from 'react';

// Context for sharing state
interface TabsContextType {
  activeTab: string;
  setActiveTab: (tab: string) => void;
}

const TabsContext = createContext<TabsContextType | null>(null);

function useTabsContext() {
  const context = useContext(TabsContext);
  if (!context) {
    throw new Error('Tabs components must be used within a Tabs provider');
  }
  return context;
}

// Parent component
interface TabsProps {
  defaultTab: string;
  children: ReactNode;
}

function Tabs({ defaultTab, children }: TabsProps) {
  const [activeTab, setActiveTab] = useState(defaultTab);

  return (
    <TabsContext.Provider value={{ activeTab, setActiveTab }}>
      <div className="tabs">{children}</div>
    </TabsContext.Provider>
  );
}

// Tab List
function TabList({ children }: { children: ReactNode }) {
  return <div className="tab-list" role="tablist">{children}</div>;
}

// Tab Button
interface TabProps {
  value: string;
  children: ReactNode;
}

function Tab({ value, children }: TabProps) {
  const { activeTab, setActiveTab } = useTabsContext();

  return (
    <button
      role="tab"
      aria-selected={activeTab === value}
      onClick={() => setActiveTab(value)}
      className={activeTab === value ? 'active' : ''}
    >
      {children}
    </button>
  );
}

// Tab Panel
interface TabPanelProps {
  value: string;
  children: ReactNode;
}

function TabPanel({ value, children }: TabPanelProps) {
  const { activeTab } = useTabsContext();

  if (activeTab !== value) return null;

  return (
    <div role="tabpanel" className="tab-panel">
      {children}
    </div>
  );
}

// Attach sub-components
Tabs.List = TabList;
Tabs.Tab = Tab;
Tabs.Panel = TabPanel;

export { Tabs };

// Usage
function App() {
  return (
    <Tabs defaultTab="overview">
      <Tabs.List>
        <Tabs.Tab value="overview">Overview</Tabs.Tab>
        <Tabs.Tab value="features">Features</Tabs.Tab>
        <Tabs.Tab value="pricing">Pricing</Tabs.Tab>
      </Tabs.List>
      <Tabs.Panel value="overview">Overview content</Tabs.Panel>
      <Tabs.Panel value="features">Features content</Tabs.Panel>
      <Tabs.Panel value="pricing">Pricing content</Tabs.Panel>
    </Tabs>
  );
}
Slot パターンを使った柔軟な複合コンポーネント
import { createContext, useContext, useState, ReactNode, isValidElement, Children, cloneElement } from 'react';

interface AccordionContextType {
  openItems: Set<string>;
  toggleItem: (id: string) => void;
  allowMultiple: boolean;
}

const AccordionContext = createContext<AccordionContextType | null>(null);

interface AccordionProps {
  children: ReactNode;
  allowMultiple?: boolean;
  defaultOpen?: string[];
}

function Accordion({ children, allowMultiple = false, defaultOpen = [] }: AccordionProps) {
  const [openItems, setOpenItems] = useState<Set<string>>(new Set(defaultOpen));

  const toggleItem = (id: string) => {
    setOpenItems((prev) => {
      const next = new Set(prev);
      if (next.has(id)) {
        next.delete(id);
      } else {
        if (!allowMultiple) next.clear();
        next.add(id);
      }
      return next;
    });
  };

  return (
    <AccordionContext.Provider value={{ openItems, toggleItem, allowMultiple }}>
      <div className="accordion">{children}</div>
    </AccordionContext.Provider>
  );
}

interface AccordionItemProps {
  id: string;
  children: ReactNode;
}

function AccordionItem({ id, children }: AccordionItemProps) {
  const context = useContext(AccordionContext);
  if (!context) throw new Error('AccordionItem must be within Accordion');

  const isOpen = context.openItems.has(id);

  return (
    <div className={`accordion-item ${isOpen ? 'open' : ''}`} data-state={isOpen ? 'open' : 'closed'}>
      {Children.map(children, (child) => {
        if (isValidElement(child)) {
          return cloneElement(child as React.ReactElement<{ itemId?: string; isOpen?: boolean }>, {
            itemId: id,
            isOpen,
          });
        }
        return child;
      })}
    </div>
  );
}

interface AccordionTriggerProps {
  children: ReactNode;
  itemId?: string;
  isOpen?: boolean;
}

function AccordionTrigger({ children, itemId, isOpen }: AccordionTriggerProps) {
  const context = useContext(AccordionContext);
  if (!context || !itemId) return null;

  return (
    <button
      className="accordion-trigger"
      aria-expanded={isOpen}
      onClick={() => context.toggleItem(itemId)}
    >
      {children}
      <span className="icon">{isOpen ? '−' : '+'}</span>
    </button>
  );
}

interface AccordionContentProps {
  children: ReactNode;
  isOpen?: boolean;
}

function AccordionContent({ children, isOpen }: AccordionContentProps) {
  if (!isOpen) return null;

  return <div className="accordion-content">{children}</div>;
}

Accordion.Item = AccordionItem;
Accordion.Trigger = AccordionTrigger;
Accordion.Content = AccordionContent;

export { Accordion };

Render Props パターン

基本的な Render Props
import { useState, ReactNode } from 'react';

interface ToggleRenderProps {
  on: boolean;
  toggle: () => void;
  setOn: (value: boolean) => void;
}

interface ToggleProps {
  defaultOn?: boolean;
  children: (props: ToggleRenderProps) => ReactNode;
}

function Toggle({ defaultOn = false, children }: ToggleProps) {
  const [on, setOn] = useState(defaultOn);

  const toggle = () => setOn((prev) => !prev);

  return <>{children({ on, toggle, setOn })}</>;
}

// Usage
function App() {
  return (
    <Toggle defaultOn={false}>
      {({ on, toggle }) => (
        <div>
          <p>The toggle is {on ? 'ON' : 'OFF'}</p>
          <button onClick={toggle}>Toggle</button>
        </div>
      )}
    </Toggle>
  );
}
マウス位置トラッカー
import { useState, useEffect, ReactNode } from 'react';

interface MousePosition {
  x: number;
  y: number;
}

interface MouseTrackerProps {
  children: (position: MousePosition) => ReactNode;
}

function MouseTracker({ children }: MouseTrackerProps) {
  const [position, setPosition] = useState<MousePosition>({ x: 0, y: 0 });

  useEffect(() => {
    const handleMouseMove = (e: MouseEvent) => {
      setPosition({ x: e.clientX, y: e.clientY });
    };

    window.addEventListener('mousemove', handleMouseMove);
    return () => window.removeEventListener('mousemove', handleMouseMove);
  }, []);

  return <>{children(position)}</>;
}

// Usage
function App() {
  return (
    <MouseTracker>
      {({ x, y }) => (
        <div
          style={{
            position: 'absolute',
            left: x + 10,
            top: y + 10,
            background: 'black',
            color: 'white',
            padding: '4px 8px',
            borderRadius: '4px',
          }}
        >
          {x}, {y}
        </div>
      )}
    </MouseTracker>
  );
}
データフェッチ用 Render Props
import { useState, useEffect, ReactNode } from 'react';

interface FetchState<T> {
  data: T | null;
  loading: boolean;
  error: Error | null;
  refetch: () => void;
}

interface FetchProps<T> {
  url: string;
  children: (state: FetchState<T>) => ReactNode;
}

function Fetch<T>({ url, children }: FetchProps<T>) {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  const fetchData = async () => {
    setLoading(true);
    setError(null);
    try {
      const response = await fetch(url);
      if (!response.ok) throw new Error('Failed to fetch');
      const result = await response.json();
      setData(result);
    } catch (err) {
      setError(err instanceof Error ? err : new Error('Unknown error'));
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    fetchData();
  }, [url]);

  return <>{children({ data, loading, error, refetch: fetchData })}</>;
}

// Usage
function UserList() {
  return (
    <Fetch<User[]> url="/api/users">
      {({ data, loading, error, refetch }) => {
        if (loading) return <p>Loading...</p>;
        if (error) return <p>Error: {error.message}</p>;

        return (
          <div>
            <button onClick={refetch}>Refresh</button>
            <ul>
              {data?.map((user) => (
                <li key={user.id}>{user.name}</li>
              ))}
            </ul>
          </div>
        );
      }}
    </Fetch>
  );
}

ベストプラクティスの概要

パターンユースケース
複合コンポーネント暗黙的な状態を共有する関連コンポーネント
Render Propsロジックを共有した動的なレンダリング
HOC横断的な関心事、コードの再利用
カスタムフック状態を持つロジックの再利用
Provider パターングローバル/共有状態の管理
制御/非制御フォーム入力の柔軟性
Prop Getterアクセシブルなコンポーネント API
State Reducerカスタマイズ可能な状態ロジック

その他の React パターン

残りの6つのパターンの完全なコード例については、references/hoc-hooks-provider-controlled-patterns.mdを参照してください。

  • 高階コンポーネント(HOC) — コンポーネントを受け取り、動作を追加した新しいコンポーネントを返す関数です。
  • パターンとしてのカスタムフック — 状態を持つロジックをuseXフックにカプセル化します。最新の React では HOC や Render Props よりもよく使われます。
  • Provider パターン — React.createContextと、コンテキストを利用するuseContextフックを組み合わせます。
  • 制御コンポーネントと非制御コンポーネント — props によって値を駆動する方式と、ref によって内部状態を駆動する方式です。
  • Prop Getter パターン — 利用側に事前設定済みの props を渡すgetProps()コールバックを返します(Downshift、Headless UI で使用されています)。
  • State Reducer パターン — 利用側が状態遷移をオーバーライドできるよう、reducer の prop を受け取ります(Kent C. Dodds による標準的な実装)。

コメント