デバッグ
この章では、Reactアプリケーションのデバッグ手法とReact Developer Toolsの使い方を学びます。
この章で学ぶこと
- console.logとconsoleのバリエーション活用
- React Developer Tools(Components、Profiler)
- Error Boundaryによるエラーハンドリング
- よくあるエラーの原因と対処法
この章では03章で作成したプロジェクトを使用します。npm run devで開発サーバーを起動し、コードを書きながら学習を進めましょう。
デバッグスキルは、コードを書く能力と同じくらい重要です。問題を素早く特定し解決できることが、開発効率を大きく左右します。この章では、効率的なデバッグに使えるツールと手法を扱います。
デバッグの基本
console.logの活用
console.logは軽視されがち?
console.logは「初心者向け」と思われがちですが、経験豊富なエンジニアも日常的に使用しています。高度なデバッガーを設定するより、適切な位置にログを追加する方が素早く問題を特定できることも多いです。ただし、本番コードにログを残さないよう注意してください。
最もシンプルなデバッグ方法です。
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState<User | null>(null)
console.log('UserProfile rendered, userId:', userId)
useEffect(() => {
console.log('Effect running, fetching user:', userId)
fetchUser(userId).then(data => {
console.log('User fetched:', data)
setUser(data)
})
return () => {
console.log('Cleanup, userId was:', userId)
}
}, [userId])
console.log('Current user state:', user)
return user ? <div>{user.name}</div> : <div>Loading...</div>
}
console のバリエーション
// グループ化
console.group('User Data')
console.log('Name:', user.name)
console.log('Email:', user.email)
console.groupEnd()
// テーブル表示
console.table(users)
// 条件付きログ
console.assert(user !== null, 'User should not be null')
// 警告・エラー
console.warn('This is deprecated')
console.error('Something went wrong')
// 時間計測
console.time('fetchData')
await fetchData()
console.timeEnd('fetchData') // fetchData: 234.56ms
React Developer Tools
React Developer Toolsは、Reactアプリケーションをデバッグするための必須ツールです。
インストール
Componentsタブ
コンポーネントツリーを視覚的に確認できます。
主な機能:
- コンポーネント階層の確認: ツリー構造で表示
- Props/Stateの確認: 選択したコンポーネントの値を表示
- 値の編集: Props/Stateをリアルタイムで変更
- コンポーネント検索: 名前でフィルタリング
- ソースコードへジャンプ:
<>アイコンをクリック
Components タブの見方:
App
├── Header
│ └── UserMenu
│ props: { user: {...} }
├── Main
│ ├── ProductList
│ │ state: { products: [...], loading: false }
│ │ └── ProductItem (×10)
│ └── Sidebar
└── Footer
Profilerタブ
レンダリングパフォーマンスを計測します。
使い方:
- 「Record」ボタンをクリック
- アプリを操作
- 「Stop」をクリック
- 結果を分析
表示される情報:
- 各コンポーネントのレンダリング時間
- なぜ再レンダリングされたか
- コミットごとの情報
Profiler の見方:
Commit 1 (32ms)
├── App (2ms)
│ rendered because: Hook 1 changed
├── ProductList (25ms)
│ rendered because: Props changed
└── Footer (0.5ms)
rendered because: Parent rendered
設定オプション
DevToolsの歯車アイコンから設定できます。
- Highlight updates: 再レンダリングされたコンポーネントをハイライト
- Hide components where...: 特定のコンポーネントを非表示
- Record why each component rendered: 再レンダリング理由を記録
ブレークポイントデバッグ
ブラウザのDevToolsを使用したデバッグです。
VSCodeでのデバッグ設定
// .vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Debug React App",
"url": "http://localhost:5173",
"webRoot": "${workspaceFolder}/src"
}
]
}
debugger文
コード内にブレークポイントを設置します。
function handleSubmit(e: React.FormEvent) {
e.preventDefault()
debugger // ここで実行が停止
const formData = new FormData(e.target as HTMLFormElement)
// ...
}
エラーハンドリング
Error Boundary
コンポーネントツリーの一部でエラーが発生した場合に、UIをクラッシュさせずにフォールバックを表示します。
import { Component, ReactNode } from 'react'
type ErrorBoundaryProps = {
children: ReactNode
fallback?: ReactNode
}
type ErrorBoundaryState = {
hasError: boolean
error: Error | null
}
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props)
this.state = { hasError: false, error: null }
}
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error }
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
// エラーログサービスに送信
console.error('Error caught by boundary:', error)
console.error('Component stack:', errorInfo.componentStack)
}
handleRetry = () => {
this.setState({ hasError: false, error: null })
}
render() {
if (this.state.hasError) {
return this.props.fallback ?? (
<div className="p-4 bg-red-100 text-red-700 rounded">
<h2>エラーが発生しました</h2>
<p>{this.state.error?.message}</p>
<button
onClick={this.handleRetry}
className="mt-2 px-4 py-2 bg-red-500 text-white rounded"
>
再試行
</button>
</div>
)
}
return this.props.children
}
}
// 使用例
function App() {
return (
<ErrorBoundary fallback={<div>問題が発生しました</div>}>
<UserProfile />
</ErrorBoundary>
)
}
Error Boundaryはクラスコンポーネントでのみ実装できます。関数コンポーネントではreact-error-boundaryライブラリを使用します。
react-error-boundary
npm install react-error-boundary
import { ErrorBoundary } from 'react-error-boundary'
function ErrorFallback({ error, resetErrorBoundary }: {
error: Error
resetErrorBoundary: () => void
}) {
return (
<div className="p-4 bg-red-100 text-red-700 rounded">
<h2>エラーが発生しました</h2>
<pre className="text-sm">{error.message}</pre>
<button
onClick={resetErrorBoundary}
className="mt-2 px-4 py-2 bg-red-500 text-white rounded"
>
再試行
</button>
</div>
)
}
function App() {
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onError={(error, info) => {
// エラーログを送信
logErrorToService(error, info)
}}
onReset={() => {
// 状態をリセット
}}
>
<MainContent />
</ErrorBoundary>
)
}
よくあるエラーと対処法
1. "Cannot read properties of undefined"
// ❌ エラーが発生する可能性
function UserProfile({ user }: { user: User | undefined }) {
return <p>{user.name}</p> // userがundefinedの場合にエラー
}
// ✅ オプショナルチェーンで対処
function UserProfile({ user }: { user: User | undefined }) {
return <p>{user?.name ?? 'ゲスト'}</p>
}
// ✅ 早期リターンで対処
function UserProfile({ user }: { user: User | undefined }) {
if (!user) {
return <p>ユーザー情報がありません</p>
}
return <p>{user.name}</p>
}
2. "Too many re-renders"
無限ループが発生しています。
// ❌ レンダリング中にsetStateを呼んでいる
function BadComponent() {
const [count, setCount] = useState(0)
setCount(count + 1) // 無限ループ!
return <p>{count}</p>
}
// ❌ useEffectの依存配列が不適切
function BadComponent() {
const [data, setData] = useState({})
useEffect(() => {
setData({ updated: true }) // dataが変わる → effectが再実行 → 無限ループ
}, [data])
}
// ✅ 正しい依存配列
function GoodComponent() {
const [data, setData] = useState({})
const [shouldUpdate, setShouldUpdate] = useState(false)
useEffect(() => {
if (shouldUpdate) {
setData({ updated: true })
setShouldUpdate(false)
}
}, [shouldUpdate])
}
3. "Each child in a list should have a unique key prop"
このエラーは見逃されがちですが、パフォーマンスやバグの原因になります。インデックスをkeyに使用すると、並び替えや削除時に意図しない挙動になることがあります。可能な限りユニークなIDを使用してください。
リストアイテムにkeyが必要です。
// ❌ keyがない
{items.map(item => (
<li>{item.name}</li>
))}
// ❌ インデックスをkeyに(並び替えで問題)
{items.map((item, index) => (
<li key={index}>{item.name}</li>
))}
// ✅ ユニークなIDをkeyに
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
4. "Cannot update a component while rendering"
// ❌ レンダリング中に親の状態を更新
function Child({ onMount }: { onMount: () => void }) {
onMount() // 親のsetStateを呼ぶと警告
return <div>Child</div>
}
// ✅ useEffectで更新
function Child({ onMount }: { onMount: () => void }) {
useEffect(() => {
onMount()
}, [onMount])
return <div>Child</div>
}
5. "Objects are not valid as a React child"
// ❌ オブジェクトをそのまま表示
function BadComponent({ user }: { user: User }) {
return <p>{user}</p> // [object Object]またはエラー
}
// ✅ プロパティを指定
function GoodComponent({ user }: { user: User }) {
return <p>{user.name}</p>
}
// ✅ JSON.stringifyでデバッグ表示
function DebugComponent({ data }: { data: unknown }) {
return <pre>{JSON.stringify(data, null, 2)}</pre>
}
StrictModeの活用
開発時に潜在的な問題を検出します。
// main.tsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>
)
StrictModeの効果:
- 副作用の検出: useEffectを2回実行して問題を検出
- 非推奨APIの警告: 古いAPIを使用していると警告
- 予期しない副作用の検出: レンダリング関数を2回実行
StrictModeによる二重実行は開発モードのみです。本番ビルドでは1回だけ実行されます。
ネットワークデバッグ
ブラウザのNetworkタブ
APIリクエストの確認に使用します。
Network タブの見方:
Name Status Type Size Time
/api/users 200 fetch 1.2 KB 234ms
/api/posts 404 fetch 128 B 156ms
リクエストのモック
開発時にAPIレスポンスをモックします。
// msw (Mock Service Worker) を使用
import { http, HttpResponse } from 'msw'
import { setupWorker } from 'msw/browser'
const handlers = [
http.get('/api/users', () => {
return HttpResponse.json([
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
])
}),
http.post('/api/users', async ({ request }) => {
const body = await request.json()
return HttpResponse.json({ id: 3, ...body }, { status: 201 })
})
]
const worker = setupWorker(...handlers)
worker.start()
デバッグのベストプラクティス
1. 問題を再現可能にする
// 再現手順を明確に
// 1. ログインする
// 2. 商品をカートに追加
// 3. チェックアウトボタンをクリック
// → エラーが発生
2. 問題を切り分ける
// コンポーネントを最小限にして問題を特定
function DebugComponent() {
const [data, setData] = useState(null)
// 最小限のコードで問題を再現
useEffect(() => {
fetchData().then(setData)
}, [])
return <pre>{JSON.stringify(data, null, 2)}</pre>
}
3. 二分探索
// コードの半分をコメントアウトして問題を絞り込む
function ComplexComponent() {
// 問題がある可能性のあるコード
const partA = useFeatureA()
// 以下をコメントアウトして切り分け
// const partB = useFeatureB()
// const partC = useFeatureC()
return <div>{/* ... */}</div>
}
4. ログを構造化する
const DEBUG = true
function debugLog(context: string, data: unknown) {
if (DEBUG) {
console.log(`[${context}]`, data)
}
}
function UserProfile({ userId }: { userId: string }) {
debugLog('UserProfile:render', { userId })
useEffect(() => {
debugLog('UserProfile:effect', { userId })
}, [userId])
return <div>...</div>
}
試してみよう
以下の課題に挑戦して、デバッグスキルを磨きましょう。
-
React Developer Tools を活用する: Componentsタブで、03章のプロジェクトのコンポーネントツリーを確認してください。任意のコンポーネントを選択し、PropsやStateの値をDevToolsから直接編集してみましょう。
-
console.timeで計測する: データフェッチ処理の実行時間を
console.time/console.timeEndで計測してみましょう。複数のAPI呼び出しがある場合、どれが最も時間がかかっているか特定してください。 -
Error Boundaryを実装する: 意図的にエラーを発生させるコンポーネントを作成し、Error BoundaryでキャッチしてフォールバックUIを表示してください。「再試行」ボタンで状態をリセットする機能も追加してみましょう。
ヒント
- Componentsタブでコンポーネントを選択後、右側のパネルでPropsやStateの値をダブルクリックすると編集できます。
console.time('fetchUsers')とconsole.timeEnd('fetchUsers')で同じラベルを使用します。非同期処理の前後に配置してください。- Error Boundaryはクラスコンポーネントで実装するか、
react-error-boundaryライブラリを使用します。resetErrorBoundary関数でエラー状態をリセットできます。
回答と解説
課題1: React Developer Tools を活用する
- ブラウザでアプリを開き、DevTools(F12)を起動
- 「Components」タブを選択
- コンポーネントツリーから任意のコンポーネントをクリック
- 右側パネルに表示されるPropsやStateの値をダブルクリック
- 新しい値を入力してEnterで確定
// 例:このコンポーネントのcountをDevToolsから直接変更できる
function Counter() {
const [count, setCount] = useState(0)
return <p>Count: {count}</p>
}
DevToolsからStateの値を直接編集すると、即座にUIに反映されます。これはデバッグ時に特定の状態を再現したい場合に役立ちます。また、コンポーネントを選択した状態で右側パネルの虫(bug)アイコン(Log this component data to the console)をクリックすると、コンソールにコンポーネントの詳細情報が出力されます。
課題2: console.timeで計測する
import { useState, useEffect } from 'react'
type User = { id: number; name: string }
type Post = { id: number; title: string }
function DataFetcher() {
const [users, setUsers] = useState<User[]>([])
const [posts, setPosts] = useState<Post[]>([])
useEffect(() => {
const fetchData = async () => {
// ユーザー取得の計測
console.time('fetchUsers')
const usersRes = await fetch('https://jsonplaceholder.typicode.com/users')
const usersData = await usersRes.json()
console.timeEnd('fetchUsers') // 例: fetchUsers: 234.56ms
// 投稿取得の計測
console.time('fetchPosts')
const postsRes = await fetch('https://jsonplaceholder.typicode.com/posts')
const postsData = await postsRes.json()
console.timeEnd('fetchPosts') // 例: fetchPosts: 189.23ms
setUsers(usersData)
setPosts(postsData)
}
// 全体の計測
console.time('totalFetch')
fetchData().then(() => {
console.timeEnd('totalFetch') // 例: totalFetch: 425.12ms
})
}, [])
return (
<div>
<h2>Users: {users.length}</h2>
<h2>Posts: {posts.length}</h2>
</div>
)
}
コンソールに各APIコールの実行時間が表示されます。どのAPIが最も時間がかかっているか特定でき、パフォーマンス改善の優先度を決める際に役立ちます。並列で取得したい場合はPromise.allを使用します。
課題3: Error Boundaryを実装する
import { Component, ReactNode, ErrorInfo, useState } from 'react'
// Error Boundary コンポーネント
type ErrorBoundaryProps = {
children: ReactNode
}
type ErrorBoundaryState = {
hasError: boolean
error: Error | null
}
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props)
this.state = { hasError: false, error: null }
}
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error }
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('Error caught:', error)
console.error('Component stack:', errorInfo.componentStack)
}
handleReset = () => {
this.setState({ hasError: false, error: null })
}
render() {
if (this.state.hasError) {
return (
<div className="p-4 bg-red-100 text-red-700 rounded">
<h2 className="font-bold">エラーが発生しました</h2>
<p className="text-sm mt-1">{this.state.error?.message}</p>
<button
onClick={this.handleReset}
className="mt-3 px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600"
>
再試行
</button>
</div>
)
}
return this.props.children
}
}
// 意図的にエラーを発生させるコンポーネント
function BuggyComponent() {
const [shouldError, setShouldError] = useState(false)
const handleTriggerError = () => {
setShouldError(true)
}
if (shouldError) {
throw new Error('意図的なエラーです!')
}
return (
<button
onClick={handleTriggerError}
className="px-4 py-2 bg-blue-500 text-white rounded"
>
エラーを発生させる
</button>
)
}
// 使用例
function App() {
return (
<ErrorBoundary>
<BuggyComponent />
</ErrorBoundary>
)
}
「エラーを発生させる」ボタンをクリックするとエラーが発生し、Error BoundaryがキャッチしてフォールバックUIを表示します。「再試行」ボタンをクリックするとhasErrorがfalseにリセットされ、BuggyComponentが再度レンダリングされます(shouldErrorもリセットされるため正常に表示されます)。
まとめ
console.logは最もシンプルで効果的なデバッグツール- React Developer Toolsでコンポーネントツリーとパフォーマンスを確認
- Error Boundaryでエラーを適切にハンドリング
- StrictModeで潜在的な問題を早期発見
- 問題を再現可能にし、切り分けて特定する
次の章では、クラスコンポーネントとError Boundaryについてさらに詳しく学びます。