404 Not Found

404 Not Found


nginx

useEffect とライフサイクル

useStateがコンポーネントの「メモリ」だとすれば、useEffectはその「カメラ」に相当します。これにより、コンポーネントが画面にレンダリングされた後(「写真を撮った後」)、データの取得、イベントの購読、DOMの操作、タイマーの設定といった追加の操作を行うことができます。


1. 学習内容



2. データ読み込みページの物語

(1) 課題:データ要求により無限ループが発生する

アリスは、コンポーネントの読み込み時にユーザーデータを取得したいと考えているため、次のように記述します。

JSX
function UserProfile({ userId }) {
  const [user, setUser] = React.useState(null)

  // ❌ Error:Requesting data directly within a component's function body
  fetch('/api/users/' + userId)
    .then(res => res.json())
    .then(data => setUser(data))

  return <div>{user?.name}</div>
}

質問

  1. fetchsetUser(data) → コンポーネントの再レンダリング
  2. 再度レンダリング → fetchを再度実行 → ステータスを再度設定 → 再度レンダリング...
  3. 無限ループ! ページがフリーズしています。

アリスは、「コンポーネントがマウントされたときに一度だけ実行される」方法が必要です。

(2) useEffect による解決策

JSX
import { useState, useEffect } from 'react'

function UserProfile({ userId }) {
  const [user, setUser] = useState(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState(null)

  // ✅ Correct:useEffect + Empty array of dependencies
  useEffect(() => {
    fetch('/api/users/' + userId)
      .then(res => {
        if (!res.ok) throw new Error('Failed to load')
        return res.json()
      })
      .then(data => {
        setUser(data)
        setLoading(false)
      })
      .catch(err => {
        setError(err.message)
        setLoading(false)
      })
  }, [userId])  // Only at userId Re-execute when changes occur

  if (loading) return <p>Loading......</p>
  if (error) return <p>Error:{error}</p>
  return <h1>{user.name}</h1>
}

メリットuseEffect 副作用をレンダリングロジックから分離する――レンダリングはUIのみを担当し、副作用はレンダリング完了後に実行される。



3. useEffect の基礎

(1) 基本的な構文

JSX
useEffect(() => {
  // Side Effect Logic(Execute after rendering is complete)
  console.log('Component rendering complete!')
  
  return () => {
    // Cleanup Function(Run before unloading or re-executing a component)
    console.log('Addressing Side Effects')
  }
}, [Dependency Array])
セクション 説明
() => { ... } レンダリングが画面に反映されたに実行される副作用コールバック
return () => { ... } クリーンアップ関数。コンポーネントのアンインストールや依存関係の変更により再実行が行われる前に実行される
[dependencies] useEffect が再実行されるタイミングを制御する配列
100%
graph TB
    A[Component Rendering] --> B[React Submit DOM Update]
    B --> C[useEffect Callback Execution]
    C --> D{Changes in Dependencies?}
    D -->|is | E[Execute the previous cleanup function]
    E --> F[Re-execute useEffect]
    D -->|No| G[Skip]
    F --> H{Component Uninstallation?}
    G --> H
    H -->|is | I[Execute the cleanup function]
    
    style C fill:#61dafb,color:#000
    style I fill:#ff6b6b,color:#fff

(2) 依存関係配列の3つのパターン

JSX
// Pattern 1:Empty array [] — Execute only once when mounted
useEffect(() => {
  console.log('Component Mounting')
  fetch('/api/init').then(setData)
}, [])  // Never re-execute

// Pattern 2:Dependencies [a, b] — Re-execute when dependencies change
useEffect(() => {
  console.log('userId It has changed.:', userId)
  fetch('/api/users/' + userId).then(setUser)
}, [userId])  // userId Re-execute when changes occur

// Pattern 3:No array dependencies — Executed on every render
useEffect(() => {
  console.log('Executed on every render')
})  // Note:No array!
依存関係の配列 実行タイミング ユースケース
[] マウント時のみ データ要求とサブスクリプションの初期化
[dep1, dep2] マウント時 + 依存関係が変更されたとき パラメータに応じてデータが変更される
無数のセット レンダリングのたびに ほとんど使われない(通常は他の手法に置き換えられる)


4. クリーンアップ関数

クリーンアップ関数は、メモリリークやレースコンディションを防ぐ上で重要な役割を果たします。

(1) タイマーのリセット

JSX
function Timer() {
  const [seconds, setSeconds] = useState(0)

  useEffect(() => {
    const timer = setInterval(() => {
      setSeconds(s => s + 1)
    }, 1000)

    // ✅ Clear the timer when the component is unloaded
    return () => {
      clearInterval(timer)
      console.log('The timer has been cleared.')
    }
  }, [])  // Start only when mounted

  return <p>Running: {seconds}s</p>
}
// If you don't clean it,:The timer continues to run even after the component is unloaded,Error "Update on unmounted components"

(2) イベントリスナーの整理

JSX
function WindowSize() {
  const [size, setSize] = useState({ width: window.innerWidth, height: window.innerHeight })

  useEffect(() => {
    function handleResize() {
      setSize({ width: window.innerWidth, height: window.innerHeight })
    }

    window.addEventListener('resize', handleResize)

    // ✅ Remove event listeners when the component is unmounted
    return () => {
      window.removeEventListener('resize', handleResize)
    }
  }, [])

  return <p>Window:{size.width} × {size.height}</p>
}

(3) リクエストのキャンセル(レースコンディションを回避するため)

JSX
function SearchResults({ query }) {
  const [results, setResults] = useState([])

  useEffect(() => {
    let cancelled = false

    fetch('/api/search?q=' + query)
      .then(res => res.json())
      .then(data => {
        // ✅ If the component has been uninstalled or query Has changed,Do not update status
        if (!cancelled) {
          setResults(data)
        }
      })

    // ✅ Cleanup:Uncheck
    return () => {
      cancelled = true
    }
  }, [query])

  return <div>{results.map(r => <p key={r.id}>{r.name}</p>)}</div>
}
// When users quickly switch search terms:
// 1. Search"A" → useEffect Execute
// 2. Search"AB" → Clear Function Markers cancelled=true → useEffect Re-execute
// 3. Search"A" the response came back → cancelled=true → Do not update status(Avoid flashing) 

(1) ▶ サンプル:クリーンアップ関数の包括的な実演

JSX
// ============================================
// Example:useEffect Declaration Cycle Demonstration
// Features:Start the timer when mounted + Listening for Keyboard Events
//       Clean up all resources upon uninstallation
// ============================================

function LifecycleDemo() {
  const [count, setCount] = useState(0)
  const [logs, setLogs] = useState([])

  // Effect 1:Title Update
  useEffect(() => {
    document.title = `Count:${count}`
  }, [count])

  // Effect 2:Timer
  useEffect(() => {
    const id = setInterval(() => {
      setCount(c => c + 1)
    }, 2000)

    addLog('⏱️ The timer has started')

    return () => {
      clearInterval(id)
      addLog('⏱️ The timer has been cleared.')
    }
  }, [])

  // Effect 3:Keyboard Events
  useEffect(() => {
    function handleKey(e) {
      if (e.key === ' ') {
        setCount(c => c + 5)
      }
    }
    window.addEventListener('keydown', handleKey)

    return () => {
      window.removeEventListener('keydown', handleKey)
      addLog('⌨️ Keyboard listening has been removed')
    }
  }, [])

  function addLog(msg) {
    setLogs(prev => [...prev, `${new Date().toLocaleTimeString()} ${msg}`])
  }

  return (
    <div>
      <h2>useEffect Life Cycle Demonstration</h2>
      <p>Count:{count} (+1 every 2s, Space +5)</p>
      <div style={{ maxHeight: '200px', overflow: 'auto', backgroundColor: '#f5f5f5', padding: '10px', borderRadius: '4px', fontSize: '12px', fontFamily: 'monospace' }}>
        {logs.map((log, i) => <div key={i}>{log}</div>)}
      </div>
    </div>
  )
}
▶ 試してみよう

5. useEffect をコンポーネントのライフサイクルに紐付ける

クラスコンポーネントの時代には、3つのライフサイクルフックがありましたが、フックの時代では、useEffect—1つのフックで3つの役割を果たします:

100%
graph LR
    subgraph "The Lifecycle of Class Components"
        A1[componentDidMount] --> A2[componentDidUpdate]
        A2 --> A3[componentWillUnmount]
    end
    
    subgraph "Hooks 's  useEffect"
        B1["useEffect(fn, [])<br/>Execute when mounted"] 
        B2["useEffect(fn, [dep])<br/>Mount + run when deps change"]
        B3["return cleanup<br/>Clean up before uninstalling or rerunning"]
    end
クラスのコンポーネントのライフサイクル 対応する useEffect
componentDidMount useEffect(() => { ... }, [])
componentDidUpdate useEffect(() => { ... })(無数のセット)または useEffect(() => { ... }, [dep])
componentWillUnmount のクリーンアップ関数 useEffect(() => { return () => { ... } }, [])

(1) ▶ サンプル:つのuseEffectを使用して複数のライフサイクルフェーズを処理する

JSX
// ============================================
// Example:Chat Room Component——One useEffect Covering the Entire Lifecycle
// ============================================

function ChatRoom({ roomId }) {
  const [messages, setMessages] = useState([])

  useEffect(() => {
    // === componentDidMount + componentDidUpdate ===
    console.log(`🟢 Connect to the chat room:${roomId}`)
    const connection = createConnection(roomId)
    connection.connect()

    connection.on('message', (msg) => {
      setMessages(prev => [...prev, msg])
    })

    // === componentWillUnmount + Clean Up Before Re-Execution ===
    return () => {
      console.log(`🔴 Leave the chat room:${roomId}`)
      connection.disconnect()
    }
  }, [roomId])  // roomId When things change:Disconnect the old connection → Join a New Chat Room

  return (
    <div>
      <h3>Chat Room:{roomId}</h3>
      {messages.map((msg, i) => <p key={i}>{msg}</p>}
    </div>
  )
}

// Usage
<ChatRoom roomId="general" />     // Connect general Chat Room
<ChatRoom roomId="random" />      // roomId Changes → Disconnect general → Connect random
// When a component is unloaded → Disconnect the current connection(Execution of the cleanup function)
▶ 試してみよう

6. useEffect と useLayoutEffect の違い

React には 2 つの「副作用」フックが用意されていますが、これらは実行タイミングが異なります。

100%
graph LR
    A[Trigger Rendering] --> B[Computational Virtual DOM]
    B --> C[DOM Update]
    C --> D{useLayoutEffect?}
    D -->|is | E[Execute in parallel<br/>Blocking Browser Rendering]
    D -->|No| F[Browser rendering complete]
    F --> G[useEffect Asynchronous Execution]
    
    style E fill:#faad14,color:#000
    style G fill:#52c41a,color:#fff
フック 実行タイミング レンダリングをブロックするか 使用例
useEffect ブラウザのレンダリング後(非同期) 90%のシナリオ:データリクエスト、イベントリスナー、ロギング
useLayoutEffect DOMの更新後、ブラウザによるレンダリングの(同期) DOMの寸法を測定し、レイアウトを読み取り、アニメーションを同期させる
JSX
import { useState, useEffect, useLayoutEffect } from 'react'

function MeasureExample() {
  const [width, setWidth] = useState(0)
  const ref = useRef(null)

  // ❌ useEffect Read DOM Dimensions → Users may notice flickering
  useEffect(() => {
    setWidth(ref.current.offsetWidth)  // Read After Drawing → May trigger additional rendering
  }, [])

  // ✅ useLayoutEffect → Read Before Drawing,Transparent to the user
  useLayoutEffect(() => {
    setWidth(ref.current.offsetWidth)
  }, [])

  return <div ref={ref}>Width:{width}px</div>
}

ルール:まずは useEffect と入力してください。「ちらつき」や「レイアウトの乱れ」といった問題が発生した場合にのみ、useLayoutEffect に切り替えてください。



7. 完全な例:データダッシュボード

JSX
// ============================================
// Complete Example:Data Dashboard(useEffect Integrated Applications)
// Features:Refresh data at regular intervals + Window Adaptation + Resource Cleanup
// ============================================

import { useState, useEffect } from 'react'

function Dashboard() {
  const [data, setData] = useState(null)
  const [isLive, setIsLive] = useState(true)
  const [lastUpdate, setLastUpdate] = useState(null)
  const [windowSize, setWindowSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight
  })

  // Effect 1:First Load + Refresh at set intervals
  useEffect(() => {
    function fetchData() {
      // Simulation API Request
      const mockData = {
        users: Math.floor(Math.random() * 1000) + 500,
        orders: Math.floor(Math.random() * 200) + 100,
        revenue: Math.floor(Math.random() * 50000) + 10000,
        growth: (Math.random() * 20 - 5).toFixed(1)
      }
      setData(mockData)
      setLastUpdate(new Date().toLocaleTimeString())
    }

    // Execute once immediately
    fetchData()

    // Refresh at set intervals
    let interval
    if (isLive) {
      interval = setInterval(fetchData, 5000)
    }

    return () => {
      clearInterval(interval)
    }
  }, [isLive])  // isLive Reset the timer when a change occurs

  // Effect 2:Window Size Monitoring
  useEffect(() => {
    function handleResize() {
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight
      })
    }

    window.addEventListener('resize', handleResize)
    return () => window.removeEventListener('resize', handleResize)
  }, [])

  // Effect 3:Page Title
  useEffect(() => {
    document.title = `Dashboard - ${data ? `${data.users} User` : 'Loading......'}`
  }, [data])

  // Effect 4:Leave Page Prompt
  useEffect(() => {
    function handleBeforeUnload(e) {
      if (isLive) {
        e.preventDefault()
        e.returnValue = ''
      }
    }
    window.addEventListener('beforeunload', handleBeforeUnload)
    return () => window.removeEventListener('beforeunload', handleBeforeUnload)
  }, [isLive])

  return (
    <div>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
        <h2>📊 Real-Time Dashboard</h2>
        <div>
          <span style={{ color: '#666', fontSize: '14px', marginRight: '12px' }}>
            Window:{windowSize.width}×{windowSize.height}
          </span>
          <label>
            <input type="checkbox" checked={isLive} onChange={e => setIsLive(e.target.checked)} />
            Refreshes in real time
          </label>
        </div>
      </div>

      {!data ? (
        <p style={{ textAlign: 'center', padding: '40px', color: '#999' }}>
          ⏳ Loading......
        </p>
      ) : (
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '16px' }}>
          <StatCard label="Total Users" value={data.users.toLocaleString()} color="#1890ff" />
          <StatCard label="Today's Orders" value={data.orders.toLocaleString()} color="#52c41a" />
          <StatCard label="Monthly Income" value={`$${data.revenue.toLocaleString()}`} color="#ff4d4f" />
          <StatCard label="Growth Rate" value={`${data.growth}%`} color={parseFloat(data.growth) >= 0 ? '#52c41a' : '#ff4d4f'} />
        </div>
      )}

      <p style={{ textAlign: 'right', color: '#999', fontSize: '12px', marginTop: '12px' }}>
        Last Updated:{lastUpdate} {isLive ? '(Auto-refresh every 5s)' : '(Suspended)'}
      </p>
    </div>
  )
}

function StatCard({ label, value, color }) {
  return (
    <div style={{
      border: '1px solid #f0f0f0',
      borderRadius: '8px',
      padding: '20px',
      textAlign: 'center',
      boxShadow: '0 2px 4px rgba(0,0,0,0.05)'
    }}>
      <p style={{ color: '#666', margin: '0 0 8px 0' }}>{label}</p>
      <p style={{ fontSize: '28px', fontWeight: 'bold', color, margin: 0 }}>{value}</p>
    </div>
  )
}

期待される出力:4つの統計カード、リアルタイム更新の切り替えボタン、ウィンドウサイズインジケーター、および最終更新時刻を含むデータダッシュボード。


(1) ▶ サンプル:useEffect を使用したリアルタイムデータ同期の実装

JSX
function LiveSearch({ apiBase }) {
  const [query, setQuery] = useState('')
  const [results, setResults] = useState([])
  const [loading, setLoading] = useState(false)

  useEffect(() => {
    if (!query.trim()) { setResults([]); return }
    setLoading(true)
    const controller = new AbortController()
    fetch(`${apiBase}/search?q=${encodeURIComponent(query)}`, { signal: controller.signal })
      .then(res => res.json())
      .then(data => { setResults(data); setLoading(false) })
      .catch(err => { if (err.name !== 'AbortError') setLoading(false) })
    return () => controller.abort()
  }, [query, apiBase])

  return (
    <div style={{ maxWidth: 400, margin: '0 auto' }}>
      <input value={query} onChange={e => setQuery(e.target.value)}
        placeholder="Search users..." style={{ width: '100%', padding: 8, borderRadius: 4, marginBottom: 8 }} />
      {loading && <p>Searching...</p>}
      <ul style={{ listStyle: 'none', padding: 0 }}>
        {results.map(r => <li key={r.id} style={{ padding: 4, borderBottom: '1px solid #f0f0f0' }}>{r.name}</li>)}
      </ul>
    </div>
  )
}
▶ 試してみよう

(2) ▶ サンプル:useEffect を使ってウィンドウサイズの変更を監視する

JSX
function useWindowSize() {
  const [size, setSize] = useState({ width: window.innerWidth, height: window.innerHeight })

  useEffect(() => {
    function handleResize() {
      setSize({ width: window.innerWidth, height: window.innerHeight })
    }
    window.addEventListener('resize', handleResize)
    return () => window.removeEventListener('resize', handleResize)
  }, [])

  return size
}

function ResponsiveLayout() {
  const { width } = useWindowSize()
  const layout = width < 640 ? 'mobile' : width < 1024 ? 'tablet' : 'desktop'

  return (
    <div style={{ padding: 16, maxWidth: 600, margin: '0 auto' }}>
      <h3>Viewport: {width}px ({layout})</h3>
      {layout === 'mobile' && <p>📱 Single column layout</p>}
      {layout === 'tablet' && <p>📟 Two column layout</p>}
      {layout === 'desktop' && <p>💻 Three column layout with sidebar</p>}
    </div>
  )
}
▶ 試してみよう

(3) ▶ サンプル:useEffect と localStorage を使った状態の永続化

JSX
function usePersistedState(key, initial) {
  const [value, setValue] = useState(() => {
    try {
      const saved = localStorage.getItem(key)
      return saved !== null ? JSON.parse(saved) : initial
    } catch { return initial }
  })

  useEffect(() => {
    try { localStorage.setItem(key, JSON.stringify(value)) }
    catch (e) { console.error('Save failed:', e) }
  }, [key, value])

  return [value, setValue]
}

function TodoApp() {
  const [todos, setTodos] = usePersistedState('todos', [])
  const [input, setInput] = useState('')

  function addTodo() {
    if (!input.trim()) return
    setTodos(prev => [...prev, { id: Date.now(), text: input.trim(), done: false }])
    setInput('')
  }

  function toggleTodo(id) {
    setTodos(prev => prev.map(t => t.id === id ? { ...t, done: !t.done } : t))
  }

  return (
    <div style={{ maxWidth: 400, margin: '0 auto' }}>
      <h3>Todos (persisted)</h3>
      <div style={{ display: 'flex', gap: 8, marginBottom: 8 }}>
        <input value={input} onChange={e => setInput(e.target.value)}
          onKeyDown={e => e.key === 'Enter' && addTodo()}
          placeholder="Add task..." style={{ flex: 1, padding: 8, borderRadius: 4 }} />
        <button onClick={addTodo} style={{ padding: '8px 16px', cursor: 'pointer' }}>Add</button>
      </div>
      <ul style={{ listStyle: 'none', padding: 0 }}>
        {todos.map(t => (
          <li key={t.id} onClick={() => toggleTodo(t.id)}
            style={{ padding: 4, cursor: 'pointer', textDecoration: t.done ? 'line-through' : 'none', color: t.done ? '#999' : '#333' }}>
            {t.text}
          </li>
        ))}
      </ul>
    </div>
  )
}
▶ 試してみよう

❓ よくある質問

Q useEffectと、コンポーネント内に直接コードを記述する場合の違いは何ですか?
A useEffect内のコードはレンダリングが完了した後にのみ実行されるため、UIをブロックしません。一方、コンポーネントの関数本体内に直接記述されたコードは、レンダリング中に実行される(これによりレンダリングがブロックされる)ため、レンダリングが行われるたびに実行されます。主な違いは、useEffect を使用すると、コンポーネントがアンマウントされた際にクリーンアップ処理を実行できる点です。
Q なぜ useEffect のクリーンアップ関数は、再実行のたびに実行されるのですか?
A これは React の設計上の保証であり、依存関係の変更によって引き起こされる「レースコンディション」を防ぐためです。例えば、userIdが変更された場合、新しいリクエストが開始される前に古いリクエストがクリーンアップされ、新しいデータが古いデータを上書きすることが保証されます。もし新しいリクエストが古いリクエストを直接上書きしてしまうと、「古いリクエストが先に完了する → データが元に戻る」というバグが発生する可能性があります。
Q 複数の useEffect フックの実行順序はどのようになりますか?
A コード内に記述されている順序で実行されます。したがって、「初期化」(データの読み込みなど)を処理する useEffect フックを最初に配置し、「変更への対応」(localStorage への状態の同期など)を処理するフックを最後に配置する必要があります。1回のレンダリング中に、すべての useEffect フックが順番に実行されます。
Q useEffectuseLayoutEffect の違いは何ですか?
A 実行されるタイミングが異なります。useEffect は DOM が更新された後(ブラウザがレンダリングした後)に非同期で実行されるため、ユーザーにはちらつきが見られません。useLayoutEffectは、DOMが更新された後(ブラウザがレンダリングを行う前)に同期的に実行されるため、DOMを変更してからブラウザにレンダリングさせることができます。99%のケースでは useEffect を使用してください。useLayoutEffect は、「DOMのレイアウトに基づいて計算を行い、その後同期的にDOMを変更する必要がある」場合(要素の寸法を測定した後にその位置を調整する場合など)にのみ使用してください。

📖 まとめ


📝 練習問題

  1. 基本問題(難易度 ⭐)PageTitle コンポーネントを作成してください。useEffect を使用して、コンポーネントがマウントされた際にページタイトルを「Welcome to My Website」に変更し、アンマウントされた際には元のタイトルに戻してください。
  2. 上級演習(難易度 ⭐⭐)OnlineStatus コンポーネントを作成し、useEffect を使用して navigator.onLine および online/offline からのイベントを監視し、ネットワークの状態をリアルタイムで表示するようにしてください。
  3. 課題(難易度:⭐⭐⭐):パラメータ url および interval を受け取り、インターフェースを interval ミリ秒ごとに 1 回ポーリングし、{ data, isLoading, error } を返す、usePolling という名前のカスタムフックを作成してください。このフックは、ポーリングの一時停止と再開に対応している必要があります。
Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%