🔰

【イラストで分かる】Reactとライフサイクル

2024/01/29に公開

はじめに

こんにちは。
ソフトウェアエンジニアをしています、Koyaです。

普段、ReactやNext.js周りを勉強してます。
最近は単に動くものを実装するのではなく、Reactの仕組みを踏まえた実装をするように意識してます。

そこで今回はreactのライフサイクルについて調べましたので、まとめたいと思います。
また自分の整理を含めて、極力細かく説明していこうと思ってます。
(おかげで大作になりました😅)

可能な限り公式ドキュメント等で収集した信頼できる情報を基にまとめていますが、間違いや認識違い等あると思います。
ぜひコメントで指摘いただければと思います。

また筆者は視覚優位な特徴(*1)を持ちます。
同じ視覚優位な特徴を持つ人たちに向けて、わかりやすいように可能な限りイラスト・図を使って説明したいと思います。

では、よろしくお願いします!

*1 : 物事の理解の仕方は人によって異なると言われています

前提

ライフサイクルに関して、述べる前に前提知識が必要ですので以下にまとめました。
主に

  • コンポーネント
  • 「レンダリング」の定義

についてです。
不要な人は飛ばしてください。

前提①:コンポーネント

reactはコンポーネントという単位で構成されています。

コンポーネント from React document

React アプリはコンポーネントで構成されています。
コンポーネントとは、独自のロジックと外見を持つ UI(ユーザインターフェース)の部品のことです。
コンポーネントは、ボタンのような小さなものである場合も、ページ全体を表す大きなものである場合もあります。

以下のように、各部品を組み合わせることで一つのサイトを作っていくイメージです。
コンポーネントの粒度は自由に調整できます。もっと小さくbuttonごとにコンポーネントを作ることも可能です。

では、このコンポーネントをどのように作るのでしょうか?
このコンポーネントの作成方法は大きく以下の2つあります。

  • クラスコンポーネント
  • 関数コンポーネント

クラスと関数の違い

せっかくなので、「両者の違い」と「なぜ2つ存在するのか」についてにも触れておきましょう。

両者の違い

大きく3つです。

  • 実装方法
  • 引数(props)の受け渡し
  • ライフサイクル

簡単に言うと、

  • 「関数の定義の仕方」
  • 「データの受け渡し方」
  • 「ライフサイクル(後述で詳しく書きます)」

が違うんだなぁと思ってもらえれば。
詳しくはクラスに関する公式ドキュメント関数に関する公式ドキュメントを見てください。

なぜ2つ方法が存在するのか、使い分けについて

存在理由については、(筆者の認識だと)
「使い分けるため」というよりは
「歴史的背景があり、2つ方法が生まれた」に近いです。

🏯歴史🏯

①React.createClassを使った、React初期のComponentの定義

当時Class構文に対応したjavascriptがまだ正式にリリースされてなかったため、React.createClass() でClass構文を使わずComponentを作成するメソッドが用意された。

②React.Componentを継承したClassBasedなコンポーネントの定義

①の方法では、コード量が多くなりやすい。
長大なオブジェクトの定義でカンマのつけ忘れなどでsyntax errorを起こしてしまうも多くあったのもあり、使用されなくなった。
そのため、現在のクラス(の基礎)による記述方法が生まれた。

③関数で定義するStateless functional componentの定義

②の方法では、Stateすら持たない、ただrender() したいだけのComponentなど、通常のClassによる定義がオーバースペックになる場合があった。
そこで、シンプルでカジュアルに定義できるStateless functional componentが用意される。これにより、メモリ確保の不要とパフォーマンスの向上が実現。

④関数で定義する(現在の)関数コンポーネントの定義 ←今ココ

③の方法では、SFCを定義したあとでStateをもたせたくなった場合、通常のClassに書き換えざるを得ず大変。
そこで、フックを使ってstateを持てる(現在の)関数コンポーネントの定義方法が誕生。

簡単なイメージ

という流れで、現在2つの実装方法がある状態です。

これでコンポーネントに関して、ある程度今までの流れ含めなんとなくわかったのではないでしょうか?


前提②:「レンダリング」の定義

フロントエンド分野で「レンダリング」は

  • ブラウザの「レンダリング」
  • Reactが指す「レンダリング」

の2つの文脈があることをご存じだったでしょうか?
レンダリングの意味が2つ存在するせいで、話がよくわからなくなります。
一度整理しておきましょう。

まずブラウザの文脈での「レンダリング」は皆さんがご存じの「画面を描写する」という意味です。

では、Reactが指す「レンダリング」とは何でしょうか?

React documentを見ると

“Rendering” is React calling your components.

と書いてあります。どういうことでしょうか?
Reactの「レンダリング」を知る前にDOMについて知っておく必要があります。

DOMとは

Document Object Model の略でHTMLやXML文書を取り扱うためのAPIです。
HTMLやXMLのドキュメントに含まれる要素や要素に含まれるテキストのデータをオブジェクトとして扱い、階層的に組み合わされたものとして識別します。

このDOMをWebページに記述したJavaScriptで使用することで、Webページ上のテキストデータを読み込んだり、新しい要素を追加したりができるようになります。
以下のようなHTMLがあったら、

<html>
<head>
    <title></title>
</head>
<body>
    <header></header>
    <main>
        <h1>TITLE</h1>
        <p>CONTENT</p>
    </main>
    <footer></footer>
</body>
</html>

上記のHTMLを下記のドキュメントツリーとして認識します。

このことを踏まえて、Reactの「レンダリング」について見ていきましょう。

React documentのRender and Commitを見ると、描写までに大きく3ステップあるようです。

  1. Triggering a render
  2. Rendering the component
  3. Committing to the DOM

とあります。
簡単に説明していきます。

1.Triggering a render

これはTrigger(引き金をひく)ですから、「レンダーの引き金を引くこと」つまり実施されるタイミングのことを指しています。
つまり、以下の実施タイミングの時にステップ2に移行します。
・最初に画面を表示する時
・画面更新する時

2.Rendering the component

ここで、Renderingというワードが使われてますが、Reactの文脈なので気を付けてください。
ドキュメント記載では
「“Rendering” is React calling your components.」であり、
「After you trigger a render, React calls your components to figure out what to display on screen .」とあります。
これはどういうことかというと、「コンポーネントの取得」です。
実施タイミングが
・最初に画面を表示する時 → ルートコンポーネント(コンポーネント全体)を取得
・画面更新する時     → 更新する前と後の差分があるコンポーネントだけを取得

3. Committing to the DOM

2のステップで取得したコンポーネントを実行し、DOMに反映します。
つまり、
・最初に画面を表示する時 → ルートコンポーネントを基に画面に表示
・画面更新する時     → 更新する前と後の差分があった部分だけを更新し画面に表示

簡単なイメージ

この3ステップでReactでは画面表示を行っております。
つまり、Reactの「レンダリング」とは、
ステップ2の「2.Rendering the component(calling your components) 」のことであり、
ステップ3「3. Committing to the DOM」ではないのでご注意ください。

仮想DOMとは

React周りを触っていると「仮想DOM」というワードがよく出てくると思います。
Reactは、非常に高速に動くという特徴があり,これを実現する概念として実装されています。
なぜこの話をするかというと、先ほど説明したReactの画面表示までの3ステップで以下のような実行があったと思います。

2.Rendering the component

・画面更新する時     → 更新する前と後の差分があるコンポーネントだけを取得

この時、実は仮想DOMが使われています。
具体的には、「更新する前と後の差分」を検出するために使われます。

旧React documentでは

仮想 DOM (virtual DOM; VDOM) は、インメモリに保持された想像上のまたは「仮想の」UI 表現が、ReactDOM のようなライブラリによって「実際の」DOM と同期されるというプログラミング上の概念です。
このプロセスは差分検出処理 (reconciliation)と呼ばれます。

どいうことかイラストを使って説明しますね!
いったん、ブラウザで使用されている普通のDOMを物理DOMとしますね

更新が必要なところだけを変更することで少ないリソースで動かせます。
これでReactの非常に高速に動くという特徴を実現しています。

必要な情報は揃いました。
では、本題に入っていきましょう(前提でこんなに書く予定じゃなかった、、、)

本題

クラスなり関数なりで定義したコンポーネントをブラウザに表示・更新・破棄されるまでの流れをライフサイクルといいます。
今回はこのライフサイクルについて述べます。
実はライフサイクルはクラスコンポーネントと関数コンポーネントで若干異なります。
(そのため、「前提」の章で説明しました)

まずクラスコンポーネントのほうがわかりやすいので、クラスコンポーネントから説明していきます。

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

ライフサイクルに関しては以下の画像がわかりやすいです。

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

ライフサイクル(初表示・更新・破棄されるまでの流れ)と上の画像はこの対応です

  • 初表示 マウント時
  • 更新 更新時
  • 削除 アンマウント時

Linuxに触れている人は「マウント」は知っていると思いますが、知らない人は
「マウント」=「何かにセット(セッティング)して使えるようにする」
というイメージを持っていただければよいと思います。

ここに書かれていることの概要は、実はもう皆さん一度見ています。
そうです。
「前提②:「レンダリング」の定義」で書いている以下の流れの事です。

  1. Triggering a render
  2. Rendering the component
  3. Committing to the DOM

上の画像の一番左にも Render と Commit という文字があることからも察せると思います。

「前提②:「レンダリング」の定義」の説明の時、

  • 最初に画面を表示する時
  • 画面更新する時

を詳しく区別せず一緒に述べてました。
「最初に表示する時」と「更新する時」で動いている関数メソッドは(上の画像のように)それぞれ若干違います。
今回はこのメソッドに着目してライフサイクルの動きを把握します。

初表示 マウント時について

マウント時には、画像にあるconstructor, render, componentDidMount以外にもメソッドはあり、定義できます。
今回はこの3つについて説明しますが、気になる方は旧React document見てみてください。

以下のメソッドが呼び出されます

  • constructor() : マウント前に呼び出される。state の初期化もメソッドのバインドもしないのであれば、不要。
  • render() : 必須メソッド。コンポーネントを取得し、JSXをreturn( = DOMとrefsのフェーズに移行)する。
  • componentDidMount() : DOMに反映された後、実行される。DOMにアクセスする必要がある処理や、HTTP Requestの送信などを行う。

更新時について

画像にあるように、props(渡されるデータ)やstate(状態)等が変わったとき=「画面更新する時」です。
以下のメソッドが呼び出されます

  • render() : 同じ
  • componentDidUpdate() : DOMに反映された後に呼び出す。
    更新前の値と更新後の値を比較して何か処理を行う。

アンマウント時について

コンポーネントを削除する時です。
以下のメソッドが呼び出されます

  • componentWillUnmount() : コンポーネントがDOMから削除される直前に呼び出す(WillUnmountなので)。
    コンポーネントのリソースを解放したりしてメモリリークを防ぐために必要なクリーンアップ処理を記述。

まとめると、これが

解像度を上げると

ということです

クラスの定義をするときに、各メソッドを記述すると、それぞれのタイミングでメソッドが呼び出されます。
結構、かゆいところまで手が届くようになります。
これがクラスコンポーネントにおけるライフサイクルの動きです。

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

やっとここまで来ました、もう一息です(笑)。
そして、真の本題はココです。
クラスコンポーネントより、関数コンポーネントのほうが今後主流になると思いますので。

公式ドキュメントに、関数コンポーネントにおけるライフサイクルの動きに関する情報を見つけられませんでした。
これは(おそらく)「前提①:コンポーネント」/「歴史」でも述べましたが、関数コンポーネントにフックが導入されたためです。
結論から言うと、クラスコンポーネントのライフサイクルの仕組みは、関数コンポーネントではフックを使うことで実現されています。
なので、仕組みを知りたい場合はフックを見ていくことになります。

しかし、今回は以下のリポジトリでクラスコンポーネントのようなイラストを作ってくれてました(ありがたい)
https://github.com/Wavez/react-hooks-lifecycle

これを参考に見ていきましょう。

で、上のイラストを見るとuseMomo()とかuseCallback()とかありますが、
これがわからない方は以下を見てください。わかりやすくまとめられています。
https://ja.legacy.reactjs.org/docs/hooks-intro.html
https://qiita.com/seira/items/f063e262b1d57d7e78b4

クラスコンポーネントのイラストより、出てくるワードの数は増えていますが(笑)、
hookを使うことでコードがシンプルになります。
例えば、
マウント時は
constructor()やrender()を記述する必要はなく、通常の関数定義で充分です。
またcomponentDidMount()や更新時のcomponentDidUpdate()を記述しなくても、useEffect()が勝手にやってくれます。

非常に便利です。
関数コンポーネントのライフサイクルに関しては、フックのほうに役割が移行しましたのでまた次回にフックについてはみていきたいと思います。

まとめ

いかがでしたでしょうか?
少しでも皆さんの理解の支援ができていれば幸いです。

今後の実装方針として、
基本は関数コンポーネントでコンポーネント作成し、細かい調整したい場合はクラスコンポーネントで実装になるかなと思います。

参考資料

GitHubで編集を提案

Discussion