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 AppuseRefで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 AppPromise.allを使わない非効率なコードをたまに見かけるので、複数のAPIの読み込みが必要な場合は注意が必要です。

