useEffectの正しい使いどころと、使わなくていい典型パターン
TL;DR(先に結論)
- useEffect は “最後の手段(escape hatch)”
→ React の「外」と同期するときだけ使うもの。 - 「React の 中だけ で完結すること」に使っていたら、ほぼ確実に設計ミス。
- 不要な useEffect が多いと
- 再レンダリングが増えてパフォーマンス悪化
- バグの温床
- コードが読みにくくなる
この記事では、
- 「React の 中 と 外」のイメージ
- やりがちな「不要な useEffect」5パターンと、その直し方
- じゃあ どんなときなら useEffect を使ってよいのか
- 最後にレビュー用チェックリスト
までを、初心者でもイメージできるレベルで整理します。
[!NOTE]
React 18 の Strict Mode について
React 18 以降、開発環境で Strict Mode が有効だと useEffect が2回実行される仕様になりました。
これは「副作用を正しくクリーンアップしているか」をテストするための意図的な動作です。
2回実行されて壊れる useEffect は、そもそもクリーンアップが足りていないサイン。
本番環境では1回だけ実行されるので、ユーザーに影響はありません。
1. 「React の中」と「外」をちゃんと分ける
1-1. React の「中」(inside)
React が面倒を見てくれる世界です。
- コンポーネント関数の中
propsuseState,useReducerの state- それらから計算される値
(const fullName = firstName + lastNameみたいなやつ) - JSX (
return <div>...</div>) - イベントハンドラの中で
- state を更新する (
setState) - 親からもらったコールバックを呼ぶ
- state を更新する (
特徴:
- すべて「ただの JavaScript の計算」で完結していて、
- 結果は「React が DOM を再描画してくれる」ことで UI に反映される
➡ ここで完結する話に、useEffect は不要
1-2. React の「外」(outside)
React が管理していない世界。
ここに触るときに useEffect が登場する可能性が出てきます。
具体例:
- DOM を直接触る
document.getElementById(...)ref.current.focus()ref.current.scrollIntoView()ref.current.classList.add(...)/remove(...)
- ブラウザのグローバル API
window.addEventListener('resize', ...)window.removeEventListener(...)setInterval/setTimeoutlocalStorage.getItem / setItem
- ネットワーク・サーバー
fetch('/api/...')WebSocket,EventSource
- 外部ライブラリ
- Chart.js / Map ライブラリ / カスタムウィジェットなど
- 時間に依存する処理
- 現在時刻を定期的に更新する、カウントダウンなど
特徴:
- React はこの世界の後始末を自動ではやってくれない
- 自分で「開始」と「終了(クリーンアップ)」を意識する必要がある
➡ ここを扱うための“逃げ道”が useEffect
1-3. 判断の合言葉
今書こうとしている処理は
「React の中だけで完結する話か? 外に触っている話か?」
- 中だけ → useEffect 使うな(ほぼ確実に不要)
- 外に触る → useEffect 候補(ただし本当に必要かはまだ検討)
このモデルを頭に置いたまま、「不要な useEffect」パターンを見ていきます。
2. 「不要な useEffect」5つのアンチパターン
一覧
| # | アンチパターン名 | ありがちな目的 | 正しい方針 |
|---|---|---|---|
| 1 | 派生状態同期 useEffect | 別 state から計算できる値を保持 | その場で計算する |
| 2 | props コピー useEffect | props をローカル state にコピー | 親で管理 or key でリセット |
| 3 | バリデーション useEffect | isValid フラグ更新 | 関数で判定して毎回計算 |
| 4 | イベント処理 useEffect | submitted フラグを見て処理 | イベントハンドラ内で完結 |
| 5 | 見た目切り替え useEffect | classList / style 直操作 | JSX で className / style を条件分岐 |
ここから 1 つずつ。
3. パターン1:派生状態を useEffect で同期している
3-1. イメージ
firstName,lastNameからfullNameを作りたい- なぜか
fullNameをuseStateして、 useEffect で更新
3-2. NGコード
function UserForm() {
const [firstName, setFirstName] = useState('太郎');
const [lastName, setLastName] = useState('山田');
// ❌ やりがち:派生値を state に持つ
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(`${lastName} ${firstName}`);
}, [firstName, lastName]);
return <div>{fullName}</div>;
}
3-3. 何がダメか
-
同じ情報を二重に持っている
- fullName は firstName と lastName から常に計算可能
-
無駄なレンダリング
- 初回:
fullNameは空 → レンダー - useEffect →
setFullName→ 再レンダー
- 初回:
-
「何か特別な理由があるのか?」と読み手を混乱させる
- state + useEffect があると「外部との同期かな?」と思わせてしまう
3-4. OKコード
function UserForm() {
const [firstName, setFirstName] = useState('太郎');
const [lastName, setLastName] = useState('山田');
// ✅ 派生値はその場で計算
const fullName = `${lastName} ${firstName}`;
return <div>{fullName}</div>;
}
3-5. 覚えること
- 「他の state / props から100%計算できる値」は state にしない
→ useEffect もいらない - 合計・フィルタ・ソート結果・フォーマット済みテキストなど、全部このカテゴリに入りがち
[!TIP]
useMemo / useCallback との使い分け
「その場で計算」が基本ですが、計算コストが高い場合はuseMemoでキャッシュを検討してください。// 計算が重いときだけ useMemo を使う const sortedItems = useMemo(() => { return items.slice().sort((a, b) => a.name.localeCompare(b.name)); }, [items]);ただし 最初から useMemo を使う必要はない。
「まずは普通に書く → 遅かったら useMemo」の順番で OK です。
useCallbackも同様で、子コンポーネントへの props 最適化時に検討。
4. パターン2:props を state にコピーして useEffect で同期している
4-1. イメージ
- 親が
initialValueを渡してくる - 子で
valuestate を作り、useEffectでinitialValueを常に追いかける
4-2. NGコード
function MyInput({ initialValue }: { initialValue: string }) {
const [value, setValue] = useState(initialValue);
// ❌ props の変化を追いかけて state 更新
useEffect(() => {
setValue(initialValue);
}, [initialValue]);
return <input value={value} onChange={e => setValue(e.target.value)} />;
}
4-3. 何がダメか
-
真実の値(ソース)が分かりづらい
initialValueが正なのか、valueが正なのか、迷う
-
ここでも再レンダリングの二度手間
- props 変化 → レンダー → useEffect →
setValue→ 再レンダー
- props 変化 → レンダー → useEffect →
-
責務が崩れている
- 本来「値の管理」は親の役割
- 子が “props の変化を監視して同期” みたいな変な仕事を持つ
4-4. OKコードA:親で管理する
function MyInput({
value,
onChange,
}: {
value: string;
onChange: (v: string) => void;
}) {
// ✅ 親から来た値をそのまま使う
return <input value={value} onChange={e => onChange(e.target.value)} />;
}
function Parent() {
const [name, setName] = useState('山田太郎');
return <MyInput value={name} onChange={setName} />;
}
4-5. OKコードB:本当に「一時編集用」の場合(モーダルなど)
function EditModal({
open,
initialValue,
onSave,
}: {
open: boolean;
initialValue: string;
onSave: (v: string) => void;
}) {
const [draft, setDraft] = useState(initialValue);
// 「開いたときだけ」初期値をコピーするなど、明確なトリガーに絞る
useEffect(() => {
if (open) {
setDraft(initialValue);
}
}, [open, initialValue]);
if (!open) return null;
return (
<div>
<input value={draft} onChange={e => setDraft(e.target.value)} />
<button onClick={() => onSave(draft)}>保存</button>
</div>
);
}
※場合によっては key={initialValue} を使ってコンポーネントごとリセットする方がシンプルなこともある。
4-6. 覚えること
- 「props をそのまま state にコピーしていないか?」を必ず疑う
- 理想は「真実の状態は親に一つだけ」
- 子が state を持つのは「一時編集」など、理由がはっきりしている場合だけ
5. パターン3:バリデーション結果を useEffect+state で持っている
5-1. イメージ
emailとpasswordからisValidを出したいisValidも state にして useEffect で更新している
5-2. NGコード
function LoginForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
// ❌ isValid を state + useEffect で管理
const [isValid, setIsValid] = useState(false);
useEffect(() => {
const ok = email.includes('@') && password.length >= 8;
setIsValid(ok);
}, [email, password]);
return (
<form>
<input value={email} onChange={e => setEmail(e.target.value)} />
<input type="password" value={password} onChange={e => setPassword(e.target.value)} />
<button disabled={!isValid}>ログイン</button>
</form>
);
}
5-3. 何がダメか
-
isValid も計算で求まるだけ
-
また無駄な 2 回レンダリング
-
ルールが useEffect の中に埋もれる
- 「@必須」「8文字以上」みたいな仕様がパッと見で分からない
5-4. OKコード
function LoginForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
// ✅ その場で計算する
const isValid = email.includes('@') && password.length >= 8;
return (
<form>
<input value={email} onChange={e => setEmail(e.target.value)} />
<input type="password" value={password} onChange={e => setPassword(e.target.value)} />
<button disabled={!isValid}>ログイン</button>
</form>
);
}
ルールを関数に切り出すと、さらに分かりやすい:
function validateLogin(email: string, password: string): boolean {
return email.includes('@') && password.length >= 8;
}
const isValid = validateLogin(email, password);
5-5. 覚えること
- 「これはただの判定ロジックだよね?」と思ったら
→ 関数化して毎回計算。state + useEffect は不要 - useEffect は「計算する場所」じゃなくて、「外部と同期を取る場所」
6. パターン4:ユーザーイベントの処理を「フラグ+useEffect」でやっている
6-1. イメージ
- 「送信ボタン押されたら API を呼びたい」
submittedフラグを立てて、useEffect でそれを見て API を呼ぶ
6-2. NGコード
function ContactForm() {
const [message, setMessage] = useState('');
const [submitted, setSubmitted] = useState(false);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
setSubmitted(true);
};
// ❌ イベントの本体処理を useEffect に追い出している
useEffect(() => {
if (!submitted) return;
fetch('/api/contact', {
method: 'POST',
body: JSON.stringify({ message }),
}).then(() => {
alert('送信しました');
});
}, [submitted, message]);
return (
<form onSubmit={handleSubmit}>
<textarea value={message} onChange={e => setMessage(e.target.value)} />
<button type="submit">送信</button>
</form>
);
}
6-3. 何がダメか
-
イベント → 処理 の対応関係がコードに出てこない
- 実際にやりたいのは「submit → API」なのに、
- コード上は「submit → submitted を true → useEffect が API」を見に行く
というムダなワンクッションがある
-
条件が増えると地獄
- ローディング、エラー、リトライ…と増やすと
state フラグだらけ+useEffect 内 if だらけになる
- ローディング、エラー、リトライ…と増やすと
6-4. OKコード:イベントハンドラ内で完結させる
function ContactForm() {
const [message, setMessage] = useState('');
const [isSending, setIsSending] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsSending(true);
setError(null);
try {
const res = await fetch('/api/contact', {
method: 'POST',
body: JSON.stringify({ message }),
});
if (!res.ok) {
throw new Error('送信に失敗しました');
}
alert('送信しました');
} catch (err) {
// ✅ エラーも state で管理して UI に反映
setError(err instanceof Error ? err.message : '不明なエラー');
} finally {
setIsSending(false);
}
};
return (
<form onSubmit={handleSubmit}>
<textarea value={message} onChange={e => setMessage(e.target.value)} />
{error && <p style={{ color: 'red' }}>{error}</p>}
<button type="submit" disabled={isSending}>
{isSending ? '送信中…' : '送信'}
</button>
</form>
);
}
6-5. 覚えること
- 「ユーザー操作に対する処理」はイベントハンドラで完結させる
- useEffect を使うのは、「描画が反映された後の処理」が本当に必要なときだけ
7. パターン5:見た目の切り替えを useEffect+DOM操作でやっている
7-1. イメージ
activeフラグに応じてパネルの見た目を変えたいref.current.classList.add/removeを useEffect で叩く
7-2. NGコード
function Panel({ active }: { active: boolean }) {
const ref = useRef<HTMLDivElement | null>(null);
// ❌ DOM を直接いじって class を変える
useEffect(() => {
if (!ref.current) return;
if (active) {
ref.current.classList.add('active');
} else {
ref.current.classList.remove('active');
}
}, [active]);
return <div ref={ref}>パネル</div>;
}
7-3. 何がダメか
-
React の「状態 → UI」っていう一番の強みを捨てている
- className は普通に JSX で条件分岐すればいいだけ
-
DOMと状態のズレリスク
- StrictMode の二重実行や ref のタイミングずれなどで想定外の動きになる可能性
7-4. OKコード
function Panel({ active }: { active: boolean }) {
return (
<div className={active ? 'panel active' : 'panel'}>
パネル
</div>
);
}
style でも同じ:
function Panel({ active }: { active: boolean }) {
return (
<div style={{ opacity: active ? 1 : 0.5 }}>
パネル
</div>
);
}
7-5. 覚えること
-
「この DOM 操作、className / style の条件分岐で書けない?」
- 書けるなら、100% そっちが正解
8. 結局、どんなときなら useEffect を使っていいのか?
ここまで「使うな」ばかりだったので、OK パターンだけ整理します。
useEffect を使うべき典型ケース
-
外部 API からのデータ取得
- コンポーネントのマウント時やID変更時にデータフェッチ
- ただし、React Query / SWR 等のライブラリに任せる構成も増えている
-
サブスクリプションやリスナーの管理
- WebSocket 接続の開始・終了
window.addEventListener/removeEventListenersetInterval/clearIntervalなどのタイマー
useEffect(() => { const handleResize = () => { setWidth(window.innerWidth); }; window.addEventListener('resize', handleResize); // ✅ クリーンアップ関数で後片付け return () => { window.removeEventListener('resize', handleResize); }; }, []);[!IMPORTANT]
クリーンアップを忘れると…- リスナーが残り続けてメモリリーク
- コンポーネントが unmount しても処理が走り続ける
- Strict Mode で2回実行されるとリスナーが重複登録される
return で後片付けを書く習慣をつけるとよさそう。
-
DOM を直接触る必要がある特殊ケース
- 特定要素へのフォーカス
- スクロール位置の調整
- サードパーティ描画ライブラリの初期化・破棄
-
レイアウト計算が必要なケース(useLayoutEffect)
- 要素の幅・高さを測って位置を調整する場合
useEffectだと 描画後 に実行されるため、ちらつきが起きることがあるuseLayoutEffectは 描画前(DOMは更新済み) に同期的に実行されるので、
ユーザーに一瞬だけ古い状態を見せたくない場合に使う
useLayoutEffect(() => { // DOM を測定してから state を更新 → ちらつかない const height = ref.current?.offsetHeight ?? 0; setContainerHeight(height); }, []);⚠️
useLayoutEffectはブラウザの描画をブロックするため、重い処理には向きません。
この4カテゴリに入っていない useEffect は、まず疑っていい。
9. レビュー用チェックリスト
PRレビューやセルフレビューで、useEffect を見たらこれを回す:
-
この処理は React の外を触っているか?
- DOM 直操作 / イベントリスナー / タイマー / fetch / WebSocket…
- 触っていないなら → useEffect ほぼ不要
-
この state は、他の state/props から計算できないか?
- YES → state+useEffect で同期するな。毎回計算せよ
-
ユーザーイベントの処理を「フラグ+useEffect」でやってないか?
- YES → イベントハンドラに処理を戻せるはず
-
見た目(className/style)の切り替えを DOM 操作でやってないか?
- YES → JSX の条件分岐に移せるはず
10. さいごに
- useEffect は「悪」じゃない。
でも、「Reactの中だけで完結する話なのに使っている」時点で設計が負けている。 - 「中」と「外」を意識して線を引き、
「これは中の話だから useEffect 使うな」
「これは外の話だから useEffect の出番」
と判断できるようになると、コードの質が一段上がる。