メインコンテンツまでスキップ

Cursor を使用した React のデバッグ:Hooks、状態、パフォーマンス

Cursor React Debugging

React アプリケーションは、状態管理、hooks の依存関係、レンダリングの最適化により、複雑なバグが発生する可能性があります。Cursor の AI 機能は、状態フローの追跡、不要な再レンダリングの特定、hooks の問題のデバッグに役立ちます。このガイドでは、一般的な React デバッグシナリオについて説明します。

前提条件​

開始する前に:

  • Cursor をインストール(最新バージョンを推奨)
  • React DevTools ブラウザ拡張機能
  • React hooks(useState、useEffect、useMemo)の基礎知識
  • React のレンダリングサイクルの理解

デバッグ環境のセットアップ​

推奨される Cursor 設定​

// .vscode/settings.json
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"typescript.preferences.importModuleSpecifier": "relative",
"editor.quickSuggestions": {
"strings": true
}
}

React DevTools 統合​

  1. ブラウザに React DevTools をインストール
  2. Cursor でプロジェクトを開く
  3. Components タブを使用してコンポーネントツリーを検査
  4. Profiler タブを使用してレンダリングパフォーマンスを測定

React Hooks のデバッグ​

useEffect の依存関係の問題​

最も一般的な hooks のエラーは、依存関係の欠落に関連しています:

// ❌ 誤り:依存関係が不足
useEffect(() => {
fetchUser(userId);
}, []); // eslint-disable-line react-hooks/exhaustive-deps

Cursor に修正させる:

この useEffect に欠落している依存関係の警告があります。無限ループを避けながら修正してください。

Cursor の提案:

// ✅ 正しい:すべての依存関係を含める
useEffect(() => {
fetchUser(userId);
}, [userId]); // userId が含まれている

カスタム Hooks のデバッグ​

// src/hooks/useLocalStorage.ts
import { useState, useEffect, useCallback } from 'react';

export function useLocalStorage<T>(key: string, initialValue: T) {
// レンダリングごとに localStorage にアクセスしないよう遅延初期化
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(`Error reading localStorage key "${key}":`, error);
return initialValue;
}
});

const setValue = useCallback(
(value: T | ((val: T) => T)) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(`Error setting localStorage key "${key}":`, error);
}
},
[key, storedValue]
);

return [storedValue, setValue] as const;
}

状態フローのデバッグ​

Prop Drilling の追跡​

データが複数の層を通過する場合、Cursor を使用して Context にリファクタリングします:

// ❌ 変更前:Prop Drilling
function App() {
const [user, setUser] = useState(null);
return <Layout user={user} setUser={setUser} />;
}

function Layout({ user, setUser }) {
return <Header user={user} setUser={setUser} />;
}

// ✅ 変更後:Context の使用
const UserContext = createContext(null);

function App() {
const [user, setUser] = useState(null);
return (
<UserContext.Provider value={{ user, setUser }}>
<Layout />
</UserContext.Provider>
);
}

Context コンシューマーのデバッグ​

// useContextSelector を使用した細粒度の購読
import { createContext, useContext, useCallback } from 'react';

const ThemeContext = createContext(null);

export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
}

// コンポーネント内
function ThemedButton() {
const { theme, toggleTheme } = useTheme();

// theme が変更されたときのみ再レンダリング、Context 内のすべてではない
return (
<button
style={{ background: theme.primary }}
onClick={toggleTheme}
>
Toggle Theme
</button>
);
}

パフォーマンスのデバッグ​

不要なレンダリングの特定​

React DevTools Profiler の使用:

  1. Profiler タブを開く
  2. Record(記録)をクリック
  3. アプリケーションと対話
  4. 記録を停止してフレームグラフを分析

memo を使用した最適化​

import { memo, useMemo, useCallback } from 'react';

// 不要なレンダリングを防ぐためのコンポーネントラップ
const ExpensiveList = memo(function ExpensiveList({ items, onSelect }) {
return (
<ul>
{items.map(item => (
<ListItem key={item.id} item={item} onSelect={onSelect} />
))}
</ul>
);
});

// 親コンポーネント内
function Parent() {
const [items, setItems] = useState([]);
const [filter, setFilter] = useState('');

// フィルタリングされたリストをメモ化
const filteredItems = useMemo(
() => items.filter(item => item.name.includes(filter)),
[items, filter]
);

// 参照の等価性を維持するためにコールバックをメモ化
const handleSelect = useCallback((id: string) => {
console.log('Selected:', id);
}, []);

return (
<ExpensiveList items={filteredItems} onSelect={handleSelect} />
);
}

useMemo と useCallback のガイド​

Hook用途例
useMemo高価な計算大きな配列のフィルタリング/ソート
useCallback子コンポーネントにコールバックを渡すイベントハンドラ
memo純粋なコンポーネントの最適化大きなリスト項目のレンダリング
過度の最適化

useMemo/useCallback を追加する前にパフォーマンスをプロファイリングしてください。Cursor は実際のパフォーマンスボトルネックを特定するのに役立ちます。

一般的な React エラー​

無限ループ​

Too many re-renders. React limits the number of renders to prevent an infinite loop.

原因:レンダリング中に直接状態を設定

// ❌ 誤り:レンダリング中に状態を設定
function BadComponent() {
const [count, setCount] = useState(0);
setCount(count + 1); // 無限ループを引き起こす
return <div>{count}</div>;
}

// ✅ 正しい:イベントハンドラで状態を設定
function GoodComponent() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
);
}

古いクロージャ​

function Timer() {
const [count, setCount] = useState(0);

useEffect(() => {
const interval = setInterval(() => {
// ❌ 誤り:'count' は常に 0
console.log(count);
setCount(count + 1);
}, 1000);

return () => clearInterval(interval);
}, []); // count 依存関係が不足

// ✅ 正しい:関数型更新を使用
useEffect(() => {
const interval = setInterval(() => {
setCount(c => c + 1); // 常に最新の値を使用
}, 1000);

return () => clearInterval(interval);
}, []);

return <div>{count}</div>;
}

Hydration の不一致​

Warning: Text content did not match. Server: "..." Client: "..."

Cursor を使用して SSR の問題を修正:

// クライアントでのみレンダリングされる動的コンテンツ
function ClientDate() {
const [date, setDate] = useState<string | null>(null);

useEffect(() => {
setDate(new Date().toLocaleDateString());
}, []);

if (!date) return <span>Loading...</span>;
return <span>{date}</span>;
}

非同期操作のデバッグ​

データフェッチパターン​

// src/hooks/useApi.ts
import { useState, useEffect, useRef } from 'react';

export function useApi<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const abortController = useRef<AbortController | null>(null);

useEffect(() => {
abortController.current = new AbortController();

async function fetchData() {
try {
setLoading(true);
setError(null);

const response = await fetch(url, {
signal: abortController.current?.signal,
});

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}

const result = await response.json();
setData(result);
} catch (err) {
if (err.name !== 'AbortError') {
setError(err);
}
} finally {
setLoading(false);
}
}

fetchData();

return () => {
abortController.current?.abort();
};
}, [url]);

return { data, loading, error };
}

Cursor を使用したエラーバウンダリ​

// src/components/ErrorBoundary.tsx
import { Component, ErrorInfo, ReactNode } from 'react';

interface Props {
children: ReactNode;
fallback?: ReactNode;
}

interface State {
hasError: boolean;
error: Error | null;
}

export class ErrorBoundary extends Component<Props, State> {
state: State = { hasError: false, error: null };

static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}

componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('ErrorBoundary caught:', error, errorInfo);
// 監視サービスにエラーを送信
}

render() {
if (this.state.hasError) {
return (
this.props.fallback || (
<div role="alert">
<h2>Something went wrong</h2>
<details>
<summary>Error details</summary>
<pre>{this.state.error?.message}</pre>
</details>
<button onClick={() => this.setState({ hasError: false })}>
Try again
</button>
</div>
)
);
}

return this.props.children;
}
}

クイックデバッグチェックリスト​

問題確認項目
コンポーネントが過剰にレンダリングされるReact DevTools Profiler
状態が更新されないuseEffect の依存関係
Context の値が古いProvider の値の参照
メモリリークuseEffect のクリーンアップ関数
Hydration エラークライアント/サーバーのコンテンツ差異
無限ループレンダリング中の状態更新

関連リソース​

最終更新:2025 年 7 月