なぜ "卒業" が必要なのか?
useEffect
は 「React と外部世界を同期するための窓」 として設計されました。
しかし、データ取得・計算・状態管理まで何でも詰め込むと 二重フェッチ や ウォーターフォール が発生し、パフォーマンスも可読性も悪化します。
※二重フェッチ
→データを1回取ってきたいだけなのに、なぜか2回リクエストが飛んでしまう現象
※ウォーターフォール
→複数のデータ取得が順番待ちで流れるように1つずつ実行されてしまい、全体が遅くなる現象
React 19 と Next.js 15 では以下の仕組みを活用することで、適切な責任分離が可能になりました。
-
Server Components +
use()
/cache()
でサーバーサイドデータ取得 - クライアントでは SWR + Suspense でクライアントサイドデータ取得
この記事では useEffectを本来の役割に集中させ、本当に必要な場面以外で使わない ための実装パターンをまとめます。
1. useEffect の本質を理解する
- 目的は「副作用 (side-effects) の同期」 — React が制御しない外部システムと状態を合わせるためのフック
- 計算・データ取得・DOM 無関係の値算出には不要 — レンダリング中や Server Components で済ませる
- 必ずクリーンアップを返し「開始 ↔ 終了」を意識 — イベントリスナ・タイマー・サブスクリプションは解除必須
useEffectが適切なケース
代表パターン | 具体例 |
---|---|
ブラウザ API |
addEventListener , IntersectionObserver , scroll 位置の取得 |
外部サービス SDK | Firebase Web SDK の onSnapshot , Stripe Elements など |
非 React 管理 DOM | サードパーティ UI ウィジェットを初期化/破棄 |
これ以外(データ取得・計算)は以下の手段を優先します。
2. Server Components + use()
/ cache()
―― "副作用ゼロ" でデータ取得
2-1. Server Components を選ぶ理由
- Node.js / Edge Runtime で動作するため クライアントJSバンドルサイズが増えない
-
use()
が Promise 解決を自動でSuspenseに委譲し、コードが"同期風"に書ける -
cache()
が同一リクエスト内の重複フェッチを防ぐ
2-2. 実装例
// Server Components
import { cache, use } from 'react'
import { Suspense } from 'react'
import ProductList from './ProductList'
const getProducts = cache(async () => {
const res = await fetch('https://api.example.com/products', {
next: { revalidate: 1800 } // 30分ごとに自動再生成
})
if (!res.ok) throw new Error('Failed to fetch products')
return res.json() as Product[]
})
export default function Page() {
return (
<Suspense fallback={<p>商品を読み込み中...</p>}>
<ProductListWrapper />
</Suspense>
)
}
const ProductListWrapper = () => {
const products = use(getProducts())
return <ProductList products={products} />
}
注意点: Next.js 15 から fetch()
のデフォルトは cache: 'no-store'
に変更されました。キャッシュしたい場合は revalidate
オプションか dynamic = 'force-static'
を明示する必要があります。
3. クライアントは SWR + Suspense ―― "常に最新"&ローディング統一
3-1. 基本パターン
'use client'
import useSWR from 'swr'
import { Suspense } from 'react'
const fetcher = (url: string) => fetch(url).then(r => {
if (!r.ok) throw new Error('Failed to fetch')
return r.json()
})
const ProfileInner = () => {
const { data } = useSWR('/api/user', fetcher, { suspense: true })
return <h2>Hello, {data.name}さん</h2>
}
export default function Profile() {
return (
<Suspense fallback={<p>ユーザー情報を読み込み中...</p>}>
<ProfileInner />
</Suspense>
)
}
実装ポイント
-
suspense: true
で SWR が Promise を投げ、Suspense 境界で一括ローディング管理 - SWR は Stale-While-Revalidate 戦略 — 旧データを即座に描画し、裏で再検証
→ UX が滑らか
3-2. パフォーマンス最適化テクニック
import useSWR, { preload } from 'swr'
// ページ遷移前にデータをプリロード
preload('/api/user', fetcher)
ページ遷移前にキャッシュを温めれば Suspense の待ち時間をゼロにできます。
4. useEffect を"必要最小限"に保つリファクタリング手順
手順 | やること | 使う仕組み |
---|---|---|
1 |
grep -R "useEffect(" src/ で現状把握 |
— |
2 | データ取得 を Server Component へ移動 |
use() / cache()
|
3 | ユーザー固有データ は SWR に置き換え |
useSWR , mutate()
|
4 | DOM イベントなど純粋な副作用 だけが残るか確認 | useEffect + クリーンアップ |
Strict Mode 二重実行対策
リスナー登録や WebSocket 接続は冪 (べき) 等(idempotent)に実装し、必ずクリーンアップ関数を返してください。
useEffect(() => {
const controller = new AbortController()
const handleResize = () => {
// リサイズ処理
}
window.addEventListener('resize', handleResize, {
signal: controller.signal
})
return () => {
controller.abort() // クリーンアップ
}
}, [])
5. useEffect最適化ポイント
項目 | 理由 |
---|---|
fetch() の cache / revalidate を明示 |
デフォルトが no-store のため、毎回ネットワーク通信が発生 |
共通データは cache() で重複抑止 |
同一リクエスト内でのフェッチ回数を1回に制限 |
SWR の再検証オプション (revalidateOnFocus 等) を要件に合わせて調整 |
モバイル環境での無駄な通信を防止 |
- useEffect 内で
setState
とfetch
を併用しない - ウォーターフォール/二重フェッチを誘発する原因
- useEffect には必ずクリーンアップ関数を返す
- メモリリーク・重複購読を防ぐ
6. 終わりに
- useEffect = 外部システム同期専用フック として再定義する
-
データ取得 は Server Components (
use()
/cache()
) または クライアント SWR + Suspense へ分離 - 残ったuseEffectは本当に必要な副作用のみ
useEffectを最小限の使用にとどめ、アプリケーションもコードも "副作用" を少なくすることで、パフォーマンス向上とコードの可読性向上を両立できます。
採用情報
アシストエンジニアリングでは一緒に働くフロントエンド、バックエンドエンジニアを募集しています!
少しでも興味のある方は、カジュアル面談からでもお気軽にお話ししましょう!
お問い合わせはこちらから↓
https://official.assisteng.co.jp/contact/
参考
https://ja.react.dev/reference/react/useEffect
https://ja.react.dev/reference/react/cache
https://swr.vercel.app/ja