useEffectは最終手段!5つのアンチパターンから学ぶuseEffectの副作用
はじめに
「useEffect は副作用を扱うためのもの」「useEffect は最終手段」
React を書いていると、こういった話を耳にすることがあると思います。React 公式ドキュメントでも、You Might Not Need an Effectというページで「多くの場合 useEffect は不要」と明言されています。
そこで今回は、具体的にどんな問題が起きるのか、ちゃんと確かめてみようと思います!
本記事では「なんとなく良くないらしい」で終わらせず、useEffect を誤用したときに何が起きるのかを、Before/After のレンダリング回数を比較しながら体感していきます。
そもそも「副作用」とは?
React における**副作用(Side Effect)**とは、「レンダリング中に行うべきではない処理」のことです。
公式ドキュメントによると、React コンポーネントは純粋関数であるべきとされています。
// 純粋関数:同じ入力には常に同じ出力を返す
function Double({ number }) {
return <span>{number * 2}</span>;
}
純粋関数の特徴:
- 自分の仕事に集中する:呼び出される前に存在していたオブジェクトや変数を変更しない
- 同じ入力には同じ出力:同じ props には常に同じ JSX を返す
では、副作用とは何か?
// 副作用の例
- DOMの直接操作
- データのフェッチ(API呼び出し)
- タイマーのセット
- ローカルストレージへのアクセス
- イベントリスナーの登録
これらは「外部システムとの同期」が必要な処理であり、useEffect の本来の用途です。
しかし、多くの場合、useEffect を使わなくても解決できます。
5 つのアンチパターン
パターン 1:派生状態の計算
🔗 デモを見る
❌ Before(悪い例)
state から計算できる値を、useEffect で別の state に保存するパターンです。
const [category, setCategory] = useState("all");
const [filteredProducts, setFilteredProducts] = useState(products);
const [totalPrice, setTotalPrice] = useState(0);
// ❌ useEffectで派生状態を計算
useEffect(() => {
const filtered =
category === "all"
? products
: products.filter((p) => p.category === category);
setFilteredProducts(filtered);
}, [category]);
// ❌ さらに別のuseEffectで合計を計算
useEffect(() => {
const total = filteredProducts.reduce((sum, p) => sum + p.price, 0);
setTotalPrice(total);
}, [filteredProducts]);
問題点:
- カテゴリ変更で3 回レンダリングされる(state 変更 → effect → state 変更 → effect → state 変更)
- 一瞬古いデータが表示される可能性がある
- コードが複雑になる
✅ After(良い例)
const [category, setCategory] = useState("all");
// ✅ useMemoで派生値を計算
const filteredProducts = useMemo(() => {
return category === "all"
? products
: products.filter((p) => p.category === category);
}, [category]);
// ✅ 合計も同様に
const totalPrice = useMemo(() => {
return filteredProducts.reduce((sum, p) => sum + p.price, 0);
}, [filteredProducts]);
改善点:
- カテゴリ変更で1 回だけレンダリング
- 常に最新の計算結果が表示される
- useEffect なし!
パターン 2:props で state リセット
🔗 デモを見る
❌ Before(悪い例)
親からの props が変わったとき、useEffect で state をリセットするパターンです。
function ProfileForm({ user }) {
const [name, setName] = useState(user.name);
const [email, setEmail] = useState(user.email);
// ❌ propsが変わったらuseEffectでstateをリセット
useEffect(() => {
setName(user.name);
setEmail(user.email);
}, [user]);
return (
<form>
<input value={name} onChange={(e) => setName(e.target.value)} />
<input value={email} onChange={(e) => setEmail(e.target.value)} />
</form>
);
}
// 親コンポーネント
<ProfileForm user={selectedUser} />;
問題点:
- ユーザー切替時に一瞬古いデータが表示される
- 余分なレンダリングが発生
✅ After(良い例)
function ProfileForm({ user }) {
// useEffectは不要!
const [name, setName] = useState(user.name);
const [email, setEmail] = useState(user.email);
return (
<form>
<input value={name} onChange={(e) => setName(e.target.value)} />
<input value={email} onChange={(e) => setEmail(e.target.value)} />
</form>
);
}
// ✅ 親コンポーネント: key属性を追加
<ProfileForm key={selectedUser.id} user={selectedUser} />;
改善点:
-
keyが変わると React がコンポーネントを破棄 → 再作成 - state は自動的に初期値にリセット
- 古いデータが表示されることがない
パターン 3:イベントハンドラで行うべき処理
🔗 デモを見る
❌ Before(悪い例)
ボタンクリック等のイベント処理を、フラグ + useEffect で行うパターンです。
const [shouldSubmit, setShouldSubmit] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
// ❌ フラグを監視して処理を実行
useEffect(() => {
if (!shouldSubmit) return;
const submitOrder = async () => {
setIsSubmitting(true);
await submitToServer();
setIsSubmitting(false);
setShouldSubmit(false); // フラグをリセット(忘れがち!)
};
submitOrder();
}, [shouldSubmit]);
const handleSubmit = () => {
// 直接処理せず、フラグを立てるだけ
setShouldSubmit(true);
};
問題点:
- フラグ管理が複雑
- フラグのリセット忘れでバグが発生しやすい
- 処理の流れが追いにくい
✅ After(良い例)
const [isSubmitting, setIsSubmitting] = useState(false);
// ✅ イベントハンドラで直接処理
const handleSubmit = async () => {
setIsSubmitting(true);
await submitToServer();
setIsSubmitting(false);
};
改善点:
-
shouldSubmitフラグ不要 - 処理の流れが明確
- useEffect なし!
パターン 4:データフェッチ
🔗 デモを見る
❌ Before(悪い例)
クリーンアップなしでデータをフェッチするパターンです。
const [postId, setPostId] = useState(1);
const [post, setPost] = useState(null);
useEffect(() => {
setLoading(true);
// ❌ クリーンアップがない
fetchPost(postId).then((data) => {
setPost(data);
setLoading(false);
});
// クリーンアップ関数がない!
}, [postId]);
問題点:
- 競合状態(Race Condition) が発生する
- 素早く ID を切り替えると、遅いリクエストの結果が後から表示される
- アンマウント後に setState が呼ばれる可能性がある
✅ After(良い例)
const [postId, setPostId] = useState(1);
const [post, setPost] = useState(null);
useEffect(() => {
const abortController = new AbortController();
setLoading(true);
fetchPost(postId, abortController.signal)
.then((data) => {
setPost(data);
setLoading(false);
})
.catch((err) => {
if (err.name === "AbortError") return; // キャンセルは無視
setError(err.message);
});
// ✅ クリーンアップ: 古いリクエストをキャンセル
return () => abortController.abort();
}, [postId]);
改善点:
-
AbortControllerで古いリクエストをキャンセル - 競合状態を防止
- 常に最新のデータのみ表示
パターン 5:チェーンされた useEffect
🔗 デモを見る
❌ Before(悪い例)
複数の useEffect が連鎖的に実行されるパターンです。
const [country, setCountry] = useState("");
const [cities, setCities] = useState([]);
const [selectedCity, setSelectedCity] = useState(null);
const [weather, setWeather] = useState(null);
// useEffect #1: 国が変わったら都市を取得
useEffect(() => {
fetchCities(country).then((data) => {
setCities(data);
setSelectedCity(data[0]?.id); // これがuseEffect #2をトリガー
});
}, [country]);
// useEffect #2: 都市が選択されたら天気を取得
useEffect(() => {
if (!selectedCity) return;
fetchWeather(selectedCity).then(setWeather);
}, [selectedCity]);
問題点:
-
country変更 → render → effect#1 → state 更新 → render → effect#2 → state 更新 → render - 連鎖的なレンダリングが発生
- データフローが追いにくい
✅ After(良い例)
const [country, setCountry] = useState("");
const [data, setData] = useState({
cities: [],
selectedCity: null,
weather: null,
});
// ✅ 一つのuseEffectでまとめて処理
useEffect(() => {
if (!country) return;
let cancelled = false;
const fetchData = async () => {
const cities = await fetchCities(country);
if (cancelled) return;
const weather = await fetchWeather(cities[0]?.id);
if (cancelled) return;
// 一度にすべて更新
setData({ cities, selectedCity: cities[0]?.id, weather });
};
fetchData();
return () => {
cancelled = true;
};
}, [country]);
改善点:
- データ取得を一箇所で管理
- 中間状態の更新による余分なレンダリングを削減
- データフローが明確
まとめ:useEffect を使う前に確認すること
| やりたいこと | useEffect の代わりに |
|---|---|
| state から計算できる値を求めたい |
useMemoまたは直接計算 |
| props が変わったら state をリセットしたい |
key属性を使う |
| ボタンクリック等で処理を実行したい | イベントハンドラで直接実行 |
| データをフェッチしたい | TanStack Query / SWR を検討 |
| 複数の useEffect が連鎖している | 一つにまとめる or イベントハンドラに移動 |
useEffect の本来の用途は、「外部システムとの同期」です。
- DOM API の操作
- サードパーティライブラリとの連携
- WebSocket の接続
- イベントリスナーの登録/解除
これら以外の用途で useEffect を使おうとしている場合は、
一度立ち止まって「本当に useEffect が必要か?」を考えてみるのがよさそうですね。
デモ・ソースコード
実際に動かして違いを確認できるデモサイトを用意しました。
🔗 デモ: https://tamoco-mocomoco.github.io/react-use-effect-anti-pattern/
📦 GitHub: https://github.com/tamoco-mocomoco/react-use-effect-anti-pattern
参考資料
- You Might Not Need an Effect - React 公式
- Keeping Components Pure - React 公式(日本語)
- Synchronizing with Effects - React 公式
Discussion