Skip to content

3.6 副作用 (useEffect) の考え方 ​

ここで押さえてほしいのは、「useEffect は 外部システムと React を同期させる ためにある。純粋に描画に必要な計算や、ユーザーのクリックに反応する処理を書く場所ではない」という一点です。

useEffect はとても汎用的に見えるため、誤用されやすい API です。「何にでも使える万能道具」として扱うとコードが壊れやすくなるので、用途を絞って考えていきましょう。

外部システムの同期とは ​

公式は useEffect の役割をこう定義しています。

An Effect lets you keep your component synchronized with some external system (like a chat service). Here, external system means any piece of code that's not controlled by React, such as: A timer managed with setInterval() and clearInterval(). An event subscription using window.addEventListener() and window.removeEventListener(). A third-party animation library...

出典: react.dev — useEffect Reference

「React の管理下にないもの」と React の state を橋渡しするのが役目です。具体例として、

  • タイマー (setInterval / setTimeout)
  • イベントリスナー (window.addEventListener)
  • サーバーとの接続 (WebSocket、サブスクリプション)
  • 外部ライブラリ (jQuery プラグイン、地図ライブラリなど)
  • ネットワークフェッチ (データ取得)
  • 解析ログの送信

などが挙げられます。

図 3.6.1 React の世界と外部システムの境界 ​

React の世界React が管理するコンポーネントstate / props再レンダー / コミットuseEffectstart / stop外部システムReact の外にあるsetInterval / setTimeoutWebSocket / 購読外部ライブラリ(地図など)ブラウザの window / document

この図の左側に収まる話 (state を増やす、計算する、ユーザー操作で変える) は、useEffect を使わずに書けるはずです。useEffect の出番は、右側の世界と左側の世界をつなぐ ときだけ、と覚えておくと迷いにくくなります。

シグネチャと 3 ステップ ​

useEffect のシグネチャはシンプルです。

ts
useEffect(setup, dependencies?)

書き方は公式のこの 3 ステップに沿わせると安定します。

  1. Declare an Effect. By default, your Effect will run after every commit.
  2. Specify the Effect dependencies.
  3. Add cleanup if needed.

出典: react.dev — Synchronizing with Effects

最小例 — タイマー ​

tsx
import { useEffect, useState } from 'react'

export default function Clock() {
  const [now, setNow] = useState(new Date())

  useEffect(() => {
    const id = setInterval(() => setNow(new Date()), 1000)
    return () => clearInterval(id)  // cleanup
  }, [])

  return <time>{now.toLocaleTimeString()}</time>
}
  • setup: setInterval(...) を起動する
  • cleanup: clearInterval(id) で停止する
  • dependencies: 空配列 [] なので「コンポーネントがマウントされた時に 1 回だけ start」「アンマウントされた時に 1 回だけ stop」

依存配列の意味 ​

useEffect の第 2 引数は 依存配列 です。指定によって挙動が 3 通りに分かれます。

指定挙動
省略毎回のコミット後に実行
[]初回コミット後のみ実行
[a, b]初回コミット後 + a または b が変わるたびに実行

公式の説明は次の通りです。

If you omit this argument, your Effect will re-run after every commit of the component.

出典: react.dev — useEffect Reference

依存配列が役立つのは、「props や state の値に応じて再同期すべき外部処理」がある場合です。

tsx
function ChatRoom({ roomId }: { roomId: string }) {
  useEffect(() => {
    const connection = createConnection(roomId)
    connection.connect()
    return () => connection.disconnect()
  }, [roomId])  // roomId が変わるたびに再接続

  return <p>Room: {roomId}</p>
}

roomId が 'general' → 'travel' に変わると React は次の順で動きます。

  1. 前回の cleanup ('general' から disconnect) を呼ぶ
  2. 新しい setup ('travel' に connect) を呼ぶ

cleanup の考え方 ​

cleanup は「setup がやったことを巻き戻す」関数です。公式は Effect をこう捉えることを勧めています。

Instead, try to think about each Effect independently from your component's lifecycle. ... An Effect can only do two things: to start synchronizing something, and later to stop synchronizing it.

出典: react.dev — Lifecycle of Reactive Effects

「マウント」「アンマウント」で考えるよりも、start / stop のペア で考えると間違いにくくなります。

  • addEventListener → removeEventListener
  • setInterval → clearInterval
  • connection.connect() → connection.disconnect()
  • fetch → AbortController による cancel

図 3.6.2 依存変化で Effect が再同期する流れ ​

ライフサイクルを step-through で眺める ​

sequence 図だけではピンと来にくいので、実際にステップを踏んでみるデモを用意しました。ボタンを ①→②→③ の順に押して、setup と cleanup がどのタイミングで呼ばれるかを観察してみてください。

useEffect(() => {
  const c = createConnection(roomId)
  c.connect()
  return () => c.disconnect()
}, [roomId])
外部システム: 切断
  • (ボタンを順番に押して、setup と cleanup がどう呼ばれるか観察してみてください)

ここが腑に落ちると、useEffect の挙動は怖くなくなります。「マウント時に setup、依存が変わると cleanup → setup の順で 1 往復、アンマウント時に最後の cleanup」の 3 パターンしかないんですね。

よくある誤用 — Effect が不要なケース ​

初心者が最も消耗するのは、「本来 Effect でやるべきでないことを Effect でやってしまう」ケースです。react.dev には「You Might Not Need an Effect」という章があり、本書でもこれを強調します。

誤用 1 — 派生値を state + Effect で作る ​

tsx
// NG
function Form({ firstName, lastName }: Props) {
  const [fullName, setFullName] = useState('')

  useEffect(() => {
    setFullName(firstName + ' ' + lastName)
  }, [firstName, lastName])

  return <p>{fullName}</p>
}

これは二重レンダーを引き起こします。1 回目で親から新しい props が来てレンダー、2 回目で Effect が setFullName を呼んでさらにレンダー、となります。

When something can be calculated from the existing props or state, don't put it in state. Instead, calculate it during rendering.

出典: react.dev — You Might Not Need an Effect

tsx
// OK
function Form({ firstName, lastName }: Props) {
  const fullName = firstName + ' ' + lastName
  return <p>{fullName}</p>
}

派生値は ふつうの変数 として計算するのが正解です。

誤用 2 — イベントの代わりに Effect を使う ​

tsx
// NG: 購入時のロジックを Effect に入れている
useEffect(() => {
  if (product.isInCart) {
    showNotification(`Added ${product.name} to the shopping cart!`)
  }
}, [product])

購入という ユーザーの操作 に伴う処理は、イベントハンドラに書くべきです。

Use Effects only for code that should run because the component was displayed to the user.

出典: 同上

tsx
// OK
function buyProduct() {
  addToCart(product)
  showNotification(`Added ${product.name} to the shopping cart!`)
}

<button onClick={buyProduct}>買う</button>

判断基準は 「その処理は、画面に映ったから起きるのか、ユーザーが何かしたから起きるのか」 です。後者ならイベントハンドラに書きます。

誤用 3 — props が変わったら state をリセットする ​

tsx
// NG: userId が変わったらコメント欄を空にする
useEffect(() => {
  setComment('')
}, [userId])

これは key prop で表現するのが正解です。

By passing userId as a key to the Profile component, you're asking React to treat two Profile components with different userId as two different components that should not share any state.

出典: 同上

tsx
// OK
<Profile userId={userId} key={userId} />

key が変わるとコンポーネントは作り直されます (→ 3.7 リストと key で深掘りします)。

手を動かして確かめる ​

タイマーを useEffect で動かすサンプルです。cleanup をコメントアウトすると、開発時の StrictMode で二重登録になる様子も観察できます。

StackBlitz で開く

まとめ ​

  • useEffect は「外部システムと React を同期させる」ためのフック。
  • 書き方は setup / cleanup / dependencies の 3 点セット。
  • start / stop のペアで Effect を考えると安定する。
  • 派生値・イベント処理・state リセットの多くは Effect を使わずに書ける。
  • 迷ったら「You Might Not Need an Effect」のページを読み返す。

さらに深く学ぶには ​

参照ソース ​