Reactのライフサイクルにおけるアンチパターンまとめ!初心者でもわかるReactの注意点
生徒
「Reactでライフサイクルを使うときにやってはいけないことってありますか?」
先生
「はい、いくつか典型的なアンチパターンがあります。知らずにやるとバグの原因になったり、パフォーマンスが悪くなったりします。」
生徒
「例えばどんなことですか?」
先生
「例えば副作用の無限ループや、状態管理の誤用などですね。それぞれ詳しく見ていきましょう。」
1. useEffectやuseLayoutEffectでの無限ループ
Reactの副作用フックで依存配列を正しく設定しないと、レンダリングごとに副作用が繰り返されて無限ループになります。例えば、useEffect内で状態を更新し、その状態を依存配列に含めない場合です。
import React, { useState, useEffect } from "react";
function LoopExample() {
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1); // 依存配列が空だと無限ループ
}, []);
return <p>{count}</p>;
}
export default LoopExample;
2. 状態の過剰更新
コンポーネント内で状態を頻繁に更新すると、レンダリングが何度も発生しパフォーマンスが低下します。特に大きなリストや複雑なUIでは顕著です。
対策としては、状態をまとめたり、useMemoやuseCallbackで関数や計算結果をキャッシュすることが有効です。
3. 副作用のタイミングを誤る
useEffectはレンダリング後に実行され、useLayoutEffectはレンダリング直後に実行されます。ここを誤解すると、画面表示が崩れたり、予期せぬ副作用が発生します。
例えばDOMのサイズを計算する場合はuseLayoutEffectを使い、データフェッチなどの非表示処理はuseEffectを使うのが基本です。
4. コンポーネントのアンマウント時の処理忘れ
コンポーネントがアンマウントされる際にタイマーやイベントリスナーを解除しないと、メモリリークやエラーの原因になります。useEffectのクリーンアップ関数で必ず解除しましょう。
import React, { useEffect } from "react";
function TimerExample() {
useEffect(() => {
const id = setInterval(() => console.log("tick"), 1000);
return () => clearInterval(id); // クリーンアップを忘れない
}, []);
return <p>タイマー動作中</p>;
}
export default TimerExample;
5. propsの直接操作や外部状態の誤用
親コンポーネントから渡されたpropsを直接変更することは避けましょう。Reactではpropsは読み取り専用です。また、外部のグローバル状態を直接変更するのもアンチパターンです。
状態を更新する場合は必ずsetStateやContextを通して変更するのが安全です。
6. レンダリングの副作用をUIに混ぜる
レンダリング中にコンソールログやDOM操作などの副作用を直接行うと、並行レンダリングや再レンダリング時に不整合が起こります。副作用はuseEffectやuseLayoutEffect内で行うようにしてください。
7. 重い処理を直接レンダリングに入れる
レンダリング内で重い計算やAPI呼び出しを行うと、画面がフリーズすることがあります。重い処理はuseMemoでメモ化したり、非同期で処理することが推奨されます。
8. イベントハンドラ内で状態の誤更新
ボタンや入力フォームのイベントで状態を連続更新する場合、以前の状態を正しく参照していないと期待通りに動かないことがあります。関数型更新(prevStateを使う)で正確に更新しましょう。
9. useEffectの依存配列の誤設定
依存配列を空にする、あるいは必要な値を含めないと副作用が正しく発火しません。常に必要な値を含めるようにして、Reactの警告も確認してください。
10. コンポーネント分割の不十分
1つのコンポーネントに多くの処理や状態を詰め込みすぎると、ライフサイクルの管理が複雑になり、アンチパターンにつながります。小さく分割して単純なコンポーネントにすることで、ライフサイクルを正しく管理できます。
まとめ
Reactの開発において、ライフサイクルの理解は避けて通れない非常に重要なテーマです。特に、モダンなReact開発の主流である「関数コンポーネント」と「Hooks(フック)」の組み合わせでは、直感的にコードを書いてしまうと、今回紹介したようなアンチパターンに陥りやすい傾向があります。
React開発者が意識すべきポイントの再確認
Reactのレンダリングは、私たちが想像するよりも頻繁に行われます。そのため、「いつ」「どのタイミングで」「何が」実行されるのかを正確に把握しておく必要があります。特に初心者が陥りやすいのが、useEffectの無限ループです。これは、副作用の中で自身の依存関係にある状態(ステート)を更新してしまうことで発生しますが、コードが複雑になればなるほど、どこでループが発生しているのか見つけにくくなります。
また、クリーンアップ関数の重要性も忘れてはいけません。タイマー処理(setInterval)や外部APIとの接続(WebSocket)、あるいはウィンドウのスクロールイベントの監視などを設定した際、コンポーネントが画面から消える(アンマウント)ときにそれらを解除しないと、ブラウザのメモリを無駄に消費し続け、最終的にはアプリケーション全体の動作が重くなる「メモリリーク」を引き起こします。
より実践的なReactコードの書き方
アンチパターンを回避し、より「Reactらしい」堅牢なコードを書くためには、以下のようなベストプラクティスを意識しましょう。例えば、状態を更新する際には「現在の値」に依存しないように、関数型の更新を利用することが推奨されます。
import React, { useState, useEffect, useCallback } from "react";
/**
* ライフサイクルのベストプラクティスを取り入れたカウンターコンポーネント
* 1. 関数型更新による状態管理
* 2. 正確なクリーンアップ処理
* 3. 不要な再生成を防ぐためのメモ化
*/
const SafeCounter = () => {
const [count, setCount] = useState(0);
const [isRunning, setIsRunning] = useState(true);
// 1. 関数型更新を使うことで、countを依存配列に入れずに済む(無限ループ回避)
const tick = useCallback(() => {
setCount((prevCount) => prevCount + 1);
}, []);
useEffect(() => {
let timerId;
if (isRunning) {
// 毎秒カウントアップするタイマーをセット
timerId = setInterval(tick, 1000);
}
// 2. クリーンアップ関数でメモリリークを確実に防ぐ
return () => {
if (timerId) {
clearInterval(timerId);
}
};
}, [isRunning, tick]); // 必要な依存関係のみを指定
return (
<div className="p-4 border rounded shadow-sm bg-light">
<h3 className="text-primary">実践的なタイマー</h3>
<p className="fs-2">現在のカウント: {count}</p>
<button
className="btn btn-warning me-2"
onClick={() => setIsRunning(!isRunning)}
>
{isRunning ? "一時停止" : "再開"}
</button>
<button
className="btn btn-danger"
onClick={() => setCount(0)}
>
リセット
</button>
</div>
);
};
export default SafeCounter;
これからの学習に向けて
Reactは常に進化を続けており、React 18以降の「並行レンダリング(Concurrent Rendering)」の導入により、副作用の扱いはさらに厳格な理解が求められるようになっています。初心者のうちは「動けばいい」と考えがちですが、中長期的なメンテナンス性を考えると、アンチパターンを避け、Reactのライフサイクルという「仕組み」に逆らわないコードを書くことが上達への近道です。
まずは小さなコンポーネントから、依存配列の漏れがないか、不要なレンダリングが発生していないかを確認する癖をつけていきましょう。ブラウザのデベロッパーツールや、React Developer Toolsを活用して、実際にコンポーネントがどのような挙動をしているか可視化するのも非常に効果的です。
生徒
「先生、今回のまとめでライフサイクルの重要性がよく分かりました!useEffectの中で安易にsetCount(状態更新)をしてしまうのが、なぜあんなに危険なのかがようやく腑に落ちました。」
先生
「それは良かったです。useEffectは強力ですが、その分『魔法』のように見えてしまうことがあります。自分が書いたコードが、どのタイミングで、何度実行されるのかを意識するのがプロへの第一歩ですよ。」
生徒
「特にクリーンアップ関数の話が印象的でした。今まで『消えるときの処理』なんてあまり考えていなかったので、タイマーが裏で動き続けるという話を聞いて、自分の過去のコードが怖くなりました(笑)」
先生
「ははは、誰でも最初はそうですよ。でも、メモリリークはユーザーのブラウザを重くする原因になりますから、これからは『セットしたら片付ける』という習慣を大切にしましょう。」
生徒
「はい!あと、依存配列(Dependency Array)の指定も。Reactの公式ドキュメントやリンター(ESLint)が警告を出してくれる理由が分かりました。あえて空にするのと、書くべきなのに書かないのは全然違うんですね。」
先生
「その通りです。Reactのルールを守ることは、結果的にバグの少ない快適なアプリケーションを作ることにつながります。次は、カスタムフックを使ってこれらのロジックをきれいに分離する方法についても学んでいきましょうか。」
生徒
「ぜひお願いします!もっと洗練されたReactのコードが書けるようになりたいです!」