mcpField
R
react-patterns

react-patterns

github.com/claude-dev-suite開発4641 回インストール4 日前に更新
リポジトリ

react-patterns について

React 開発者向けのコンポーネントパターンをまとめたスキルです。複合コンポーネント、Render Props、HOC、カスタムフック、コンポジションと、それぞれの用途や注意点を扱います。

主な機能

  1. 01複合コンポーネントパターン
  2. 02Render Props と HOC
  3. 03カスタムフック
  4. 04コンポーネントのコンポジション
  5. 05パターン選択と問題の調査

活用シーン

  1. 01関連コンポーネントを柔軟に構築
  2. 02状態を持つロジックを再利用
  3. 03コンポーネントのロジックと UI を分離
  4. 04Context や描画の問題を調査

インストール

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

$npx skills add https://github.com/claude-dev-suite/claude-dev-suite --skill react-patterns

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

同梱ファイルadvanced.md

詳細

React パターン

完全なリファレンス:Container/Presentational、State Machine、Slots、Polymorphic、Provider のパターンについては、advanced.md を参照してください。

Compound Components

連携して一貫性のある UI を構成するコンポーネントです。

import { createContext, useContext, useState, ReactNode } from 'react';

interface AccordionContextValue {
  openItems: Set<string>;
  toggle: (id: string) => void;
}

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

function useAccordion() {
  const context = useContext(AccordionContext);
  if (!context) {
    throw new Error('Accordion components must be used within Accordion');
  }
  return context;
}

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

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

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

// Attach sub-components
Accordion.Item = Item;
Accordion.Trigger = Trigger;
Accordion.Content = Content;

Render Props

レンダリングを制御するために、関数を children または props として渡します。

interface DataFetcherProps<T> {
  url: string;
  children: (state: {
    data: T | null;
    loading: boolean;
    error: Error | null;
    refetch: () => void;
  }) => ReactNode;
}

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

  const fetchData = useCallback(async () => {
    setLoading(true);
    setError(null);
    try {
      const res = await fetch(url);
      if (!res.ok) throw new Error('Failed to fetch');
      setData(await res.json());
    } catch (e) {
      setError(e as Error);
    } finally {
      setLoading(false);
    }
  }, [url]);

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

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

// Usage
<DataFetcher<User[]> url="/api/users">
  {({ data, loading, error, refetch }) => {
    if (loading) return <Spinner />;
    if (error) return <ErrorMessage error={error} onRetry={refetch} />;
    return <ul>{data?.map(user => <li key={user.id}>{user.name}</li>)}</ul>;
  }}
</DataFetcher>

Higher-Order Components(HOC)

機能を追加するために、コンポーネントをラップします。

function withLoading<P extends object>(
  WrappedComponent: React.ComponentType<P>,
  LoadingComponent: React.ComponentType = DefaultSpinner
) {
  return function WithLoadingComponent(props: P & { loading: boolean }) {
    const { loading, ...restProps } = props;
    if (loading) return <LoadingComponent />;
    return <WrappedComponent {...(restProps as P)} />;
  };
}

// HOC with Refs
function withLogger<P extends object>(WrappedComponent: React.ComponentType<P>) {
  const WithLogger = React.forwardRef<unknown, P>((props, ref) => {
    useEffect(() => { console.log('Props changed:', props); }, [props]);
    return <WrappedComponent {...props} ref={ref} />;
  });
  WithLogger.displayName = `WithLogger(${WrappedComponent.displayName || WrappedComponent.name})`;
  return WithLogger;
}

Custom Hooks パターン

再利用可能なロジックをフックに切り出します。

// useLocalStorage
function useLocalStorage<T>(key: string, initialValue: T) {
  const [storedValue, setStoredValue] = useState<T>(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch { return initialValue; }
  });

  const setValue = useCallback((value: T | ((val: T) => T)) => {
    const valueToStore = value instanceof Function ? value(storedValue) : value;
    setStoredValue(valueToStore);
    window.localStorage.setItem(key, JSON.stringify(valueToStore));
  }, [key, storedValue]);

  return [storedValue, setValue] as const;
}

// useDebounce
function useDebounce<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState(value);
  useEffect(() => {
    const timer = setTimeout(() => setDebouncedValue(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);
  return debouncedValue;
}

// useOnClickOutside
function useOnClickOutside(
  ref: RefObject<HTMLElement>,
  handler: (event: MouseEvent | TouchEvent) => void
) {
  useEffect(() => {
    const listener = (event: MouseEvent | TouchEvent) => {
      if (!ref.current || ref.current.contains(event.target as Node)) return;
      handler(event);
    };
    document.addEventListener('mousedown', listener);
    document.addEventListener('touchstart', listener);
    return () => {
      document.removeEventListener('mousedown', listener);
      document.removeEventListener('touchstart', listener);
    };
  }, [ref, handler]);
}

Composition パターン

継承よりもコンポジションを優先します。

function Card({ children, className }: CardProps) {
  return <div className={`card ${className || ''}`}>{children}</div>;
}

function CardHeader({ children }: { children: ReactNode }) {
  return <div className="card-header">{children}</div>;
}

function CardBody({ children }: { children: ReactNode }) {
  return <div className="card-body">{children}</div>;
}

// Specialized cards through composition
function UserCard({ user }: { user: User }) {
  return (
    <Card>
      <CardHeader>
        <Avatar src={user.avatar} />
        <h3>{user.name}</h3>
      </CardHeader>
      <CardBody><p>{user.bio}</p></CardBody>
    </Card>
  );
}

ベストプラクティスのまとめ

パターン使用する場面
Compound Components柔軟性の高い、関連するコンポーネント
Render Props振る舞いを共有し、レンダリングを制御する場合
HOC横断的な関心事、レガシーコード
Custom Hooks再利用可能な状態を持つロジック
Composition単純な要素から複雑な要素を構築する場合
Container/Presentationalロジックと UI を分離する場合
State Machine複雑な状態遷移
Polymorphic柔軟な要素の種類
  • 再利用可能なロジックにはカスタムフックを使用します
  • コンポーネントは小さく、役割を明確に保ちます
  • DX を向上させるために TypeScript を使用します
  • パターンを過剰に使用しないでください。KISS を心がけます
  • HOC を過度にネストしないでください

アンチパターン

アンチパターン問題解決策
単純なコンポーネントの過剰な設計不必要な複雑さシンプルな props を使用する
HOC の過剰なネストprops の衝突、デバッグの困難さコンポジションまたはフックを使用する
メモ化しない Render Propsパフォーマンスの問題レンダー関数をメモ化する
深い階層の prop drilling保守が困難Context または Compound Components を使用する
頻繁に変化する値に Context を使用するパフォーマンスの問題状態管理ライブラリを使用する

トラブルシューティングのクイックガイド

問題考えられる原因修正方法
Context を利用できないProvider がないProvider コンポーネントでラップする
HOC で ref が失われるref が転送されていないReact.forwardRef を使用する
Render Props による再レンダリング関数の同一性が変化するレンダー関数をメモ化する

リファレンスドキュメント

コメント