Reactコンポーネントのライフサイクルには3つのフェーズがあります。マウント(コンポーネントが初めて画面に現れる)、更新(state、props、コンテキストが変わったので再レンダリングされる)、アンマウント(取り除かれる)です。関数コンポーネントでは、これらのフェーズに useEffect で反応します。[] を持つエフェクトはマウントの後に、依存値を持つエフェクトはそれが変わった更新の後に実行され、エフェクトが返す関数はアンマウント時に実行されます。
プレビューの下のコンソールに、各フェーズが起きた瞬間に表示されます。
コンソールは render, count = 0 と mounted で始まります。Countをクリックするたびに、レンダリングと更新が記録されます。Unmountで unmounted が記録され、もう一度Mountすると count = 0 からやり直しです。stateがコンポーネントと一緒に捨てられたからです。
マウント
コンポーネントは、Reactがツリーのある場所で初めてそれを描画するときにマウントされます。Reactは関数を呼び、返されたJSXからDOMノードを作って挿入し、ブラウザに描画させてから、コンポーネントのエフェクトを実行します。
useEffect(fn, []) は、コンポーネントが画面にある間ずっと存在すべきものを始める場所です。購読、タイマー、接続、最初のデータリクエストなどです。描画の後に実行されるので、最初のフレームを遅らせることはありません。
更新
コンポーネントは、次のときに更新(再レンダリング)されます。
- 自分のstateが変わったとき
- 親がもう一度レンダリングされたとき(デフォルトでは、propsが変わったかどうかにかかわらず、すべての子もレンダリングされます)
- 読んでいるコンテキストが変わったとき
各更新の後、Reactは依存値が変わったエフェクトを実行します。エフェクトを再実行する前に、そのエフェクトの前回のクリーンアップを実行するので、新しい購読が開く前に古い購読は閉じられます。何がレンダリングのきっかけになるかは、レンダリングのページで詳しく扱っています。
アンマウント
コンポーネントは、親がそれを描画しなくなったとき(上の {show && <Counter />} のように)、その key が変わったとき、またはルート自体がアンマウントされたときにアンマウントされます。ReactはそのDOMノードを取り除き、すべてのエフェクトのクリーンアップを実行し、そのstateを捨てます。後でコンポーネントがまた現れても、それはまったく新しいインスタンスです。
親と子
Reactは上から下へレンダリングしますが、エフェクトは下から上へ実行します。親と子が一緒にマウントされると、子のエフェクトが先に実行されるので、親のエフェクトは子が完全に準備できていることを前提にできます。
マウントでは render Parent、render Child、Child effect、Parent effect が記録されます。Removeを押すと、クリーンアップは逆の順番で実行されます。Parent cleanup、次に Child cleanup です。
レンダーフェーズとコミットフェーズ
各更新は2つのフェーズを経ます。
- レンダーフェーズ。Reactはコンポーネントを呼んで新しいJSXを計算し、前回の結果と比べます。画面にはまだ何の変化もありません。Reactはこの作業を一時停止したり、StrictModeで2回実行したり、捨てたりすることがあります。だからこそレンダリングは純粋でなければなりません。リクエストも購読も、外の変数の変更も禁止です。
- コミットフェーズ。Reactは差分をDOMに適用し、refを付け、
useLayoutEffectのコールバックを実行します。すべてブラウザが描画する前です。useEffectのコールバックはその後、たいていはブラウザが描画してから実行されます。クリックなどの個別の入力が更新のきっかけなら、Reactは代わりにレイアウトエフェクトのすぐ後にそれらを実行し、次のイベントの前に結果を用意します。
クリックするたびに、3行が同じ順番で記録されます。これらのエフェクトには依存配列がないので、レンダリングのたびに実行されます。両方の第2引数に [] を追加すると、最初のレンダリングの後にだけ実行されます。
クラスのライフサイクルメソッドとフックでの同等品
クラスコンポーネントは、ライフサイクルを名前の付いたメソッドとして書き出します。古いコードや、今もクラスが必要なエラーバウンダリで目にするでしょう。
| クラスのメソッド | 実行されるとき | 関数コンポーネントでの同等品 |
|---|---|---|
constructor | 最初のレンダリングの前に1回 | useState(initial)、useRef |
static getDerivedStateFromProps | 毎回のレンダリングの前 | レンダリング中に値を計算する、または key でリセットする |
render | 毎回のレンダリング | 関数の本体 |
componentDidMount | 最初のレンダリングがDOMに反映された後 | useEffect(fn, []) |
shouldComponentUpdate | 更新の前。更新を飛ばせる | React.memo(逆の答え「propsは等しいか」を返す) |
getSnapshotBeforeUpdate | DOMが変わる直前 | 直接のフックはない。refと組み合わせた useLayoutEffect が一番近い |
componentDidUpdate | 各更新の後 | useEffect(fn, [deps])(マウントの後にも実行される) |
componentWillUnmount | 取り除かれる前 | useEffect が返すクリーンアップ |
getDerivedStateFromError、componentDidCatch | 子が例外を投げたとき | なし。エラーバウンダリのクラスを書く |
次のクラスは、実行されるすべてのメソッドをログに出します。
マウントでは constructor、getDerivedStateFromProps、render、componentDidMount が記録されます。クリックすると getDerivedStateFromProps、shouldComponentUpdate、render、getSnapshotBeforeUpdate、componentDidUpdate が記録されます。shouldComponentUpdate が false を返すようにすると、クリックしてもボタンは更新されなくなります。stateは変わりますが、Reactがレンダリングを飛ばすのです。このようなクラスを関数に変換する方法は、クラスコンポーネントのページで紹介しています。
ライフサイクルではなく同期で考える
フックの対応表は古いコードを読むのに役立ちますが、大きな「マウントした」エフェクトと大きな「更新した」エフェクトを1つずつ作るような、クラスの癖をそのまま写したエフェクトを書かせてしまうことがあります。エフェクトは「この外部のものをこれらの値と同期させておく」と読むほうがよいのです。roomId に接続し、roomId が変わったら切断してもう一度接続する、という具合です。同期させるもの1つにつきエフェクトを1つ書き、それが使う値を依存値として並べ、いつ実行するかはReactに任せてください。
よくある質問
Reactコンポーネントのライフサイクルにはどんなフェーズがありますか?
マウント(Reactがコンポーネントを作り、その出力を画面に置く)、更新(state、props、コンテキストが変わったので再レンダリングされる)、アンマウント(取り除かれ、後片付けをする)です。
componentDidMountに相当するフックは何ですか?
useEffect(() => { ... }, []) です。空の依存配列によって、最初のレンダリングが画面に出た後に1回だけ実行されます。それが返す関数が componentWillUnmount に相当します。
componentDidUpdateに相当するフックは何ですか?
[userId] のように、気にかける値を依存配列に入れた useEffect です。componentDidUpdate と違って最初のレンダリングの後にも実行されるので、マウントと更新をまとめて扱います。
親と子のエフェクトはどの順番で実行されますか?
Reactは親から下へレンダリングしますが、エフェクトは子から上へ実行します。子のエフェクトが親のエフェクトより先に実行されます。アンマウント時は、親のクリーンアップが子のクリーンアップより先に実行されます。
レンダーフェーズとコミットフェーズとは何ですか?
レンダーフェーズでは、Reactがコンポーネントを呼んで画面がどう見えるべきかを計算します。この作業は一時停止されたり捨てられたりすることがあります。コミットフェーズでは、変更をDOMに適用し、レイアウトエフェクトを実行します。通常のエフェクトはその後、たいていはブラウザが描画してから実行されます。