1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

なぜ "卒業" が必要なのか?

useEffect「React と外部世界を同期するための窓」 として設計されました。
しかし、データ取得・計算・状態管理まで何でも詰め込むと 二重フェッチウォーターフォール が発生し、パフォーマンスも可読性も悪化します。

※二重フェッチ
 →データを1回取ってきたいだけなのに、なぜか2回リクエストが飛んでしまう現象
※ウォーターフォール
 →複数のデータ取得が順番待ちで流れるように1つずつ実行されてしまい、全体が遅くなる現象

React 19 と Next.js 15 では以下の仕組みを活用することで、適切な責任分離が可能になりました。

  • Server Components + use() / cache() でサーバーサイドデータ取得
  • クライアントでは SWR + Suspense でクライアントサイドデータ取得

この記事では useEffectを本来の役割に集中させ、本当に必要な場面以外で使わない ための実装パターンをまとめます。

1. useEffect の本質を理解する

  1. 目的は「副作用 (side-effects) の同期」 — React が制御しない外部システムと状態を合わせるためのフック
  2. 計算・データ取得・DOM 無関係の値算出には不要 — レンダリング中や Server Components で済ませる
  3. 必ずクリーンアップを返し「開始 ↔ 終了」を意識 — イベントリスナ・タイマー・サブスクリプションは解除必須

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. 実装例

app/page.tsx
// 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. 基本パターン

components/Profile.tsx
'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. パフォーマンス最適化テクニック

utils/preload.ts
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)に実装し、必ずクリーンアップ関数を返してください。

hooks/useWindowResize.tsx
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 内で setStatefetch を併用しない
  • ウォーターフォール/二重フェッチを誘発する原因
  • 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

1
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?