mcpField
R
react-patterns

react-patterns

github.com/giuseppe-trisciuoglio開発3654.7k 回インストール7 週間前に更新
リポジトリ

react-patterns について

React 19で開発するエンジニア向けのパターン集です。Next.js App Routerを含む開発で、フック、Server Actions、非同期UI、パフォーマンスの指針を整理しています。

主な機能

  1. 01React 19 APIとフックのパターン
  2. 02Server/Client Componentの指針
  3. 03楽観的UIとServer Actions
  4. 04Suspenseと非同期レンダリング
  5. 05状態管理とパフォーマンスのパターン

活用シーン

  1. 01Next.js App Routerアプリの構築
  2. 02検証付きServer Actionフォームの実装
  3. 03データ変更への楽観的な応答
  4. 04複雑なコンポーネント状態に使うフックの選択

インストール

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

$npx skills add https://github.com/giuseppe-trisciuoglio/developer-kit --skill react-patterns

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

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

references/
component-patterns.md
hooks-patterns.md
learn.md
performance-patterns.md
react19-features.md
reference.md
typescript-patterns.md

詳細

React 19 の開発パターン

概要

Next.js App Router、Server Actions、楽観的 UI、並行機能に対応した React 19 のパターンです。API の概要はクイックリファレンスを、コピーして使えるパターンは例を参照してください。

使用する場面

  • Next.js App Router で React 19 アプリケーションを構築する
  • useOptimistic または useTransition で楽観的 UI を実装する
  • フォーム検証を伴う Server Actions を作成する
  • クラスコンポーネントからフックに移行する
  • React Compiler で並行レンダリングを最適化する
  • useReducer またはカスタムフックで複雑な状態を管理する
  • 非同期処理を Suspense 境界でラップする

クイックリファレンス

パターンフック/API使用例
ローカル状態useStateシンプルなコンポーネントの状態
複雑な状態useReducer複数のアクションを持つ状態マシン
副作用useEffectサブスクリプション、データ取得
共有状態useContext / createContextコンポーネント間のデータ
DOM へのアクセスuseRefフォーカス、測定、タイマー
パフォーマンスuseMemo / useCallbackコストの高い計算
緊急性の低い更新useTransition大きなリストの検索/フィルター
コストの高い UI の遅延useDeferredValue更新中も古い値を表示
リソースの読み取りuse() (React 19)レンダリング中の Promise とコンテキスト
楽観的 UIuseOptimistic (React 19)ミューテーションへの即時フィードバック
フォームの状態useFormStatus (React 19)子コンポーネントの保留状態
フォームの状態useActionState (React 19)Server Action の結果
自動メモ化React Compiler手動でのメモ化/コールバックを不要にします

手順

  1. コンポーネントの種類を特定する:Server Component と Client Component のどちらが必要か判断します
  2. フックを選択する:状態管理と副作用に適したフックを使用します
  3. Props の型を定義する:すべてのコンポーネントの Props に TypeScript インターフェースを定義します
  4. 非同期処理を扱う:データ取得を行うコンポーネントを Suspense 境界でラップします
  5. 最適化する:コストの高いレンダリングには React Compiler または手動のメモ化を使用します
  6. エラーを処理する:エラーを適切に処理するために ErrorBoundary を追加します
  7. Server Actions を検証する:Zod/スキーマ検証を定義してから、次のテストを行います
    • 無効な入力を送信 → 拒否されることを確認します
    • 有効な入力を送信 → 成功することを確認します

例

クライアントとのインタラクションを伴う Server Component
// Server Component (default) — async, fetches data
async function ProductPage({ id }: { id: string }) {
  const product = await db.product.findUnique({ where: { id } });

  return (
    <div>
      <h1>{product.name}</h1>
      <AddToCartButton productId={product.id} />
    </div>
  );
}

// Client Component — handles interactivity
'use client';
function AddToCartButton({ productId }: { productId: string }) {
  const [isPending, startTransition] = useTransition();

  const handleAdd = () => {
    startTransition(async () => {
      await addToCart(productId);
    });
  };

  return (
    <button onClick={handleAdd} disabled={isPending}>
      {isPending ? 'Adding...' : 'Add to Cart'}
    </button>
  );
}
即時フィードバックのための useOptimistic
'use client';
import { useOptimistic } from 'react';

function TodoList({ todos, addTodo }: { todos: Todo[]; addTodo: (t: Todo) => Promise<void> }) {
  const [optimisticTodos, addOptimisticTodo] = useOptimistic(
    todos,
    (state, newTodo: Todo) => [...state, { ...newTodo, pending: true }]
  );

  const handleSubmit = async (formData: FormData) => {
    const newTodo = { id: Date.now(), text: formData.get('text') as string };
    addOptimisticTodo(newTodo);  // Immediate UI update
    await addTodo(newTodo);      // Actual backend call
  };

  return (
    <form action={handleSubmit}>
      {optimisticTodos.map(todo => (
        <div key={todo.id} style={{ opacity: todo.pending ? 0.5 : 1 }}>
          {todo.text}
        </div>
      ))}
      <input type="text" name="text" />
      <button type="submit">Add</button>
    </form>
  );
}
フォームを使った Server Action
// app/actions.ts
'use server';
import { z } from 'zod';
import { revalidatePath } from 'next/cache';

const schema = z.object({
  title: z.string().min(5),
  content: z.string().min(10),
});

export async function createPost(prevState: any, formData: FormData) {
  const parsed = schema.safeParse({
    title: formData.get('title'),
    content: formData.get('content'),
  });

  if (!parsed.success) {
    return { errors: parsed.error.flatten().fieldErrors };
  }

  await db.post.create({ data: parsed.data });
  revalidatePath('/posts');
  return { success: true };
}

// app/blog/new/page.tsx
'use client';
import { useActionState } from 'react';
import { createPost } from '../actions';

export default function NewPostPage() {
  const [state, formAction, pending] = useActionState(createPost, {});

  return (
    <form action={formAction}>
      <input name="title" placeholder="Title" />
      {state.errors?.title && <span>{state.errors.title[0]}</span>}
      <textarea name="content" placeholder="Content" />
      <button type="submit" disabled={pending}>
        {pending ? 'Publishing...' : 'Publish'}
      </button>
    </form>
  );
}
カスタムフック
export function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(true);

  useEffect(() => {
    function handleOnline() { setIsOnline(true); }
    function handleOffline() { setIsOnline(false); }

    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);

    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);

  return isOnline;
}
緊急性の低い更新のための useTransition
function SearchableList({ items }: { items: Item[] }) {
  const [query, setQuery] = useState('');
  const [isPending, startTransition] = useTransition();
  const [filteredItems, setFilteredItems] = useState(items);

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setQuery(e.target.value);
    startTransition(() => {
      setFilteredItems(items.filter(i => i.name.toLowerCase().includes(e.target.value.toLowerCase())));
    });
  };

  return (
    <div>
      <input value={query} onChange={handleChange} />
      {isPending && <span>Filtering...</span>}
      <ul>{filteredItems.map(i => <li key={i.id}>{i.name}</li>)}</ul>
    </div>
  );
}

ベストプラクティス

Server と Client の選択
  • Server Component から始めます(ディレクティブは不要です)
  • 'use client' を追加するのは、フック、ブラウザー API、イベントハンドラーを使用する場合に限ります
状態管理
  • 状態は最小限に保ちます。派生値はエフェクト内ではなく、レンダリング中に計算します
  • 関連するアクションが複数ある状態には useReducer を使用します
  • 状態を最も近い共通の祖先へ引き上げます
エフェクト
  • エフェクトは外部システムとの同期にのみ使用します
  • 正しい依存配列を必ず指定します
  • サブスクリプションやタイマーにはクリーンアップ関数を返します
  • 状態を直接変更してはいけません。必ず新しい参照を作成します
パフォーマンス
  • React Compiler を使用する場合:手動の useMemo、useCallback、memo は避けます
  • React Compiler を使用しない場合:コストの高い計算には useMemo を、安定したコールバックには useCallback を使用します
  • 優先度の低い状態更新には useTransition を使用します
  • リストのキーには配列のインデックスではなく、安定した ID を使用します
React 19 固有の注意点
  • use(promise) を使うコンポーネントは Suspense 境界でラップします
  • フォームと Server Action の連携には useActionState を使用します
  • Server Action の入力を検証します。これらは公開エンドポイントです
  • Server Component から Client Component には、シリアライズ可能なデータを渡します

制約と警告

  • サーバーコンポーネント:フック、イベントハンドラー、ブラウザー API は使用できません
  • use() フック:レンダー中にのみ呼び出せます。コールバックやエフェクト内では呼び出せません
  • サーバーアクション:'use server' ディレクティブを含める必要があります。必ず入力を検証してください
  • 状態の変更:状態を直接変更しないでください。必ず新しい参照を作成してください
  • エフェクトの依存関係:useEffect の依存配列にすべての依存関係を含めてください
  • メモリーリーク:useEffect の戻り値で、サブスクリプションとイベントリスナーを必ずクリーンアップしてください

参考資料

詳しいパターンについては、以下のファイルを参照してください。

  • references/hooks-patterns.md — useState、useEffect、useRef、useReducer、カスタムフック、よくある落とし穴
  • references/component-patterns.md — Props、コンポジション、状態のリフトアップ、コンテキスト、複合コンポーネント、エラーバウンダリー
  • references/react19-features.md — use()、useOptimistic、useFormStatus、useActionState、サーバーアクション、サーバーコンポーネント、移行ガイド
  • references/performance-patterns.md — React Compiler のセットアップ、useMemo、useCallback、useTransition、useDeferredValue、遅延読み込み
  • references/typescript-patterns.md — 型付き props、ジェネリックコンポーネント、イベントハンドラー、判別可能なユニオン、コンテキストの型指定
  • references/learn.md — 基礎から React 19 の高度な内容までを段階的に学ぶガイド
  • references/reference.md — すべての React フックとコンポーネント API の完全な API リファレンス

コメント