Reactアプリケーションはコンポーネントから構築されており、すべてのコンポーネントには「命」があります。画面に現れ、新しい情報に反応し、やがて消え去ります。このリズムを無視すれば、その代償は現実のものとなります。メモリリークが忍び寄り、レンダリングパフォーマンスが低下します。ユーザーがボタンをクリックしても反応しません。なぜなら、そのボタンに紐付いていたコンポーネントは10分前にアンマウントされているのに、イベントリスナーだけが生き残っているからです。コンポーネントのライフサイクルを理解することは、単なる学術的な演習ではありません。それは、実際の利用環境下でも信頼性を維持できるソフトウェアを書くための基盤なのです。
コンポーネントの存在を3つのステージで考えてみてください。開発者はしばしば、それらを「誕生」「成長」「死」と表現します。Reactでは、これらをmounting、updating、unmountingと呼びます。各ステージには特定の役割があり、コードを実行したりクリーンアップしたりするための正確なタイミングが用意されています。
3つのフェーズ
Mountingは誕生です。コンポーネントが作成され、初めてブラウザのDOMに挿入されます。これはセットアップのためのウィンドウです。コンポーネントがリモートデータを必要とする場合、このタイミングでリクエストを行います。propsから計算された初期stateが必要な場合も、ここで行います。クラスコンポーネントの時代、これはcomponentDidMountの領域でした。現代の関数コンポーネントでは、useEffectと空の依存配列(dependency array)を使用してこれを処理します。重要な詳細は、このコードブロックがReactによって要素がページに配置された直後に一度だけ実行されるということです。これにより、ユーザープロファイルの取得やlocalStorageからの値の読み取りといった、一度限りのセットアップに最適となります。
Updatingは成長です。マウントされたコンポーネントは静止していません。親からpropsが届き、ユーザーの操作によってstateが変化します。親コンポーネント自体が再レンダリングされると、子コンポーネントもそれに従います。Reactは、UIをデータと同期させるためにコンポーネントを再レンダリングします。このフェーズはコンポーネントの寿命の間に何度も繰り返されます。依存関係(dependency)が変化するたびにエフェクトが再度実行され、コンポーネントは新しい出力をDOMへと送り出します。このフェーズは非常に頻繁にトリガーされるため、ほとんどのパフォーマンス問題が発生する場所でもあります。ここでの不要な作業は、急速に蓄積していきます。
Unmountingは死です。ユーザーが別のページに遷移したり、条件付きレンダリングによって切り替わったりすることで、コンポーネントが画面から消えます。この時点でコンポーネントは破棄されますが、外部の世界は必ずしもそれを知りません。タイマーは動き続け、WebSocketはリスニングを続け、windowオブジェクト上のイベントリスナーは紐付いたままになります。これらをクリーンアップしないと、死んだコンポーネントへの参照が保持され、メモリリークが発生します。ブラウザは「幽霊」のためにサイクルを浪費することになります。Reactがクリーンアップ関数を提供しているのは、まさにコンポーネントが完全に消え去る前に、インターバルを停止し、接続を閉じ、リスナーを削除できるようにするためです。
クラスメソッドからHooksへ
長年、Reactエンジニアはクラスコンポーネントを通じてこれらのフェーズを管理してきました。ロジックは個別のメソッドに分割されていました。componentDidMount内でデータを取得し、componentDidUpdate内でpropsの変化に対応し、componentWillUnmount内で後片付けを行っていました。問題は、関連するロジックが分散してしまったことです。例えばライブチャットの接続のような単一の機能であっても、接続、スレッド変更のチェック、切断を行うために、3つの異なるライフサイクルメソッドにコードを書く必要がありました。
Hooksがそれを変えました。関数コンポーネントのuseEffectフックを使用すると、ライフサイクルのフェーズごとではなく、目的ごとにロジックをグループ化できます。それでも、どこを見るべきかを知っていれば、メンタルモデルはきれいに対応しています。
マウンティングを処理するには、useEffect(() => { ... }, [])と記述します。空の依存配列は、このエフェクトに外部の依存関係がないことをReactに伝えるため、初回レンダリングの後にのみ実行されます。これはcomponentDidMountの代わりとなります。
更新を処理するには、Reactが監視すべき特定の値を指定します:useEffect(() => { ... }, [dependency])。レンダリング間でdependencyが変化すると、
