レンダリングとマウントの違い
レンダリングとマウントの違いが書いてある記事
ライフサイクルについての記事
再レンダリングについての記事
コンポーネントをまとめてみる
React Hooks in Action という本をまとめてみた
Reactの強みはアプリケーションとコンポーネントのstateをUIとどのように同期させるかということ。ユーザの行動によってステートが変化するとき、Reactはブラウザに表示されているDOMの変更箇所を計算する。(最新のデータと画面に表示されてるものを一致させる)
こんなふうにQuizコンポーネントを例に考える。このコンポーネントには次のようなものを定義している。
- State
- イベントハンドラー
- 副作用
- クリーンアップ関数
- UI のreturn
JavaScript では関数の中に関数を定義できるので、関数コンポーネントの中にさまざまな関数を定義できる。
クラスコンポーネントに比べて関数コンポーネントは次のような利点がある
- コードが短く、整理してかける
- コンストラクタで
super
を呼び出さなくていい -
this
を使ってハンドラーを結びつけなくていい - ライフサイクルのモデルがシンプル
- ローカルステートが、副作用やUIと同じスコープで書ける
この画像のように、クラスコンポーネントでは副作用のコードはライフサイクルメソッド(componentDidMount
、componentWillUnmount
、componentWillUpdate
)に強く関連していた。しかし関数コンポーネントでは、useEffect
として同じ動作のコードは1つにまとめることができる。Hooksにカプセルかされた。
これをみる限り、Reactにおいてライフサイクルがコードの前面に出てくるのは副作用、useEffect
を扱うときみたい。関数コンポーネントにおいて、ライフサイクルメソッドは使われないので
useEffect
の構文とライフサイクルの関係を表す図があったので貼り付ける。
これをみると、componentDidMount
とcomponentWillUpdate
に書く関数に対応するのがuseEffect
本体のスコープに書くもので、componentWillUnmount
に書く関数に対応するものがuseEffect
のreturn
メソッドのスコープ書くものだとわかる。このようにライフサイクルメソッドは構文として生まれ変わった。
もちろん、クラスコンポーネントと関数コンポーネントを無理やり対応づけたものなので、役割は似ているが同じものではない。
関数コンポーネントのライフサイクルはなんなんだ
クラスコンポーネントでは以下の図のようにライフサイクルははっきりしていた。しかし関数コンポーネントではライフサイクルという用語はドキュメントでも使われていない。なので関数コンポーネントを使う限り、ライフサイクルを考えるのは不適切なのかなとも思ったが、あえて関数コンポーネントのライフサイクルを考えてみたい。
しかしその作業は先人がすでにやっているので、その記事から必要な箇所を引用することにする。
関数コンポーネントのライフサイクル
定義
参照先の記事では関数コンポーネントのライフタイムを「マウント」「更新」「アンマウント」の3段階に分けている。また、関数コンポーネントのスコープを「initialize」「effect」「removeEffect」「render」の4パーツに分けて、それぞれのライフタイムで関数コンポーネントのコードがどの順番で実行されていくのかを分けている。4パーツは次のように分けることができる。
function Example() {
// 「initialize」
useEffect(() => {
// 「effect」
return () => {
// 「removeEffect」
}
});
// 「render」
return <></>;
}
また、各ライフサイクルで実行される順番は次のようなものである。
ライフタイム | スコープの実行順 |
---|---|
マウント | initialize → render → effect |
更新 | initialize → render → removeEffect → effect |
アンマウント | removeEffect |
-
removeEffect
に記述するクリーンアップ関数は、アンマウント時だけでなく、更新時のeffectが呼ばれる前にも呼ばれる。
各スコープの説明
initialize
- setStateやメソッドの定義、つまり初期化を行う
- この時点でDOMは描画はされていないが、更新時はされていることあル。が、それはすでにReactの手を離れた描画済みのものであり、ここでDOMを参照することはできない
- 副作用は必ず
useEffect
を使う
renderer
- ブラウザにDOMを表示する
- もしくはDOMに描画済みのデータを更新する
effect
- 副作用を起こす
- ただし、依存配列が指定されている場合はそれによって、実行されるかされないかが変わる。
-
componentDidMount
とcomponentDidUpdate
が一緒になった位置付けだが厳密には違う
removeEffect
- 副作用のクリーンアップが行われる
- ただし、依存配列が指定されている場合はそれによって、実行されるかされないかが変わる
-
componentWillUnmount
のような位置付けだが厳密には違う
関数コンポーネントとクラスコンポーネントのライフサイクルの違い
- (クラスコンポーネントの)
constructor
はマウント時にしか呼ばれないが、(関数コンポーネントの)initialize
は更新時にも呼ばれる -
componentWillUnmount
はアンマウント時にしか呼ばれないが、removeEffect
は更新時にeffect
が実行されるならその前に呼ばれる
たとえばstateやpropsが変わった場合にもinitialize
される。
reqctQueryやカスタムフックの初期化がinitialize
で行われているので、重たい処理や冪等でない処理が走る場合、バグやパフォーマンス低下の原因になるかもしれない
そのためにメモ化がある
クラスコンポーネントに対する関数コンポーネントの位置付け
関数コンポーネントはクラスコンポーネントのrender関数そのもの
だからマウント、更新のたびに関数コンポーネントに書かれた全てが実行されてしまう。
ちまたで関数が実行されることが「レンダリング」と呼ばれていることの由来
それを避けるためにはメモ化して関数の実行そのものを防ぐ必要がある
メモ化
initialize
は更新時にも実行される。useCallback
やuseMemo
を使っての最適化が必要となる
雑感
このスクラップには関数コンポーネントのライフサイクルしか書かなかったが、クラスコンポーネントのライフサイクルと合わせて考えたらわかりやすい。また、もう少し洗練された図として次のような画像があった。灰色で囲まれている箇所がライフタイムとして書いたものと対応している。またクラスコンポーネントのライフサイクルとも対応しているので比較するといい。
useEffect
は使わないほうがいい論
ネットを見るとuseEffect
は使わないほうがいいと書かれた記事がよくある。自分も、useEffect
について調べるまではライフサイクルなぞ考えず、「外部と通信するときにだけ使うもの」というふうな雑な理解をしていた。
これについては今はわからないので後で詳しく調べたい
Reactの関数コンポーネントには純粋性を保つことが求められている。純粋から外れるものをuseEffectに書くように進められている。純粋性とクラスコンポーネントで使われていた3つのライフタイムは何か関係があるのか?この3つでは値が変化しうる箇所だったのか
「副作用」という単語はどうやら関数型プログラミングの用語らしい。「関数の純粋性」も用語の1つだった。もしかしたらライフサイクルという文脈でReactを捉えるのが筋違いかもしれない。
上のReactのドキュメントで気になる箇所があったのでメモ
- 副作用は通常、イベントハンドラに属します。イベントハンドラは、ボタンがクリックされたといった何らかのアクションんが実行されたときにReactが実行する関数です。イベントハンドラは、コンポー年との「内側」で定義されているものではありますが、レンダーの「最中」に実行されるわけではありません!つまり、イベントハンドラは純粋である必要はありません。
- いろいろ探してもあなたの副作用を書くのに適切なイベントハンドラがどうしても見つからない場合は、コンポーネントから返されたJSXに
useEffect
呼び出しを付加することで副作用を付随させることも可能です。これにより、Reactに、その関数をレンダーの後(その時点なら副作用が許されます)で呼ぶように指示できます。ただしこれは最終手段であるべきです。
というかドキュメントはuseEffectやstateについて色々書いててためになるので読むべきだった。
レンダリングとマウントの違い
この記事がわかりやすいので内容を書写する
マウントとは
Reactコンポーネントに対応するインスタンスとDOMノードの作成と、それをDOMツリーへの追加を行う処理。簡単に言えば、該当Reactコンポーネントを画面に表示するために行われる最初の処理のこと
マウントは1つの処理を指しているのではなく、 initialize
→ render
→ effect
からなる一連の処理
アンマウントとは
DOMツリーからDOMノードを削除すること。
アンマウントが起きるとき
- 条件によるレンダリングの変更
- 親コンポーネントのアンマウント
- キーの変更
- ルーティングによる切り替え
レンダリングとは
レンダリングという言葉は公式ドキュメントで使われていない。
この記事でReactの日本語サイトのメンテナーが書き込んでた。render(ing)を「レンダー」と「描画」で訳し分けるルールにしていると言っている。「レンダー」をReactがrender()
や関数コンポーネントの本体を呼び出すこと。「描画」をブラウザが画面にDOMを反映する動作のこと。
しかしレンダリングとレンダーの言葉が指している意味はほぼ変わらない。記事の書き手が意識している処理は同じだから!
下の表から分かるように、render
はマウント時と更新時でレンダリングが行われる。
ライフタイム | スコープの実行順 |
---|---|
マウント | initialize → render → effect |
更新 | initialize → render → removeEffect → effect |
アンマウント | removeEffect |
React Hooks Cycle の図からも分かるように、ReactコンポーネントをDOMに出力するまでをレンダーフェーズとコミットフェーズに分けて考えられることが多い。
マウントとレンダーの違い
マウントは、最初にReactコンポーネントがDOMに出力されるときに行われる一連の処理
レンダーは、ReactコンポーネントをDOMに出力するために様々な情報が読み込まれること。
マウント処理の中にレンダーは含まれるが、レンダーはマウント時のみ動くわけではなく、更新時にも動く。
各ライフサイクルが発生するタイミング
さっきから何度か書いたこの図は、各ライフサイクルで行われる処理の対応を書いているが、いつライフサイクルが切り替わるのかを書いてないから実用的でない。なのでそれぞれのライフサイクルに切り替わるタイミングをまとめておく。
ライフサイクル | スコープの実行順 |
---|---|
マウント | initialize → render → effect |
更新 | initialize → render → removeEffect → effect |
アンマウント | removeEffect |
マウント時の具体例
コンポーネントが初めてレンダリングされたとき
これは分かる。
条件付きレンダリングによってDOMツリーに追加されたとき
ボタンをクリックしてshow
をtrue
にすると、Child
コンポーネントが初めて表示されてマウントが発生する。再度非表示にしてから再表示しても、毎回新しくマウントされる。
function App() {
const [show, setShow] = React.useState(false);
return (
<div>
<button onClick={() => setShow(!show)}>
{show ? "Hide" : "Show"} Child
</button>
{show && <Child />}
</div>
);
}
function Child() {
React.useEffect(() => {
console.log("Child コンポーネントがマウントされました");
}, []);
return <div>Child Component</div>;
}
- 親コンポーネントが再マウントされたとき
親コンポーネントがマウントされ直す場合、その子コンポーネントもサイドマウントされる。
key
を変更するとコンポーネントはアンマウントし、新しいコンポーネントとしてマウントする
function Parent() {
const [key, setKey] = React.useState(1);
return (
<div>
<button onClick={() => setKey(key + 1)}>Re-mount Parent</button>
<Child key={key} />
</div>
);
}
function Child() {
React.useEffect(() => {
console.log("Child コンポーネントがマウントされました");
}, []);
return <div>Child Component</div>;
}
マウント時の主な処理
- データの取得(APIコール)。サーバーやローカルストレージからデータを取得し、状態を初期化する
- イベントリスナーの登録
- アニメーションや初期化処理
更新時の具体例
- stateの変更
- 親から渡されるpropsの変更
- 親の再レンダリング
- Contextの変更
state
)が変更された場合
状態(Stateが変化すると、そのコンポーネントは再レンダリングされる。Stateの変更は多くの場合、コールバックかuseEffect
フックのどちらかで発生する。
props
)が変更される場合
親コンポーネントから渡されるプロパティ(function Parent() {
const [value, setValue] = React.useState(0);
return (
<div>
<Child value={value} />
<button onClick={() => setValue(value + 1)}>更新</button>
</div>
);
}
function Child({ value }) {
React.useEffect(() => {
console.log("Child コンポーネントが更新されました: ", value);
});
return <div>Value: {value}</div>;
}