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回だけにするサンプル