mcpField
T
tech-react

tech-react

github.com/ravnhq開発1658 回インストール21 週間前に更新
リポジトリ

tech-react について

React開発者向けに、コンポーネント、フック、Server/Client境界の実践パターンをまとめたガイドです。React 19の機能も扱い、Suspense、安定したリストキー、測定に基づく最適化を重視します。

主な機能

  1. 01フック呼び出しのルール
  2. 02Server/Clientコンポーネント境界
  3. 03Suspenseによる非同期処理
  4. 04安定したリストキー
  5. 05性能測定と最適化

活用シーン

  1. 01コンポーネントのフック使用を整理
  2. 02クライアント側のデータ取得をServer Componentsへ移行
  3. 03リスト並べ替え後の状態問題を調査

インストール

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

$npx skills add https://github.com/ravnhq/ai-toolkit --skill tech-react

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

同梱ファイル

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

rules/
_sections.md
component-no-mutation-callbacks.md
component-semantic-elements.md
component-use-key-for-reset.md
effect-cleanup.md
effect-when-not-needed.md
hooks-rules.md
hooks-use-for-promises.md
perf-no-premature-memo.md
perf-stable-keys.md
server-client-boundaries.md

詳細

コアとなる課題

React のパターンは、メジャーバージョンごとに進化しています。React 19 では、Promise を処理するための use() フックが導入され、クライアント/サーバーのセマンティクスがより明確になったことで、Server Component の境界が正式に定義されました。よくある落とし穴は次のとおりです。

  • フックの呼び出し順序違反:条件付きでフックを呼び出すと、React の追跡システムが破綻します
  • 不要なメモ化:測定せずに useMemo/useCallback/React.memo を使うと、オーバーヘッドが増加します
  • 肥大化したコンポーネント:Server と Client のロジックを混在させると、ストリーミングの最適化が妨げられます
  • Suspense 境界の不足:Suspense を使わずに非同期データを処理すると、レンダー全体がブロックされます
  • 古いクロージャ:依存配列が正しくない、またはクリーンアップが不足しているエフェクト
  • キーの誤用:インデックスをキーにすると、並べ替え後に状態が別のリスト項目に紐づきます

React 19 では、Promise を使用するための use() フック、追加のリクエストなしでデータを取得できる Server Components、ブロックしない更新を行う useTransition() により、これらのパターンが改善されています。

ワークフロー

  1. コンポーネントの役割を特定する:コンポーネントを Server(データ処理が中心)にするか、Client(インタラクティブ)にするかを判断します
  2. フックをトップレベルに定義する:すべてのフックは、return 文より前に無条件で実行する必要があります
  3. 非同期処理には Suspense を使う:ローディング時のフォールバックを表示するため、use() フックの呼び出しを Suspense 境界で囲みます
  4. キーを使って構成する:リスト項目には安定した一意のキーを使います。コンポーネントの状態をリセットするには key prop を使います
  5. 最適化の前に測定する:メモ化を追加する前に、React DevTools でプロファイルします
  6. サブスクリプションをクリーンアップする:システムを購読するエフェクトでは、必ずクリーンアップ関数を返します
  7. コンポーネントを小さく保つ:インタラクティブな処理は Client Components に切り出し、データ処理は Server Components に任せます

ルール

詳細なパターンについては、ルールのインデックスを参照してください。

例

肯定的なトリガー

ユーザー:「この React コンポーネントをリファクタリングして、再レンダーを減らし、フックの使い方を明確にしてください。」

期待される動作:tech-react のガイダンスを使用し、そのワークフローに従って、実行可能な提案を返します。

肯定的なトリガー:Server Component の境界

ユーザー:「useEffect を使ってクライアント側でデータを取得しています。Server Components を使って、どのようにリファクタリングすればよいですか?」

期待される動作:データ取得を Server Component に移し、use() フックを介して Promise を Client Component に渡し、Suspense 境界で囲みます。

トリガーなし

ユーザー:「リリース成果物をパッケージ化する Bash スクリプトを書いてください。」

期待される動作:tech-react を優先せず、より関連性の高いスキルを選ぶか、それを使わずに対応します。

トラブルシューティング

フック呼び出し違反("Rendered more hooks than during the previous render")
  • エラー:React が "Rendered fewer/more hooks than during the previous render" をスローします
  • 原因:条件分岐、ループ、または早期 return の中でフックが呼び出されています
  • 解決策:条件ロジックより前にフック呼び出しを移動します。データフックの実行をスキップするには、enabled オプションを使います
Suspense のフォールバックが表示されない
  • エラー:use() がサスペンドしても、ローディング時のフォールバックが表示されません
  • 原因:use() を呼び出すコンポーネントが Suspense 境界で囲まれていません
  • 解決策:<Suspense> がコンポーネントの兄弟ではなく、親になっていることを確認します
Server Component で State/イベントを使えない
  • エラー:Server Components では useState や onClick ハンドラーが動作しません
  • 原因:Server Components はサーバー上で一度レンダーされるため、クライアントとのインタラクションに応答できません
  • 解決策:インタラクティブな部分を "use client" ディレクティブ付きの Client Components に切り出します。Server Component ではデータを取得し、Client Component に渡します
useEffect が 2 回実行される、またはメモリーリークが発生する
  • エラー:エフェクトの副作用が複数回実行され、クリーンアップが実行されません
  • 原因:依存配列がない、またはクリーンアップ関数を返していません
  • 解決策:依存配列を指定します。サブスクリプション、タイマー、リスナーにはクリーンアップ関数を返します
リスト項目のフォーカスが失われる、または順序が正しく表示されない
  • エラー:入力欄のフォーカスが行間で移動し、並べ替え後に項目の順序が乱れて表示されます
  • 原因:安定した一意の識別子ではなく、配列のインデックスをキーとして使っています
  • 解決策:key={index} を、データの一意なプロパティを使った key={item.id} に変更します

Examples: Error Patterns

Error 1: Conditional Hook

Incorrect:

function UserProfile({ userId }: { userId: string | null }) {
  if (!userId) {
    return <div>Select a user</div>;
  }

  // Hook called conditionally - React can't track it!
  const [profile, setProfile] = useState(null);
  return <div>{profile?.name}</div>;
}

Correct:

function UserProfile({ userId }: { userId: string | null }) {
  const [profile, setProfile] = useState(null);

  // Early return AFTER hooks
  if (!userId) {
    return <div>Select a user</div>;
  }

  return <div>{profile?.name}</div>;
}
Error 2: Suspense Without use() Hook

Incorrect:

function Comments({ id }: { id: string }) {
  const [comments, setComments] = useState([]);

  useEffect(() => {
    fetchComments(id).then(setComments);
  }, [id]);

  return <ul>{comments.map(c => <li key={c.id}>{c.text}</li>)}</ul>;
}

Correct:

function Comments({ commentsPromise }: { commentsPromise: Promise<Comment[]> }) {
  const comments = use(commentsPromise);
  return <ul>{comments.map(c => <li key={c.id}>{c.text}</li>)}</ul>;
}

function CommentsSection({ id }: { id: string }) {
  return (
    <Suspense fallback={<div>Loading comments...</div>}>
      <Comments commentsPromise={fetchComments(id)} />
    </Suspense>
  );
}
Error 3: Server/Client Boundary Confusion

Incorrect:

// BAD: Server Component with useState
async function NotesPage() {
  const [selectedNote, setSelectedNote] = useState(null); // ERROR: Can't use state
  const notes = await db.notes.getAll();

  return (
    <div>
      {notes.map(note => (
        <button onClick={() => setSelectedNote(note)}>
          {note.title}
        </button>
      ))}
    </div>
  );
}

Correct:

// Server Component - fetch data
async function NotesPage() {
  const notes = await db.notes.getAll();

  return (
    <div>
      <NotesList notes={notes} />
    </div>
  );
}

// Client Component - handle interaction
"use client";

function NotesList({ notes }: { notes: Note[] }) {
  const [selectedNote, setSelectedNote] = useState<Note | null>(null);

  return (
    <div>
      {notes.map(note => (
        <button
          key={note.id}
          onClick={() => setSelectedNote(note)}
        >
          {note.title}
        </button>
      ))}
    </div>
  );
}

コメント