react-patterns について
React 19、Vite、Cloudflare Workers のプロジェクト向けに、コンポーネント作成、コードレビュー、リファクタリングで使うパターンガイドです。性能、コンポジション、データ取得、環境固有の注意点を優先度順に扱います。
主な機能
- 01優先度順の性能ルール
- 02非同期リクエストのウォーターフォール削減
- 03バンドルサイズと再レンダーの管理
- 04コンポーネント構成と設計の指針
- 05React 19 固有のパターン
- 06Vite と Cloudflare の注意点
活用シーン
- 01React コンポーネントの作成やレビュー
- 02遅延や再レンダーの原因調査
- 03プロパティが多いコンポーネントのリファクタリング
- 04マージ前の変更確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ jezweb/ claude-skills --skill react-patternsインストール後、Claude Code で/react-patternsを入力して読み込まれたか確認します。
詳細
React パターン
React 19 + Vite + Cloudflare Workers プロジェクト向けのパフォーマンスとコンポジションのパターンです。新しいコンポーネントを書く際のチェックリスト、既存コードを監査する際のレビューガイド、または遅さや複雑さを感じたときのリファクタリング手順として活用してください。
ルールは影響度の高い順に並んでいます。MEDIUM の問題に取り組む前に、CRITICAL の問題を修正してください。
適用するタイミング
- React コンポーネントやページを新しく作成するとき
- パフォーマンスの問題がないかコードをレビューするとき
- props が多すぎる、または再レンダーが多いコンポーネントをリファクタリングするとき
- 「なぜ遅いのか?」「なぜ再レンダーされるのか?」をデバッグするとき
- 再利用可能なコンポーネントライブラリを構築するとき
- マージ前にコードレビューするとき
1. ウォーターフォールの解消(CRITICAL)
並列で実行できる処理を逐次実行する非同期呼び出しです。パフォーマンスを損なう最大の原因です。
| パターン | 問題 | 解決策 |
|---|---|---|
| 順次 await | const a = await getA(); const b = await getB(); | const [a, b] = await Promise.all([getA(), getB()]); |
| 子コンポーネントでのフェッチ | 親がレンダーされた後に子がフェッチし、さらにその後で孫がフェッチする | フェッチを最も近い共通の祖先に引き上げ、データを子へ渡す |
| Suspense の連鎖 | 複数の Suspense 境界が順番に解決される | 非同期の兄弟コンポーネントすべてを 1 つの Suspense 境界で囲む |
| 分岐前の await | const data = await fetch(); if (condition) { use(data); } | await を分岐の中に移動する。使わない可能性のあるデータはフェッチしない |
| インポート後にレンダー | const Component = await import('./Heavy'); return <Component /> | React.lazy() + <Suspense> を使う。フォールバックを即座にレンダーする |
見つけ方: コンポーネント内で await を検索してください。それぞれの await はウォーターフォールの可能性があります。2 つの await が独立している場合は、並列で実行してください。
2. バンドルサイズ(CRITICAL)
ユーザーがダウンロードする 1 KB ごとに、待ち時間が発生します。
| パターン | 問題 | 解決策 |
|---|---|---|
| バレルインポート | import { Button } from '@/components' はバレルファイル全体を読み込む | import { Button } from '@/components/ui/button' — 直接インポートする |
| コード分割なし | 重いコンポーネントがすべてのページで読み込まれる | React.lazy(() => import('./HeavyComponent')) + <Suspense> |
| 起動時のサードパーティライブラリ | アプリのレンダー前に分析やトラッキングが読み込まれる | ハイドレーション後に読み込む: useEffect(() => { import('./analytics') }, []) |
| ライブラリ全体のインポート | import _ from 'lodash'(70KB) | import debounce from 'lodash/debounce'(1KB) |
| Lucide のツリーシェイキング | import * as Icons from 'lucide-react'(すべてのアイコン) | 明示的な指定: import { Home, Settings } from 'lucide-react' |
| React の重複 | ライブラリが独自の React をバンドルしている → 「Cannot read properties of null」 | vite.config.ts に resolve.dedupe: ['react', 'react-dom'] を設定する |
見つけ方: npx vite-bundle-visualizer — バンドルに含まれる内容を確認できます。
3. コンポジションアーキテクチャ(HIGH)
コンポーネントの構成方法は、最適化の方法よりも重要です。
| パターン | 問題 | 解決策 |
|---|---|---|
| boolean props の増加 | <Card isCompact isClickable showBorder hasIcon isLoading> | 明示的なバリアントを使う: <CompactCard>、<ClickableCard> |
| 複合コンポーネント | 15 個の props を持つ複雑なコンポーネント | <Dialog>、<Dialog.Trigger>、<Dialog.Content> に分割し、コンテキストを共有する |
| renderX props | <Layout renderSidebar={...} renderHeader={...} renderFooter={...}> | children と名前付きスロットを使う: <Layout><Sidebar /><Header /></Layout> |
| 状態のリフトアップ | 兄弟コンポーネント間で状態を共有できない | 状態を親またはコンテキストプロバイダーに移動する |
| プロバイダーの実装 | コンシューマーのコードが状態管理の内部実装を知っている | プロバイダーはインターフェース { state, actions, meta } を公開し、実装は隠す |
| インラインコンポーネント | function Parent() { function Child() { ... } return <Child /> } | Child を Parent の外で定義する。インラインコンポーネントはレンダーのたびに再マウントされる |
判断基準: コンポーネントに boolean props が 5 個を超えてある場合は、props を増やすのではなくコンポジションが必要です。
4. 再レンダリングの防止(中)
再レンダリングがすべて悪いわけではありません。目に見えるカクつきや無駄な計算を引き起こす再レンダリングだけを修正してください。
| パターン | 問題 | 解決策 |
|---|---|---|
| デフォルトのオブジェクト/配列の props | function Foo({ items = [] }) → レンダリングのたびに新しい配列参照が作られます | 外部に定義します:const DEFAULT = []; function Foo({ items = DEFAULT }) |
| effect 内の派生 state | useEffect(() => setFiltered(items.filter(...)), [items]) | レンダリング中に派生させます:const filtered = useMemo(() => items.filter(...), [items]) |
| オブジェクトの依存関係 | config が {} の場合、useEffect(() => {...}, [config]) はレンダリングのたびに実行されます | プリミティブ値を依存関係に使います:useEffect(() => {...}, [config.id, config.type]) |
| 使用しない state の購読 | コンポーネントは { user, theme, settings } を読み取りますが、使用するのは user だけです | コンテキストを分割するか、セレクターを使います:useSyncExternalStore |
| 一時的な値に state を使う | マウス移動時に const [mouseX, setMouseX] = useState(0) を使います | 頻繁に変化するものの、再レンダリングが不要な値には useRef を使います |
| インラインのコールバック props | <Button onClick={() => doThing(id)} /> — レンダリングのたびに新しい関数が作られます | useCallback または関数型の setState を使います:<Button onClick={handleClick} /> |
見つけ方:React DevTools Profiler の「Why did this render?」を確認するか、開発環境で<React.StrictMode>による二重レンダリングを確認します。
5. React 19 固有の項目(中)
React 19 で変更された、または新しく追加されたパターンです。
| パターン | 旧(React 18) | 新(React 19) |
|---|---|---|
| フォームの state | useFormState | useActionState — 名前が変更されました |
| ref の転送 | forwardRef((props, ref) => ...) | function Component({ ref, ...props }) — ref は通常の prop です |
| コンテキスト | useContext(MyContext) | use(MyContext) — 条件分岐やループ内でも使えます |
| 保留中の UI | 手動でローディング state を管理 | 緊急性の低い更新にはuseTransitionとstartTransitionを使います |
| ルートレベルの遅延読み込み | createBrowserRouterでのみ動作します | これは変わりません — <BrowserRouter>では<Route lazy={...}>は何も通知されずに無視されます |
| 楽観的更新 | 手動で state を管理 | useOptimisticフック |
| メタデータ | Helmet または手動で<head>を管理 | コンポーネントの JSX に<title>、<meta>、<link>を記述すると、自動的に<head>へ移動されます |
6. レンダリングのパフォーマンス(中)
| パターン | 問題 | 解決策 |
|---|---|---|
| 読み込み時のレイアウトシフト | 非同期データの到着時にコンテンツが跳ねます | 最終的なレイアウト寸法に合わせたスケルトン画面 |
| SVG を直接アニメーションする | SVG アニメーションがカクつきます | <div>で囲み、代わりに div をアニメーションさせます |
| 大きなリストのレンダリング | テーブルやリストに 1000 件以上の項目があります | 仮想化レンダリングには@tanstack/react-virtualを使います |
| content-visibility | スクロール可能な長いコンテンツが最初にすべてレンダリングされます | 画面外のセクションにcontent-visibility: autoを指定します |
| && による条件付きレンダリング | {count && <Items />}は count が 0 の場合に0をレンダリングします | 三項演算子を使います:{count > 0 ? <Items /> : null} |
7. データ取得(中)
| パターン | 問題 | 解決策 |
|---|---|---|
| 重複排除がない | 3 つのコンポーネントが同じデータを取得します | TanStack Query または SWR — 自動的に重複排除とキャッシュを行います |
| マウント時に取得する | useEffect(() => { fetch(...) }, []) — ウォーターフォールが発生し、キャッシュも重複排除もありません | TanStack Query を使います:useQuery({ queryKey: ['users'], queryFn: fetchUsers }) |
| 楽観的更新がない | ユーザーが保存をクリックしてから、変更が表示されるまで 2 秒待ちます | onMutateを指定したuseMutationで、視覚的なフィードバックを即座に表示します |
| インターバル内の古いクロージャ | setIntervalが古い state を捕捉します | インターバル ID と現在の値にはuseRefを使います |
| クリーンアップなしのポーリング | useEffect 内のsetIntervalにclearIntervalがありません | クリーンアップ関数を返します:useEffect(() => { const id = setInterval(...); return () => clearInterval(id); }) |
8. Vite + Cloudflare 固有の項目(中)
| パターン | 問題 | 修正方法 |
|---|---|---|
Node スクリプト内の import.meta.env | 未定義です。Vite で処理されたファイル内でのみ動作します | vite の loadEnv() を使用します |
| React の重複インスタンス | ライブラリが独自の React をバンドルしています | vite.config.ts で resolve.dedupe + optimizeDeps.include を設定します |
| Radix Select の空文字列 | <SelectItem value=""> はエラーになります | センチネル値を使用します:<SelectItem value="__any__"> |
| React Hook Form の null | {...field} は Input に null を渡します | 手動でスプレッドします:value={field.value ?? ''} |
| エッジでの環境変数 | Workers では process.env が存在しません | c.env(Hono コンテキスト)または import.meta.env(Vite のビルド時)を使用します |
レビューチェックリストとして使用する
コードをレビューする際は、すべての PR でカテゴリ 1〜3(重大 + 高)を確認します。パフォーマンスが懸念される場合にのみ、カテゴリ 4〜8 を確認します。
/react-patterns [file or component path]
ファイルを読み、優先度順にルールと照らし合わせて確認し、次の形式で指摘事項を報告します:
file:line — [rule] description of issue