useEffectは「最後の手段」。Reactの「外」と「中」を理解して、不要なコードを消す技術
TL;DR(先に結論)
- useEffect は “最後の手段(escape hatch)”
→ React の「外」と同期するときだけ使うもの。 - 「React の 中だけ で完結すること」に使っていたら、ほぼ確実に設計ミス。
- 不要な useEffect が多いと
- 再レンダリングが増えてパフォーマンス悪化
- バグの温床
- コードが読みにくくなる
この記事では、
- 「React の 中 と 外」のイメージ
- やりがちな「不要な useEffect」5パターンと、その直し方
- じゃあ どんなときなら useEffect を使ってよいのか
- 最後にレビュー用チェックリスト
までを、初心者でもイメージできるレベルで整理します。
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/setTimeout
- 永続化 API
localStorage.getItem / setItemsessionStorageIndexedDB
- ネットワーク・サーバー
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. NGパターン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 もいらない - 配列の
.filter()や.sort()、日時のフォーマット変換などは、100%このカテゴリです
4. NGパターン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:key を使ってコンポーネントをリセットする(推奨)
「モーダルを開くたびに初期値に戻したい」場合、key を使うのがシンプルで安全です。
// ✅ 親コンポーネントで key を指定
function Parent() {
const [open, setOpen] = useState(false);
const [editTarget, setEditTarget] = useState({ id: 1, name: '山田' });
return (
<EditModal
key={editTarget.id} // id が変わるとコンポーネントが再マウントされる
open={open}
initialValue={editTarget.name}
onSave={(v) => {
setEditTarget({ ...editTarget, name: v });
setOpen(false);
}}
/>
);
}
function EditModal({
open,
initialValue,
onSave,
}: {
open: boolean;
initialValue: string;
onSave: (v: string) => void;
}) {
// ✅ useState の初期値がそのまま使われる(useEffect 不要)
const [draft, setDraft] = useState(initialValue);
if (!open) return null;
return (
<div>
<input value={draft} onChange={e => setDraft(e.target.value)} />
<button onClick={() => onSave(draft)}>保存</button>
</div>
);
}
4-6. OKコードC:どうしても useEffect が必要な場合
key でのリセットが使えない場合(例:アニメーション中に状態を保持したいなど)、「閉→開」の瞬間だけを検知する必要があります。
function EditModal({
open,
initialValue,
onSave,
}: {
open: boolean;
initialValue: string;
onSave: (v: string) => void;
}) {
const [draft, setDraft] = useState(initialValue);
const prevOpen = useRef(open);
useEffect(() => {
// ✅ 「閉じていた → 開いた」瞬間だけリセット
if (open && !prevOpen.current) {
setDraft(initialValue);
}
prevOpen.current = open;
}, [open, initialValue]);
if (!open) return null;
return (
<div>
<input value={draft} onChange={e => setDraft(e.target.value)} />
<button onClick={() => onSave(draft)}>保存</button>
</div>
);
}
4-7. 覚えること
- 「props をそのまま state にコピーしていないか?」を必ず疑う
- 理想は「真実の状態は親に一つだけ」
- 子が state を持つのは「一時編集」など、理由がはっきりしている場合だけ
- リセットが必要な場合は
keyを使うのが第一選択
5. NGパターン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 回レンダリング(パターン1と同様)
-
ルールが 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. NGパターン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. NGパターン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変更時にデータフェッチ
-
サブスクリプションやリスナーの管理
- WebSocket 接続の開始・終了
window.addEventListener/removeEventListenersetInterval/clearIntervalなどのタイマー
useEffect(() => { const handleResize = () => { setWidth(window.innerWidth); }; window.addEventListener('resize', handleResize); // ✅ クリーンアップ関数で後片付け return () => { window.removeEventListener('resize', handleResize); }; }, []); -
DOM を直接触る必要がある特殊ケース
- 特定要素へのフォーカス
- スクロール位置の調整
- サードパーティ描画ライブラリの初期化・破棄
-
レイアウト計算が必要なケース(useLayoutEffect)
- 要素の幅・高さを測って位置を調整する場合
useEffectだと ブラウザのペイント後 に実行されるため、ちらつきが起きることがあるuseLayoutEffectは DOMへのコミット後、ブラウザがペイントする前 に同期的に実行されるので、
ユーザーに一瞬だけ古い状態を見せたくない場合に使う
useLayoutEffect(() => { // DOM を測定してから state を更新 → ペイント前に反映されるのでちらつかない const height = ref.current?.offsetHeight ?? 0; setContainerHeight(height); }, []);
この4カテゴリに入っていない useEffect は、まず疑っていい。
8.5. 判断フローチャート
useEffect を使うべきかどうか迷ったら、以下の質問に答えてください。
まず最初の質問:React の「外」を触っている?
| 答え | 次のステップ |
|---|---|
| NO(中だけで完結) | → 下の「内側チェック」へ |
| YES(外を触る) | → 下の「外側チェック」へ |
「内側」の場合:本当に useEffect が必要?
| 質問 | YES なら | NO なら |
|---|---|---|
| 他の state/props から計算で求まる値? | ❌ 不要:その場で計算 | 次の質問へ |
| ユーザー操作への応答? | ❌ 不要:イベントハンドラで処理 | 次の質問へ |
| **見た目(className/style)**の切り替え? | ❌ 不要:JSX で条件分岐 | 次の質問へ |
| すべて NO | 🤔 本当に必要か再検討 | — |
「外側」の場合:何を触る?
| 触るもの | 結論 |
|---|---|
| データ取得(fetch / API) | ⚠️ useEffect 可(ただしライブラリ推奨) |
| リスナー / タイマー | ✅ useEffect(クリーンアップ必須) |
| DOM 操作(レイアウト計測あり) | ✅ useLayoutEffect |
| DOM 操作(レイアウト計測なし) | ✅ useEffect |
| 外部ライブラリ(Chart.js, Map など) | ✅ useEffect(初期化・破棄を管理) |
| 外部ストアの購読(Redux, Zustand 等) | ✅ useSyncExternalStore を検討 |
8.6. 依存配列について
依存配列(第二引数の [...])の扱いは useEffect の誤用で最も多いポイントです。
基本ルール
useEffect(() => {
// userId を使った処理
fetchUser(userId);
}, [userId]); // ← userId を使っているので依存配列に含める
useEffect 内で参照しているすべての値を依存配列に含めるのが大原則です。
ESLint ルールを有効にする
ESLint の react-hooks/exhaustive-deps ルールを error にしておくと、依存配列の漏れを自動検出できます。
// .eslintrc.json
{
"rules": {
"react-hooks/exhaustive-deps": "error"
}
}
意図的に依存を省く場合
どうしても特定の依存を省きたい場合は、理由をコメントで明示してください:
useEffect(() => {
// マウント時に一度だけ実行したい
analytics.pageView(pageName);
// eslint-disable-next-line react-hooks/exhaustive-deps -- 初回マウント時のみ記録したいため
}, []);
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 の出番」
と判断できるようになると、コードの質が一段上がる。
明日からできること
-
useEffect を見たら「これは中?外?」と1秒考える
- 中の話なら、ほぼ確実に書き直せる
-
ESLint ルールを強化する
{ "react-hooks/rules-of-hooks": "error", "react-hooks/exhaustive-deps": "error" } -
データフェッチを useEffect から専用ライブラリに移行する
- TanStack Query / SWR / RTK Query など
-
既存コードで「派生状態の同期」パターンを探して潰す
useEffect+setStateのセットを検索すると見つかりやすい