Hooks時代のReactライフサイクル完全理解への道
はじめに
これはクラスコンポーネントのライフサイクルを理解した上で、それに対応するように関数コンポーネントのライフサイクルを理解しようという試みです。
厳密にはReactのライフサイクルはクラスコンポーネントと関数コンポーネントそれぞれで違う概念を持っているようで、それぞれのライフサイクルに紐付けて考えるという行為自体がナンセンスな可能性がありますが、理解の手助けになれば幸いです。
そのため、なるべくわかりやすくするために、厳密でない言い方をすることがあります。ご了承ください。
もし明らかにまずい言い回し、もしくは間違って認識しているものがある場合はコメントにて教えていただけると嬉しいです。
モチベーション
僕はReactの経験がクラスコンポーネントはちょっとだけ書いたことがあるくらいで、ほぼ関数コンポーネント×hooksから入ったようなもなのでいまいちライフサイクルが理解できていない。
Vueをずっと書いてきたが本当はReactが好きで、ここ一年ほどReactをやらせてもらってる中でVueの用語との違いに戸惑ったので、そのあたりもしっかりまとめたい。
特にuseEffectなどが出てくるとすごく難しいのでこれを機にしっかりと理解する。
用語
公式ドキュメントを見てびっくりしたが、「レンダリング」という言葉が使われていない。
その代わり「レンダー」「マウント」「描画」という言葉が使われている。
Reactの記事を見るとレンダリングという言葉がよく使われているがそれが何を示しているのか結構難しい。
例えば、「render関数が実行されること」がレンダリングの意味としてそれっぽいが、関数コンポーネントの場合「レンダリング」は、stateの初期化など含めた「関数コンポーネントが実行されること」を示しているようなことが多い。(この後の文の追記にて説明しています。)
よってここでは用語の意味をちゃんと定義したい。
曖昧さを回避するため、レンダリングという言葉は避ける。
また、ここで定義した言葉がこの記事内でこれらの意味以外で使われることはないことを約束する。
おそらく公式ドキュメントで使われる意味とほぼ同じ意味で定義できたと思うので、例えば僕のようにVueから入った人には公式ドキュメントのお供にも役立つかも??
マウント(Mount)
これが描画と言われる(Vueだと描画とほとんど意味が変わらない)ことがあるが、Reactの用語で「マウント、更新、マウント解除」という言葉があるので描画とは違う意味の用語として扱う。
意味はこのリンクのマウントのこと。つまり、一番最初にコンポーネントが初期化され、DOMが描画され、その後のフック(componentDidMount)が呼び出される一連の流れのこと。更新
「マウント、更新、マウント解除」の更新。
繰り返しになるが、この記事の中で更新と言ったらこの意味以外で使うことはないので注意してほしい。
意味はこのリンクの更新のこと。つまり、propsやstateが変更された時に、DOMが再描画され、その後のフック(componentDidUpdate)が呼び出される一連の流れのこと。
レンダー(render)、描画
renderメソッドが呼ばれ、ブラウザにDOMが描画されること。
関数コンポーネントでは、returnされたJSXがDOMとしてブラウザに描画されること===renderメソッドが呼ばれること。
stateの初期化、再計算などは含まない。
クラスコンポーネントのライフサイクル
クラスだとライフサイクルがとてもよくわかりやすい。
なぜならインスタンスの生き死にとデータがDOMへ反映されるタイミングがそのままライフサイクルと呼べるからである。
つまりインスタンス、DOMの概念がわかっていればライフサイクルがわかる。
順番
それに沿って見ていこう。
マウント
一番最初にコンポーネントが初期化され、DOMが描画され、その後のフック(componentDidMount)が呼び出される一連の流れのこと。
-
constructor
-
render
-
componentDidMount
更新
propsやstateが変更された時に、DOMが再描画され、その後のフック(componentDidUpdate)が呼び出される一連の流れのこと。
-
render
-
componentDidUpdate
アンマウント
componentWillUnmountが呼ばれた後、マウントが解除(インスタンスが死に、DOMが画面から消える)されるまでの一連の流れのこと。
- componentWillUnmount
各ライフサイクルメソッドの説明
-
constructor
クラスがインスタンス化される時に呼ばれる。
初期化を行う。
この時点でブラウザにDOMは描画されない。 -
render
ブラウザにDOMを表示する。
もしくはDOMに描画済みのデータを更新する。 -
componentDidMount
マウント時に、DOMが描画された後に実行される。
APIリクエスト、描画されたDOMにアクセスする必要のある処理などをここに書く。
更新時には実行されない。 -
componentDidUpdate
state,propsが変化した更新時に、DOMが描画された後に実行される。
しばしばcomponentDidMountと全く同じ処理が書かれる。
マウント時には実行されない。 -
componentWillUnmount
コンポーネントが破棄される前に呼ばれる。
setIntervalなどの後片付けに使われる。
関数コンポーネントのライフサイクル
ここからが本番。
なんと公式ドキュメントには関数コンポーネントのライフサイクルフックについて、記述がない。
というのもおそらくuseEffectという概念がかなり色々なタイミングで色々なことをするので難しいのではないだろうか。
ここでは先ほどのReactLifeCycleMethodsDiagramのマウント,更新、アンマウントに沿って可能な限り正確にライフサイクルを解き明かしていく。
定義
名前すらないので各場所に勝手に名前をつけていく。
順番の説明にはこの名前を使う。
もちろんこれらは僕が勝手に作った用語なので他で話しても通用しないので注意!
function Example() {
// 「initialize」
useEffect(() => {
// 「effect」
return () => {
// 「removeEffect」
}
});
// 「render」
return <></>;
}
順番
マウント
-
initialize
-
render
-
effect
更新
-
initialize
-
render
-
removeEffect
-
effect
ここで驚いた人もいると思うが、なんとuseEffectのクリーンアップ関数はアンマウント時だけではなく、更新時のeffectが呼ばれる前にも呼ばれる。ただし、マウント時には呼ばれない。
アンマウント
- removeEffect
各ライフサイクルの説明
ちなみに関数コンポーネントにそんなのものはないので、ライフサイクルメソッドとは呼ばない。
-
initialize
setStateやメソッドの定義、つまり初期化を行う。
この時点でDOMは描画されていないと思いきや、更新時は描画されていることがある。
が、それはすでにReactの手を離れた描画済みのものであり、ここでDOMを参照することはできない。(はず)
副作用は必ずuseEffectを使う。 -
render
ブラウザにDOMを表示する。
もしくはDOMに描画済みのデータを更新する。 -
effect
副作用を起こす。
ただし、依存配列が指定されている場合はそれによって実行されるかされないかが変わる。
componentDidMountとcomponentDidUpdateが一緒になった的な位置付けだが厳密には違う。 -
removeEffect
副作用のクリーンアップがされる。
ただし、依存配列が指定されている場合はそれによって実行されるかされないかが変わる。
componentWillUnmount的な位置付けだが厳密には違う。
関数コンポーネントとクラスコンポーネントのライフサイクルの違い
特徴的なのは、
- constructorはマウント時にしか呼ばれないが、initializeは更新時にも呼ばれる
- componentWillUnmountはアンマウント時にしか呼ばれないが、removeEffectは更新時にeffectが実行されるならその前に呼ばれる
constructorはマウント時にしか呼ばれないが、initializeは更新時にも呼ばれる
これが結構個人的によくわかっていなかった。
例えば、stateやpropsが変わった場合にもinitializeされる。
これちゃんと気をつけて置かないと、最近のライブラリのswrやreactQuery、カスタムフックの初期化がここで行われるのでそこで重い処理をしている、もしくは冪等でない処理が走る場合バグやパフォーマンス低下の原因になりそう。
そのためにメモ化があるのか!と今思った。
追記:クラスコンポーネントに対する関数コンポーネントの位置付け
僕の知らない事実を教えてもらったので共有。
なんと関数コンポーネントはクラスコンポーネントのrender関数そのものらしい。
これが巷で関数が実行されることが「レンダリング」と呼ばれていることの理由だったわけだ!
それを避けるためにはメモ化して関数の実行そのものを防ぐ必要がある。
メモ化
先ほども書いたようにinitializeは更新時にも実行される。
よってこのような形でuseCallbackやuseMemoを使っての最適化が必要となる。
本筋とは逸れるので、簡潔にまとまった素晴らしい記事を置いておく。
まとめ
クラスコンポーネントのライフサイクルの概念を強引に関数コンポーネントに当てはめるというちょっと無理矢理な企画だったが、個人的にはすごく勉強になった。
他の人にもそうであることを願っている。
あと用語も結構気を遣って書いたので、これで他のReact記事もなんか良い感じに用語の統制が行われてくれると嬉しい。
こうやってみるとやっぱりReactってすこし難しいなぁと思ってしまうが、これは多分Vueが簡単とかではなくて関数に状態を持たせるというパラダイムシフトがもたらしたちょっとした成長痛なのかなと思ったりする。
事実、Vueのcomposition apiとかはパフォーマンスの面倒こそ見てくれるものの、リアクティブの面倒を見てくれなくなったり、なんかリアクティブな値に.valueでアクセスすることを強制したりとなかなかだ。
とはいえ僕はReactが好きで、Vueも今のところ長くやっててキャリア的に有利があるのでやめるつもりもない。
関係ないが関数と言えば最近はHaskellにハマっててヤバい関数型のコードを書いてニヤニヤしている。
もっと理解して、美しく、機能的に書いていこう!
Discussion
React 日本語サイトのメンテナです。訳語集の作成も担当しました。「レンダリング」と「レンダー」について言及している人を初めて見て、ちょっと嬉しくなったのでコメントします。
…といっても「レンダリング」と「レンダー」については大した意味はありません。「ゲッティングする」とか「インプッティングする」とか「ダウンローディングする」とか普通言わないのに、この語だけ「レンダリングする」と訳すべき本質的理由が見いだせなかったため、当時の自分が何となく「レンダー」に統一することにした、という、それだけです。「ホバリングする」のように状態の継続を表すならまだ分かるのですが、レンダーにはそのような意味もないですし。
一方で仰るとおり render(ing) を「レンダー」と「描画」で、意図的に訳し分けるルールにしています。前者は React が
render()
や関数コンポーネントの本体を呼び出すこと、後者はブラウザが画面に DOM を反映する動作のこと、というルールになっています。元々の英語ドキュメントの時点でも、render の意味するところが文脈によって曖昧でたまに何を指すのかよく分からないという問題がありまして(「render という動詞がオーバーロードされている!」と文句言われてました)、日本語独自で上記のような使い分けを導入しています。なお現在準備中の新ドキュメントでは英語版でもその点が厳格になり、前者を render、後者を paint と呼称することになりそうです。
なんと!
日本語サイトメンテナンスありがとうございます…!とても助かっています!
なるほど!技術記事では結構レンダリングという言葉が使われるので、それは公式ではどういう使い方をされているんだろうと思って調べて驚いた次第でした。
やはりそうでしたか!
しかも英語では使い分けされておらず、日本語サイト独自だったとは、、
わかりやすいメンテナンスありがとうございます!
とても勉強になり理解が深まりました…!
わざわざコメントありがとうございます!!