Open8

レンダリングとマウントの違い

ひげひげ

コンポーネントをまとめてみる

React Hooks in Action という本をまとめてみた

Reactの強みはアプリケーションとコンポーネントのstateをUIとどのように同期させるかということ。ユーザの行動によってステートが変化するとき、Reactはブラウザに表示されているDOMの変更箇所を計算する。(最新のデータと画面に表示されてるものを一致させる)

こんなふうにQuizコンポーネントを例に考える。このコンポーネントには次のようなものを定義している。

  • State
  • イベントハンドラー
  • 副作用
  • クリーンアップ関数
  • UI のreturn

JavaScript では関数の中に関数を定義できるので、関数コンポーネントの中にさまざまな関数を定義できる。

クラスコンポーネントに比べて関数コンポーネントは次のような利点がある

  • コードが短く、整理してかける
  • コンストラクタでsuperを呼び出さなくていい
  • thisを使ってハンドラーを結びつけなくていい
  • ライフサイクルのモデルがシンプル
  • ローカルステートが、副作用やUIと同じスコープで書ける

この画像のように、クラスコンポーネントでは副作用のコードはライフサイクルメソッド(componentDidMountcomponentWillUnmountcomponentWillUpdate)に強く関連していた。しかし関数コンポーネントでは、useEffectとして同じ動作のコードは1つにまとめることができる。Hooksにカプセルかされた。

ひげひげ

これをみる限り、Reactにおいてライフサイクルがコードの前面に出てくるのは副作用、useEffectを扱うときみたい。関数コンポーネントにおいて、ライフサイクルメソッドは使われないので
useEffectの構文とライフサイクルの関係を表す図があったので貼り付ける。

これをみると、componentDidMountcomponentWillUpdateに書く関数に対応するのがuseEffect本体のスコープに書くもので、componentWillUnmountに書く関数に対応するものがuseEffectreturnメソッドのスコープ書くものだとわかる。このようにライフサイクルメソッドは構文として生まれ変わった。

もちろん、クラスコンポーネントと関数コンポーネントを無理やり対応づけたものなので、役割は似ているが同じものではない。

ひげひげ

関数コンポーネントのライフサイクルはなんなんだ

クラスコンポーネントでは以下の図のようにライフサイクルははっきりしていた。しかし関数コンポーネントではライフサイクルという用語はドキュメントでも使われていない。なので関数コンポーネントを使う限り、ライフサイクルを考えるのは不適切なのかなとも思ったが、あえて関数コンポーネントのライフサイクルを考えてみたい。

https://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/

しかしその作業は先人がすでにやっているので、その記事から必要な箇所を引用することにする。
https://zenn.dev/yodaka/articles/7c3dca006eba7d#関数コンポーネントのライフサイクル

ひげひげ

関数コンポーネントのライフサイクル

定義

参照先の記事では関数コンポーネントのライフタイムを「マウント」「更新」「アンマウント」の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

  • 副作用を起こす
  • ただし、依存配列が指定されている場合はそれによって、実行されるかされないかが変わる。
  • componentDidMountcomponentDidUpdateが一緒になった位置付けだが厳密には違う

removeEffect

  • 副作用のクリーンアップが行われる
  • ただし、依存配列が指定されている場合はそれによって、実行されるかされないかが変わる
  • componentWillUnmountのような位置付けだが厳密には違う

関数コンポーネントとクラスコンポーネントのライフサイクルの違い

  • (クラスコンポーネントの)constructorはマウント時にしか呼ばれないが、(関数コンポーネントの)initializeは更新時にも呼ばれる
  • componentWillUnmountはアンマウント時にしか呼ばれないが、removeEffectは更新時にeffectが実行されるならその前に呼ばれる

たとえばstateやpropsが変わった場合にもinitializeされる。
reqctQueryやカスタムフックの初期化がinitializeで行われているので、重たい処理や冪等でない処理が走る場合、バグやパフォーマンス低下の原因になるかもしれない
そのためにメモ化がある

クラスコンポーネントに対する関数コンポーネントの位置付け

関数コンポーネントはクラスコンポーネントのrender関数そのもの
だからマウント、更新のたびに関数コンポーネントに書かれた全てが実行されてしまう。
ちまたで関数が実行されることが「レンダリング」と呼ばれていることの由来
それを避けるためにはメモ化して関数の実行そのものを防ぐ必要がある

メモ化

initializeは更新時にも実行される。useCallbackuseMemoを使っての最適化が必要となる

雑感

このスクラップには関数コンポーネントのライフサイクルしか書かなかったが、クラスコンポーネントのライフサイクルと合わせて考えたらわかりやすい。また、もう少し洗練された図として次のような画像があった。灰色で囲まれている箇所がライフタイムとして書いたものと対応している。またクラスコンポーネントのライフサイクルとも対応しているので比較するといい。

https://github.com/Wavez/react-hooks-lifecycle

https://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/

ひげひげ

useEffectは使わないほうがいい論

ネットを見るとuseEffectは使わないほうがいいと書かれた記事がよくある。自分も、useEffectについて調べるまではライフサイクルなぞ考えず、「外部と通信するときにだけ使うもの」というふうな雑な理解をしていた。

これについては今はわからないので後で詳しく調べたい

Reactの関数コンポーネントには純粋性を保つことが求められている。純粋から外れるものをuseEffectに書くように進められている。純粋性とクラスコンポーネントで使われていた3つのライフタイムは何か関係があるのか?この3つでは値が変化しうる箇所だったのか
https://ja.react.dev/learn/keeping-components-pure

「副作用」という単語はどうやら関数型プログラミングの用語らしい。「関数の純粋性」も用語の1つだった。もしかしたらライフサイクルという文脈でReactを捉えるのが筋違いかもしれない。

上のReactのドキュメントで気になる箇所があったのでメモ

  • 副作用は通常、イベントハンドラに属します。イベントハンドラは、ボタンがクリックされたといった何らかのアクションんが実行されたときにReactが実行する関数です。イベントハンドラは、コンポー年との「内側」で定義されているものではありますが、レンダーの「最中」に実行されるわけではありません!つまり、イベントハンドラは純粋である必要はありません
  • いろいろ探してもあなたの副作用を書くのに適切なイベントハンドラがどうしても見つからない場合は、コンポーネントから返されたJSXにuseEffect呼び出しを付加することで副作用を付随させることも可能です。これにより、Reactに、その関数をレンダーの後(その時点なら副作用が許されます)で呼ぶように指示できます。ただしこれは最終手段であるべきです

というかドキュメントはuseEffectやstateについて色々書いててためになるので読むべきだった。

ひげひげ

レンダリングとマウントの違い

この記事がわかりやすいので内容を書写する
https://web.archive.org/web/20230603114643/https://de-milestones.com/react-mount-rendering/

マウントとは

Reactコンポーネントに対応するインスタンスとDOMノードの作成と、それをDOMツリーへの追加を行う処理。簡単に言えば、該当Reactコンポーネントを画面に表示するために行われる最初の処理のこと
マウントは1つの処理を指しているのではなく、 initializerendereffectからなる一連の処理

アンマウントとは

DOMツリーからDOMノードを削除すること。

アンマウントが起きるとき

  • 条件によるレンダリングの変更
  • 親コンポーネントのアンマウント
  • キーの変更
  • ルーティングによる切り替え

レンダリングとは

レンダリングという言葉は公式ドキュメントで使われていない
この記事でReactの日本語サイトのメンテナーが書き込んでた。render(ing)を「レンダー」と「描画」で訳し分けるルールにしていると言っている。「レンダー」をReactrender()や関数コンポーネントの本体を呼び出すこと。「描画」をブラウザが画面にDOMを反映する動作のこと。

しかしレンダリングとレンダーの言葉が指している意味はほぼ変わらない。記事の書き手が意識している処理は同じだから!
https://zenn.dev/link/comments/b5b9164d12b13b

下の表から分かるように、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ツリーに追加されたとき

ボタンをクリックしてshowtrueにすると、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>;
}

更新時の処理(副作用)