副作用とデータリクエスト — useEffect
この章では、React で最も重要な副作用 Hook である useEffect を学び、適切なタイミングで fetch を使って JSON ファイルからブログデータを読み込みます。
副作用とは?
React では、副作用(Side Effect)レンダリングとは無関係な操作のことを指します:
- サーバーへのデータリクエスト
- localStorage の操作
- タイマー setInterval の設定
- DOM の手動変更
- 外部イベントの購読
副作用はコンポーネント関数本体で直接実行することはできません(毎回のレンダリングで実行されてしまいます)。必ずuseEffectの中に置く必要があります。
useEffect 基本の使い方
実例
function HomePage() {
const [message, setMessage] = useState('読み込み中...')
// useEffect(fn, deps):コンポーネントのレンダリング後に副作用を実行
useEffect(() => {
console.log('コンポーネントが DOM にマウントされました。副作用を実行できます')
setMessage(データの読み込みが完了しました!)
}, []) // 空の依存配列 = 初回レンダリング後にのみ1回実行
return <p>{message}</p>
}
依存配列の3つの書き方
| 書き方 | 実行タイミング | 使用シーン |
|---|---|---|
useEffect(fn) | 毎回のレンダリング後に実行 | ほとんど使用しない。無限ループを引き起こしやすい |
useEffect(fn, []) | 初回レンダリング後にのみ1回実行 | データ読み込み、サードパーティライブラリの初期化 |
useEffect(fn, [dep]) | 初回レンダリング後 + dep が変化したときに実行 | 特定のデータの変化を監視し、対応する操作を実行 |
依存配列の省略 vs 空配列の違い:
useEffect(fn)毎回のレンダリング後に実行されます;useEffect(fn, [])初回レンダリング後にのみ1回実行されます。前者は無限ループを引き起こしやすく(effect 内の setState → レンダリング発生 → effect を再実行 → ...)、ほとんど使用されません。
fetch で JSON データを読み込む
Vue3 と同じように、記事データをpublic/posts.jsonファイルの中に置きます。
ステップ1:データファイルを作成
実例
[
{
"id": 1,
"title": "React 入門完全ガイド",
"summary": "ゼロから React Hooks を学び、useState、useEffect などの核心概念を網羅。",
"content": "<h2>なぜ React を学ぶのか?</h2><p>React は現在最も人気のあるフロントエンドフレームワークの一つです...</p>",
"category": "React",
"date": "2024-05-10"
},
{
"id": 2,
"title": "JavaScript 非同期プログラミング詳解",
"summary": "Promise、async/await、イベントループとマイクロタスクキューを一つの記事で完全に理解。",
"content": "<h2>非同期とは?</h2><p>JS はシングルスレッドです...</p>",
"category": "JavaScript",
"date": "2024-05-08"
},
{
"id": 3,
"title": "CSS Grid レイアウト実践",
"summary": "CSS Grid で複雑なレスポンシブレイアウトを簡単に実現。",
"content": "<h2>Grid 入門</h2><p>Grid は二次元レイアウトシステムです...</p>",
"category": "CSS",
"date": "2024-05-05"
}
]
ステップ2:useEffect でデータを読み込む
実例
import { useState, useEffect, useMemo } from 'react'
import BlogCard from '../components/BlogCard'
import CategoryFilter from '../components/CategoryFilter'
function HomePage() {
const [articles, setArticles] = useState([]) // 記事データ
const [isLoading, setIsLoading] = useState(true) // 読み込み状態
const [error, setError] = useState(null) // エラーメッセージ
const [activeCategory, setActiveCategory] = useState('すべて')
// useEffect でデータ読み込み:空の依存配列 = 初回レンダリング後にのみ1回実行
useEffect(() => {
let cancelled = false // コンポーネントのアンマウント後に setState を防ぐフラグ
async function fetchPosts() {
setIsLoading(true)
setError(null)
try {
const res = await fetch('/posts.json')
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const data = await res.json()
if (!cancelled) {
setArticles(data)
}
} catch (err) {
if (!cancelled) {
setError(err.message)
}
} finally {
if (!cancelled) {
setIsLoading(false)
}
}
}
fetchPosts()
// クリーンアップ関数:コンポーネントのアンマウント時に cancelled を true に設定
return () => { cancelled = true }
}, [])
const categories = useMemo(() => {
return ['すべて', ...new Set(articles.map(a => a.category))]
}, [articles])
const filteredArticles = useMemo(() => {
if (activeCategory === 'すべて') return articles
return articles.filter(a => a.category === activeCategory)
}, [articles, activeCategory])
return (
<div>
<h2 className="section-title">最新記事</h2>
{/* 読み込み中 */}
{isLoading && <p className="status-msg">読み込み中です。しばらくお待ちください...</p>}
{/* 読み込みエラー */}
{error && (
<div className="status-msg error">
<p>読み込みに失敗しました:{error}</p>
<button onClick={() => window.location.reload()}>再試行</button>
</div>
)}
{/* データ読み込み完了 */}
{!isLoading && !error && (
<>
<CategoryFilter
categories={categories}
activeCategory={activeCategory}
onCategoryChange={setActiveCategory}
/>
<p className="result-info">共 {filteredArticles.length} 篇</p>
{filteredArticles.length === 0 ? (
<p className="empty-tip">このカテゴリには記事がありません</p>
) : (
<div className="article-grid">
{filteredArticles.map(article => (
<BlogCard key={article.id} {...article} />
))}
</div>
)}
</>
)}
</div>
)
}
export default HomePage
cancelledcancelled フラグは React でよく見られるパターンです。fetch がまだ進行中のときにユーザーがページを離れると(コンポーネントのアンマウント)、その後の setState がエラー警告を出します。cleanup 関数で cancelled を設定するとcancelled = trueアンマウント後に setState をスキップできます。
クリーンアップ関数(Cleanup)
useEffect のコールバック関数は関数を返すことができ、それをクリーンアップ関数。
と呼びます。これは次の2つのタイミングで実行されます:コンポーネントのアンマウント時と次の effect の実行前。
実例
// 副作用:イベント購読
const handleScroll = () => console.log('スクロールしました')
window.addEventListener('scroll', handleScroll)
// クリーンアップ関数を返す
return () => {
// コンポーネントのアンマウント時にイベントリスナーを自動的に削除し、メモリリークを防止
window.removeEventListener('scroll', handleScroll)
}
}, [])
useEffect(() => {
// 副作用:タイマー設定
const timer = setInterval(() => {
console.log('毎秒実行')
}, 1000)
return () => clearInterval(timer) // タイマーをクリア
}, [])
| 副作用 | クリーンアップが必要? | クリーンアップ方法 |
|---|---|---|
| fetch リクエスト | である | cancelled フラグまたは AbortController を使用 |
| イベントリスナー | である | removeEventListener |
| タイマー | である | clearInterval / clearTimeout |
| DOM を変更したら元に戻す | である | cleanup で元の状態に戻す |
| localStorage への書き込み | 否 | クリーンアップ不要 |
非同期リクエストの4つの状態を処理する
すべてのデータリクエストは、以下の4つの UI 状態をカバーする必要があります:
| 状態 | 条件 | UI |
|---|---|---|
| 読み込み中 | isLoading === true | loading アニメーション、スケルトン画面 |
| 成功 | データが返ってきた | 通常のコンテンツをレンダリング |
| 失敗 | error !== null | エラーメッセージ + 再試行ボタン |
| 空データ | データが空の配列 | 空状態のメッセージ |
VS Vue3 ライフサイクル対照
| Vue3 | React | 説明 |
|---|---|---|
onMounted | useEffect(fn, []) | 初回レンダリング後に実行 |
watch(keyword, fn) | useEffect(fn, [keyword]) | 特定のデータの変化に応答 |
onBeforeUnmount | useEffect 的 cleanup | コンポーネント破棄前のクリーンアップ |
| 直接の対応なし | useEffect(() => { fn; return cleanup }, [dep]) | dep が変化したときは cleanup を実行してから fn を実行 |
章のまとめ
この章では、useEffectの核心のな使い方を習得しました:依存配列の3つの書き方、fetchによる非同期データの読み込み、4つのUI状態の処理、そしてクリーンアップ関数の役割です。
これでブログデータは外部 JSON から動的に読み込まれ、JS にハードコードされなくなりました。
その他の拡張