tech-react について
React開発者向けに、コンポーネント、フック、Server/Client境界の実践パターンをまとめたガイドです。React 19の機能も扱い、Suspense、安定したリストキー、測定に基づく最適化を重視します。
主な機能
- 01フック呼び出しのルール
- 02Server/Clientコンポーネント境界
- 03Suspenseによる非同期処理
- 04安定したリストキー
- 05性能測定と最適化
活用シーン
- 01コンポーネントのフック使用を整理
- 02クライアント側のデータ取得をServer Componentsへ移行
- 03リスト並べ替え後の状態問題を調査
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ ravnhq/ ai-toolkit --skill tech-reactインストール後、Claude Code で/tech-reactを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
rules/_sections.mdcomponent-no-mutation-callbacks.mdcomponent-semantic-elements.mdcomponent-use-key-for-reset.mdeffect-cleanup.mdeffect-when-not-needed.mdhooks-rules.mdhooks-use-for-promises.mdperf-no-premature-memo.mdperf-stable-keys.mdserver-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() により、これらのパターンが改善されています。
ワークフロー
- コンポーネントの役割を特定する:コンポーネントを Server(データ処理が中心)にするか、Client(インタラクティブ)にするかを判断します
- フックをトップレベルに定義する:すべてのフックは、return 文より前に無条件で実行する必要があります
- 非同期処理には Suspense を使う:ローディング時のフォールバックを表示するため、
use()フックの呼び出しを Suspense 境界で囲みます - キーを使って構成する:リスト項目には安定した一意のキーを使います。コンポーネントの状態をリセットするには key prop を使います
- 最適化の前に測定する:メモ化を追加する前に、React DevTools でプロファイルします
- サブスクリプションをクリーンアップする:システムを購読するエフェクトでは、必ずクリーンアップ関数を返します
- コンポーネントを小さく保つ:インタラクティブな処理は 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>
);
}