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()andclearInterval(). An event subscription usingwindow.addEventListener()andwindow.removeEventListener(). A third-party animation library...
「React の管理下にないもの」と React の state を橋渡しするのが役目です。具体例として、
- タイマー (
setInterval/setTimeout) - イベントリスナー (
window.addEventListener) - サーバーとの接続 (WebSocket、サブスクリプション)
- 外部ライブラリ (jQuery プラグイン、地図ライブラリなど)
- ネットワークフェッチ (データ取得)
- 解析ログの送信
などが挙げられます。
図 3.6.1 React の世界と外部システムの境界
この図の左側に収まる話 (state を増やす、計算する、ユーザー操作で変える) は、useEffect を使わずに書けるはずです。useEffect の出番は、右側の世界と左側の世界をつなぐ ときだけ、と覚えておくと迷いにくくなります。
シグネチャと 3 ステップ
useEffect のシグネチャはシンプルです。
useEffect(setup, dependencies?)書き方は公式のこの 3 ステップに沿わせると安定します。
- Declare an Effect. By default, your Effect will run after every commit.
- Specify the Effect dependencies.
- Add cleanup if needed.
最小例 — タイマー
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.
依存配列が役立つのは、「props や state の値に応じて再同期すべき外部処理」がある場合です。
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 は次の順で動きます。
- 前回の cleanup (
'general'から disconnect) を呼ぶ - 新しい 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.
「マウント」「アンマウント」で考えるよりも、start / stop のペア で考えると間違いにくくなります。
addEventListener→removeEventListenersetInterval→clearIntervalconnection.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 で作る
// 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.
// OK
function Form({ firstName, lastName }: Props) {
const fullName = firstName + ' ' + lastName
return <p>{fullName}</p>
}派生値は ふつうの変数 として計算するのが正解です。
誤用 2 — イベントの代わりに Effect を使う
// 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.
出典: 同上
// OK
function buyProduct() {
addToCart(product)
showNotification(`Added ${product.name} to the shopping cart!`)
}
<button onClick={buyProduct}>買う</button>判断基準は 「その処理は、画面に映ったから起きるのか、ユーザーが何かしたから起きるのか」 です。後者ならイベントハンドラに書きます。
誤用 3 — props が変わったら state をリセットする
// NG: userId が変わったらコメント欄を空にする
useEffect(() => {
setComment('')
}, [userId])これは key prop で表現するのが正解です。
By passing
userIdas akeyto theProfilecomponent, you're asking React to treat twoProfilecomponents with differentuserIdas two different components that should not share any state.出典: 同上
// OK
<Profile userId={userId} key={userId} />key が変わるとコンポーネントは作り直されます (→ 3.7 リストと key で深掘りします)。
手を動かして確かめる
タイマーを useEffect で動かすサンプルです。cleanup をコメントアウトすると、開発時の StrictMode で二重登録になる様子も観察できます。
まとめ
useEffectは「外部システムと React を同期させる」ためのフック。- 書き方は setup / cleanup / dependencies の 3 点セット。
- start / stop のペアで Effect を考えると安定する。
- 派生値・イベント処理・state リセットの多くは Effect を使わずに書ける。
- 迷ったら「You Might Not Need an Effect」のページを読み返す。
さらに深く学ぶには
- react.dev — Synchronizing with Effects — 3 ステップの詳しい説明。
- react.dev — You Might Not Need an Effect — 誤用パターンの網羅的なリスト。
- react.dev — Lifecycle of Reactive Effects — 依存変化時の再同期の考え方。
- react.dev — useEffect Reference — 完全な API リファレンス。