React: そのrender()はいつ呼ばれるのか

これは TECHSCORE Advent Calendar 2018 の2日目の記事です。
React で無邪気に開発をしていると、やたらと render() が呼ばれるのが気になります。 PureComponent を使うと render() の呼び出しを少なくできることは、なんとなく知ってはいるのですが、そもそもどういう場合に render() が呼ばれるのか、 State や Context、 はたまた Redux を使ったときに render() の呼ばれるタイミングがそれぞれどう違うのか、いまいちよくわかっていません。いろいろたっぷり試してみました。

目次

準備

図1のようなアプリケーションを考えます。

図1 アプリケーションの概要

コンポーネント App はコンポーネント A、X を含み、 A、X はそれぞれ B、Y を、 B、Y はそれぞれ C、Z を含みます。 B にはボタンが配置されており、このボタンを押すと、 Y に表示された数字がカウントアップされます。
擬似コードにすればこんな感じです。

Y に表示する数字はどこかに値を保持して状態として管理する必要がありますが、 React の場合、状態管理の方法はいくつかあります。

  • State
  • Context
  • Redux

以下、それぞれの方法で状態管理を行なった場合に、どのように render() が呼ばれるのか見ていきます。

State

State: Vanilla State

「値の操作に関するコンポーネント B」と「値の表示に関するコンポーネント Y」、両者の共通の祖先である App に State を保持します。 State を操作する countUp() と State の値を表す count を Props で下位のコンポーネントへと渡します。
ボタンを押すと、 App の setState() が呼ばれ、さらに render() が呼ばれます。 A の render() が呼ばれると、 A および X の render()、 B および Y の render()、 C および Z の render() が数珠繋ぎに呼ばれます。

図では、ボタンを押した際に render() が呼ばれるコンポーネントに赤丸を付けています。
また、ボタンを押した後の制御(のイメージ)をオレンジ色の矢印で表しています。

図2 State を使う
Try it on CodePen

State: PureComponent

PureComponent では Props に変化のないコンポーネントの render() は呼ばれないため、簡単に render() の呼び出しを抑制することができます。
ボタンを押すと App の state.count が変化しますので、 App および state.count を Props として渡している X、Y の render() が呼ばれます。しかし Z には Props を渡していませんので Z の render() は呼ばれません。
また A、B に渡している countUp() は変化しませんので、 A、B の render() は呼ばれません。さらに C には Props を渡していませんので、 C の render() も呼ばれません。

図3 State を使う。 PureComponent の場合
Try it on CodePen

State: Props に Allow Function を直接渡す

PureComponent を使った場合でも、 countUp() を Props に渡す際にその場(または render() 内)で作成した関数を渡すと、 render() が実行される毎に異なる値となるため、 A、B の render() も呼ばれてしまいます。

図4 State を使う。 PureComponent の場合。その場で作った関数を渡す
Try it on CodePen

Context

Context: Vanilla Context

Context を使うことで Props のバケツリレーを回避することができますが、状態を App の State に持ってしまうと、最初の State を使った場合と同様に全てのコンポーネントの render() が呼ばれてしまいます。

図5 Context を使う
Try it on CodePen

Context: PureComponent

Context を使い状態を App の State に持った場合でも、 PureComponent を使えば render() の呼び出しを抑制することができます。

図6 Context を使う。 PureComponent の場合
Try it on CodePen

Context: 独自の Provider を使う

App に直接 State を保持するのではなく、独自の Provider を使うことで render() の数珠繋ぎを切断することができます。

図6 Context を使う。独自の Provider を使う場合
Try it on CodePen

Context: 独自の Provider、Consumer を使う

独自の Provider に加え、独自の Consumer を使うことで、さらに render() の伝搬範囲を限定することもできます。

図7 Context を使う。独自の Provider、Consumer を使う場合
Try it on CodePen

Redux

Redux: react-redux v5.1.1

Redux を使うとコンポーネント階層の外に状態を持つことができるため、 connect() より下位のコンポーネントのみ render() が呼ばれることになります。

図8 Redux を使う
Try it on CodePen

寄り道: Redux はどうやって render() を呼んでいるのか

React では State を変化させることで render() が呼ばれます。 Context を使った場合でもどこかに State を保持する必要があります。しかし Redux では React の管理外に状態を持っており、これは React の State とは無関係です。
では、どのように Redux の状態変化を React に伝えているのか、 react-redux の実装を確認してみます。

react-redux では connnect() で Redux の世界と React の世界を繋ぎます。
connect()実装にはconnect is a facade over connectAdvanced.とありますので connectAdvanced() を見てみます。

connectAdvanced() では渡されたコンポーネントをラップして返します(※)が、このコンポーネントに State を保持しています。 Redux 側で状態変化が起こると、この State が変更(setState())されます。このときに設定されるdummyState常に空オブジェクト({})になっています。
実際には状態は変化しませんが、 render() の呼び出し連鎖を発生させるために setState() を実行しています。
※ 正確には「渡されたコンポーネントを複製したものを返す関数を返す」ですが、ここは簡単に。

もっと何か別の伝達手段があるのかと思っていましたが、 this.setState({}) とは意外な実装方法でした。

Redux: react-redux v6.0.0-beta.1

react-redux の次期バージョン(v6.0.0)では内部的に Context を使う実装に変更される予定です。
react-redux v6.0.0-beta.1

🎉 This is our first big release supporting the new React Context API!

v6.0.0-beta.1では内部向けのコンポーネントが新たに作られ、この内部にStateを保持します
Reduxの状態変化をこのStateに伝えることで render() を呼び出しています。 this.setState({}) というテクニカルな実装がなくなって、素直な実装になっています。

なお、(当たり前ですが) v6.0.0-beta.1 でも render() の呼ばれるタイミングは変わっていないようです。

Try it on CodePen

まとめ

各種状態管理方法のパターンごとに render() がどのように呼ばれるか試してみました。

以下のように考えると render() の動作が理解しやすいです。

  • App と A はコンポーネントの上下関係
    • App の State が変更されると、 A の render() が呼ばれる
  • Something と A は JSX 内の DOM 的な上下関係
    • Something の State が変更されても、 A の render() は呼ばれない

PureComponent を使うのが王道なのですが、コンポーネントの上下関係に注目して render() がいつ呼ばれるのかを把握しておくのがよさそうです。

Enjoy your React life!

Comments are closed, but you can leave a trackback: Trackback URL.