mcpField
R
react-patterns

react-patterns

github.com/dralgorhythm開発12964 回インストール15 週間前に更新
リポジトリ

react-patterns について

TypeScript を使った保守性の高い React アプリ向けのスキルです。コンポジションパターンや React 19 の機能を扱い、Web とモバイルの開発に使える実装例を示します。

主な機能

  1. 01コンポーネントのコンポジションパターン
  2. 02props とイベントの型定義
  3. 03再利用可能なカスタムフック
  4. 04React 19 のフックと機能
  5. 05状態管理とパフォーマンスのパターン
  6. 06Suspense とエラーバウンダリ

活用シーン

  1. 01再利用可能なコンポーネント API の設計
  2. 02コンポーネント間でのロジック共有
  3. 03複雑な UI 状態の管理
  4. 04ローディングとエラー処理の確認

MCP 対応

README では、React API の完全なリファレンスを調べるために Context7 MCP を推奨しています。

インストール

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

$npx skills add https://github.com/dralgorhythm/claude-agentic-framework --skill react-patterns

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

詳細

React パターン

プラットフォーム: Web およびモバイル(共通の React パターン)。Pressable、ScrollView、FlashList、セーフエリアなど、React Native 固有のパターンについては、react-native-patterns スキルを参照してください。

概要

React 19 の機能とコンポジションパターンを活用して、保守しやすい React アプリケーションを TypeScript で構築するためのパターンです。

API リファレンス: React の API リファレンス全体は Context7 MCP(mcp__context7__resolve-library-id → react)を使用してください。

ワークフロー

  • 適切なコンポーネントのコンポジションパターンを選択する
  • props とイベントに TypeScript の型を適用する
  • 共通ロジックにカスタムフックを実装する
  • 必要に応じてパフォーマンスを最適化する
  • Suspense/境界を使用して読み込み状態とエラー状態を処理する
  • コンポーネントのレンダリング動作を検証する

フィードバックループ

  • コンポーネントが TypeScript エラーなしでレンダリングされる
  • props に適切な型が付けられ、検証されている
  • カスタムフックの戻り値の型が明確である
  • 不要な再レンダリングがない(React DevTools Profiler を使用)
  • エラー境界がコンポーネントのエラーを捕捉する
  • 読み込み状態が Suspense で正しく動作する

実装例

1. コンポーネントのコンポジション
複合コンポーネント
// Shares implicit state between parent and children
const TabsContext = createContext<{ activeTab: string; setActiveTab: (id: string) => void } | null>(null);

function Tabs({ children, defaultTab }: { children: ReactNode; defaultTab: string }) {
  const [activeTab, setActiveTab] = useState(defaultTab);
  return <TabsContext.Provider value={{ activeTab, setActiveTab }}>{children}</TabsContext.Provider>;
}

function Tab({ id, children }: { id: string; children: ReactNode }) {
  const ctx = use(TabsContext);
  if (!ctx) throw new Error('Tab must be used within Tabs');
  return (
    <button role="tab" aria-selected={ctx.activeTab === id} onClick={() => ctx.setActiveTab(id)}>
      {children}
    </button>
  );
}
スロットパターン
// Named slots for flexible composition
interface CardProps { header?: ReactNode; footer?: ReactNode; children: ReactNode; }

function Card({ header, footer, children }: CardProps) {
  return (
    <div className="card">
      {header && <div className="card-header">{header}</div>}
      <div className="card-body">{children}</div>
      {footer && <div className="card-footer">{footer}</div>}
    </div>
  );
}
2. React 19 の機能
use() フック
// Unwrap promises and context — simpler than useContext
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
  const user = use(userPromise); // Suspends until resolved
  return <div>{user.name}</div>;
}
useActionState
async function updateUser(prevState: { error?: string }, formData: FormData) {
  'use server';
  return { error: undefined };
}

function UserForm() {
  const [state, formAction, isPending] = useActionState(updateUser, {});
  return (
    <form action={formAction}>
      <input name="name" disabled={isPending} />
      {state.error && <p className="error">{state.error}</p>}
      <button disabled={isPending}>{isPending ? 'Saving...' : 'Save'}</button>
    </form>
  );
}
useOptimistic
function TodoList({ todos }: { todos: Todo[] }) {
  const [optimisticTodos, addOptimisticTodo] = useOptimistic(
    todos,
    (state, newTodo: Todo) => [...state, newTodo]
  );
  async function handleAdd(formData: FormData) {
    const todo = { id: crypto.randomUUID(), text: formData.get('text') as string };
    addOptimisticTodo(todo);
    await saveTodo(todo);
  }
  return (
    <form action={handleAdd}>
      {optimisticTodos.map(todo => <li key={todo.id}>{todo.text}</li>)}
      <input name="text" /><button>Add</button>
    </form>
  );
}
3. カスタムフック
// Object return for multiple named values
function useAuth() {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);
  const login = async (credentials: Credentials) => { setUser(await api.login(credentials)); };
  return { user, loading, login };
}

// Tuple return for positional access (like useState)
function useToggle(initial = false): [boolean, () => void] {
  const [value, setValue] = useState(initial);
  return [value, useCallback(() => setValue(v => !v), [])];
}

// Composing hooks
function useLocalStorage<T>(key: string, initial: T) {
  const [value, setValue] = useState<T>(() => {
    const stored = localStorage.getItem(key);
    return stored ? JSON.parse(stored) : initial;
  });
  useEffect(() => { localStorage.setItem(key, JSON.stringify(value)); }, [key, value]);
  return [value, setValue] as const;
}
4. TypeScript + React
// Extend HTML element props
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: 'primary' | 'secondary';
  loading?: boolean;
}
function Button({ variant = 'primary', loading, children, ...props }: ButtonProps) {
  return <button className={variant} disabled={loading} {...props}>{loading ? 'Loading...' : children}</button>;
}

// Generic components with full type inference
interface ListProps<T> { items: T[]; renderItem: (item: T) => ReactNode; keyExtractor: (item: T) => string; }
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
  return <ul>{items.map(item => <li key={keyExtractor(item)}>{renderItem(item)}</li>)}</ul>;
}

// Refs as props (React 19+) — forwardRef is deprecated
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
  label: string;
  ref?: React.Ref<HTMLInputElement>;
}
function Input({ label, ref, ...props }: InputProps) {
  return <label>{label}<input ref={ref} {...props} /></label>;
}
5. 状態管理
複雑な状態に対する useReducer
type Action =
  | { type: 'FETCH_START' }
  | { type: 'FETCH_SUCCESS'; payload: User[] }
  | { type: 'FETCH_ERROR'; payload: string };

function reducer(state: { data: User[]; loading: boolean; error: string | null }, action: Action) {
  switch (action.type) {
    case 'FETCH_START': return { ...state, loading: true, error: null };
    case 'FETCH_SUCCESS': return { ...state, loading: false, data: action.payload };
    case 'FETCH_ERROR': return { ...state, loading: false, error: action.payload };
  }
}
6. パフォーマンスパターン
// memo + useCallback prevent unnecessary re-renders
const ListItem = memo(function ListItem({ item, onDelete }: ItemProps) {
  return <li>{item.name}<button onClick={() => onDelete(item.id)}>Delete</button></li>;
});

function List() {
  const [items, setItems] = useState<Item[]>([]);
  const handleDelete = useCallback((id: string) => {
    setItems(items => items.filter(item => item.id !== id));
  }, []);
  return <>{items.map(item => <ListItem key={item.id} item={item} onDelete={handleDelete} />)}</>;
}

// Lazy loading with Suspense
const Dashboard = lazy(() => import('./Dashboard'));
function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Routes><Route path="/dashboard" element={<Dashboard />} /></Routes>
    </Suspense>
  );
}
7. エラー処理
class ErrorBoundary extends Component<{ children: ReactNode; fallback?: ReactNode }, { hasError: boolean }> {
  state = { hasError: false };
  static getDerivedStateFromError(): { hasError: boolean } { return { hasError: true }; }
  componentDidCatch(error: Error, info: ErrorInfo) { console.error(error, info); }
  render() {
    return this.state.hasError ? (this.props.fallback ?? <div>Something went wrong</div>) : this.props.children;
  }
}

// Wrap async data loading with Suspense
function UserProfile({ userId }: { userId: string }) {
  const user = use(fetchUser(userId)); // Suspends
  return <div>{user.name}</div>;
}

ベストプラクティス

  • 継承よりコンポジション - 柔軟性を高めるため、コンポジションパターンを使用します
  • すべてに型を付ける - TypeScript を活用して、コンパイル時の安全性を確保します
  • 状態を使用箇所の近くに置く - 状態は、できるだけ使用する場所の近くに保ちます
  • カスタムフックを切り出す - フックを使ってコンポーネント間でロジックを共有します
  • 参照を安定させる - useCallback/useMemo を使って不要な再レンダリングを防ぎます
  • エラー境界 - コンポーネントツリーをラップして、レンダリングエラーを捕捉します
  • 読み込みには Suspense - 手動の読み込み状態ではなく、Suspense を使用します
  • props のバケツリレーを避ける - 深くネストされた props には、コンテキストまたはコンポジションを使用します

アンチパターン

  • React 19 で forwardRef を使う - 代わりに ref を通常の prop として使用します
  • 新しいコードにクラスコンポーネントを使う - 関数コンポーネントとフックを使用します
  • 状態を直接変更する - 必ず setState または reducer のアクションを使用します
  • 依存配列を省略する - useEffect/useMemo/useCallback にすべての依存関係を含めます
  • useMemo/useCallback を過剰に使う - 必要な場合にのみ最適化します(まずプロファイリングします)
  • 何にでもコンテキストを使う - コンテキストは控えめに使い、props または状態管理ライブラリを優先します
  • useState に派生状態を持たせる - 代わりにレンダリング中に派生値を計算します
  • 派生状態に useEffect を使う - useMemo を使用するか、レンダリング中に直接計算します
  • キーにインデックスを使う - リストのキーには、安定した一意の ID を使用します
  • TypeScript のエラーを無視する - 近道として 'any' や '// @ts-ignore' を決して使いません

コメント