ReactでuseEffectで初期化の処理をしてはいけない

ReactでuseEffectで初期化の処理をしてはいけない


React初心者によくある誤解のひとつに、「初期化処理はuseEffectで行う」というものがあります。

確かに、useEffectに初期処理を書くと「コンポーネントのマウント時に一度だけ実行される」ため、一見正しそうに見えます。

しかし、実際の開発ではuseEffectで初期化処理を書くことは避けるべきです。

useEffectで初期化処理を書いてしまう例

TSX
import { useEffect, useState } from 'react'

export default function App() {
  const [user, setUser] = useState<{ name: string } | null>(null)

  useEffect(() => {
    setUser({ name: 'Taro' })
  }, [])

  return <div>{user ? user.name : 'Loading...'}</div>
}

useEffectで初期化してはいけない理由

以下のコードで最初のレンダリングから正しい状態を保持できます。

TSX
const [user] = useState({ name: 'Taro' })

よって、useEffect を使う必要はありません。

fetchでの外部データ取得について

fetchで外部データを取得する際にも、useStateが以下のように使われているケースをよく見かけます。

App.tsx
import { useEffect, useState } from 'react'
import './App.css'

function App() {
  const [data, setData] = useState(null)
  const [loading, setLoading] = useState(false)

  const fetchData = async () => {
    setLoading(true)
    try {
      const response = await fetch('https://dummyjson.com/users/1?select=firstName,age')
      const result = await response.json()
      setData(result)
    }
    catch (error) {
      console.error(error)
    }
    finally {
      setLoading(false)
    }
  }

  useEffect(() => {
    fetchData()
  }, [])

  return (
    <div>
      <button onClick={fetchData} disabled={loading}>
        {data ? '再読み込み' : 'データを取得'}
      </button>
      {loading && <p>Loading...</p>}
      {data && !loading && <pre>{JSON.stringify(data, null, 2)}</pre>}
    </div>
  )
}

export default App

しかし、useEffectで外部データを取得すると、開発環境での起動中(npm run dev)はReactでは2回レンダリングされるため、2回データを取得してしまいます。

ボタンを押したときのレスポンスは1回なのに、初回読み込み時のレスポンスは2回というのはおかしいです。

そのため、この場合はuseRefで外部データ取得のフラグを管理して、1回だけデータを取得したほうが良いです。

App.tsx
import { useState, useRef } from 'react'
import './App.css'

function App() {
  const [data, setData] = useState(null)
  const [loading, setLoading] = useState(false)
  const isInitialized = useRef(false)

  const fetchData = async () => {
    setLoading(true)
    try {
      const response = await fetch('https://dummyjson.com/users/1?select=firstName,age')
      const result = await response.json()
      setData(result)
    }
    catch (error) {
      console.error(error)
    }
    finally {
      setLoading(false)
    }
  }

  if (!isInitialized.current) {
    isInitialized.current = true
    fetchData()
  }

  return (
    <div>
      <button onClick={fetchData} disabled={loading}>
        {data ? '再読み込み' : 'データを取得'}
      </button>
      {loading && <p>Loading...</p>}
      {data && !loading && <pre>{JSON.stringify(data, null, 2)}</pre>}
    </div>
  )
}

export default App

useRefでfetchでの外部データ取得を1回だけにするサンプル

ボタンのイベントによる読み込みがなく、初回のみの読み込みの場合は以下のようにuseEffectを使用しても警告が表示されず、問題ないので覚えておくと良いでしょう。

App.tsx
import { useEffect, useState } from 'react'
import './App.css'

function App() {
  const [data, setData] = useState(null)
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    fetch('https://dummyjson.com/users/1?select=firstName,age')
      .then((res) => res.json())
      .then((result) => {
        setData(result)
        setLoading(false)
      })
  }, [])

  return (
    <div>
      {loading && <p>Loading...</p>}
      {data && !loading && <pre>{JSON.stringify(data, null, 2)}</pre>}
    </div>
  )
}

export default App

初回のみの読み込みのAPIが複数の場合はPromise.allを使用します。

App.tsx
import { useEffect, useState } from 'react'
import './App.css'

function useUsers() {
  const [users, setUsers] = useState<Array<{ firstName: string, age: number }>>([])
  const [loading, setLoading] = useState(false)

  useEffect(() => {
    const fetchUsers = async () => {
      setLoading(true)
      try {
        const [user1Res, user2Res] = await Promise.all([
          fetch('https://dummyjson.com/users/1?select=firstName,age'),
          fetch('https://dummyjson.com/users/2?select=firstName,age'),
        ])

        const [user1, user2] = await Promise.all([
          user1Res.json(),
          user2Res.json(),
        ])

        setUsers([user1, user2])
      }
      catch (error) {
        console.error('Failed to fetch users:', error)
      }
      finally {
        setLoading(false)
      }
    }

    fetchUsers()
  }, [])

  return { users, loading }
}

function App() {
  const { users, loading } = useUsers()

  return (
    <div>
      {loading && <p>Loading...</p>}
      {!loading && users.map((user, index) => (
        <div key={index}>
          <h3>User {index + 1}</h3>
          <pre>{JSON.stringify(user, null, 2)}</pre>
        </div>
      ))}
    </div>
  )
}

export default App

Promise.allを使わない非効率なコードをたまに見かけるので、複数のAPIの読み込みが必要な場合は注意が必要です。