React学習ログ【React学習ログ3】メモ化で子コンポーネントが無駄に実行されるのを防ぐ2023年5月15日Reactでは親コンポーネントが実行されると、親がインポートしている子コンポーネントも勝手に実行されてしまいます。便利なの時もあるのですが、不要な時もあるのでこの現象の制御方法を解説します。具体例をお見せします。次の例を見てみましょう。親子関係の画像とソースコードです。Child;実装内容は、ボタンをクリックすれば、useState()が実行され、Appコンポーネントが再実行されるというものです。RUNNING」も出力されています。この挙動の理由は、コンポーネントは正しくは関数コンポーネントであり、言ってしまえば関数なので、親の関数が実行されるとネスト状態である子の関数も順番が来たら実行されるからです。渡されるpropsの値が常に同じでも実行される次の例を見てみましょう。App;Childコンポーネントに渡される値は常に「false」でも実行結果は先ほどと同じになります。RUNNING」が出ていますね。このことから、子コンポーネントに渡される値が変更されるから子コンポーネントが実行されるのではなく、親コンポーネントが実行されるから子コンポーネントも実行されると知っておきましょう。この挙動からある問題が発生します。子コンポーネントが大量にあると親を実行すると毎回全ての子コンポーネントを再実行してしまうことになるということです。次のようにさらにコンポーネントを増やしてみましょう。親子関係App.jsはそのままでChild.jsを少し書き換えます。Child;新しく、Grandchild.jsを作ります。Grandchild;先ほどの解説通り、増えたコンポーネントも実行されてしまいます。アプリが大きくなればなるほど、コンポーネントも増えていきます。コンポーネントの数だけ毎回実行されるとアプリに負荷がかかりパフォーマンスに影響が出ます。次からこの問題の解決方法を解説します。React.memo()で子コンポーネントの再実行を制御Reactには子コンポーネントが無駄に実行されるのを防ぐ方法が用意されています。それがReact.memo()です。React.memo()を使うことで、Reactにある状況時(例えばpropsの値が変更された)にだけ対象の子コンポーネントを再実行させることができるのです。このように再レンダリングの必要のない部分をスキップすることをメモ化と言いアプリケーションのパフォーマンスを向上させることができます。例をお見せするために先ほどの実装を下記に変更します。Grandchild.jsを新しく追加します。Grandchild;この実装だけでは結果は先ほどと同様で子要素の全てが実行されています。RUNNNING」が出力された)ここでReact.memo()で子コンポーネントの実行を制御してみましょう。先ほど、「React.memo()を使うことで、Reactにpropsの値が変更された時にだけ対象の子コンポーネントを再実行させることができる」と説明しました。/>常にfalseを指定しているのでAppコンポーネントが再実行されても子であるChildコンポーネントのpropsの値は常にfalseで変更されません。したがってChildコンポーネントにReact.memo()をこのように記述することで再実行を防ぐことができます。RUNNING」だけが表示され、子コンポーネントの再実行を防ぐことができました。Appコンポーネントを下記のようにすると、毎回propsの値がtrueとfalseの入れ替えが発生するのでボタンをクリックすると再び全ての子コンポーネントが動き出します。/>全てのコンポーネントでReact.memo()を使えばいいのでは?全てのコンポーネントにReact.memo()を使っていれば必要に応じてReactが子コンポーネントを実行する/しないを勝手に判断して効率的じゃない?いいポイントです。残念ながらReact.memo()を適切に使用しなければパフォーマンスに影響します。React.memo()は次の2つのことをしており何も設定しない時よりも負荷がかかっているからですpropsの値を保存(記憶)props比較まずは、元々持っているpropsの値をReact.memo()が記憶しておかなければなりません、そのして変更がかかるたびに、前後のpropsの値を比較するという作業が発生しています。.memo()を使う負荷を考えなければいけません。React.memo()使用の判断基準ではReact.memo()を使う/使わないはどのように判断するのでしょう?それは対象コンポーネントの更新頻度です。対象のコンポーネントのプロパティの更新頻度が少なく、下層に多くのコンポーネントがある時には有効です。逆に頻繁にプロパティが更新されるコンポーネントは使う使う必要があまりない場合にはReact.memo()があろうがなかろうが再実行されるので、React.memo()に余計な仕事させることになる。今回の例のAppコンポーネントが下記の記述の時は、クリックのたびに毎回propsの値がtrueとfalseの入れ替えが発生するので、React.memo()は不要ということです。/>一応App.jsを忘れた方に現状のApp.jsがどんな記述だったかをお見せします。頻繁にプロパティが更新されるコンポーネントは使う必要があまりないさらに深い理解へ少し不思議な現象を紹介します。Appコンポーネントに書かれていたボタンをコンポーネント化してみましょう。今回触るのはApp.jsとButton.jsだけです。それ以外のソースはそのままです。React.memo(Button);注目ポイントは2ヶ所あります。Buttonコンポーネントの受け取るpropsは親コンポーネントが再構築されても同じ関数であるButton.jsの最後にReact.memo(Button)と記述しているつまり一つ前の例と同条件です。前回の例はChildコンポーネントに送られてくるpropsは常に「false」でした。今回の例はButtonコンポーネントに送られてくるpropsは常に「toggleParagraph(関数)」ですしたがって、今回の例でも、子コンポーネントのButton.jsはクリックされても実行されないはずです。CLICKED」がクリックのたびに出力されてしまいます。結論からお伝えすると、実は今回のButtonコンポーネントで受け取るpropsの値は見た目は同じものなのですが、別の新しい値が入っているとみなされてしまうのです。これはReactというよりJavaScriptの原理です。ボタン</Button>各コンポーネントには送っているのは「false=Primitive型のデータ」で「toggleParagraph=Object型のデータ」です。ここでは各データについて詳細な解説はしません。Primitive型はこのよう↓に比較してもtrueが返ってきます。Object型(配列、オブジェクト、関数)は一見同じ値の見た目でもfalseが返ってきます。falseが成立し同じ値とみなされてReact.memo()によってClickコンポーネントが実行されない。toggleParagraphとなり新しい値とみなされてReact.memo()があってもButtonコンポーネントが実行されてしまう。オブジェクト型のデータが送られた時の子コンポーネントの実行を制御する方法はないのか?ReactのHooksの一つであるuseCallbackを使えば解決します。useCallbackで制御するuseCallback()は指定の関数(オブジェクト型データ)をコンポーネント内で保存し、そのコンポーネントが再構築されても新しく作るのではなく保存したものを使えるようにしてくれます。実装方法はこちらです。useCallbackは第一引数には記憶させる関数を指定します。第二引数には配列が入ります。第二引数の配列の値が変更されたときにだけ、関数の更新がかかります=新しいものとみなされて子コンポーネントが実行されます。今回の例のように配列を空にすると、「変更されることはないです=いつも同じ関数です」という宣言となります。これでButton.jsにReact.memo()を記述することで渡されるpropsは常に同じものになり、Buttonコンポーネントは実行されることはなくなります。詳しくなりたい人へ:useCallback()の第二引数を使うここからはかなりややこしくなるので、どうしても気になる人は呼んでください。先ほどの例ではuseCallbackの第二引数の配列をからの状態にしていました。今回はその第二引数を使う例を紹介します。この例を理解するにはまず、JavaScriptのクロージャーを知っていなければなりません。クロージャーについていうと、クロージャーとは関数を実行した後でも関数内の変数の値は保存されたままになる。これを踏まえてApp.jsをこのように書き換えましょう。App;変更点は、useStateを追加して、allowToggleという状態を管理JSX内のChildコンポーネントをshow={showParagraph}に変更JSX内にButtonAとButtonBを作成ButtonAを押すと、allowToggleがいつもtrueになるallowToggleHandler関数を作成まだuseCallback()の第二引数の配列は空のままです。ButtonA.jsとButtonB.jsはほぼ同じでconsole.logの出力だけを変更しています。React.memo()は設定されています。React.memo(ButtonB);この時の挙動を確認します。ボタン」の順に3回クリックします。RUNNING」が表示されます。理由は、allowToggleの初期値がfalseからtrueに変わったことで、そもそもの親であるAppコンポーネントが再実行されたのと、ButtonAコンポーネントに渡されるpropsもallowToggleHandler(関数)なので毎回違う値としてみなされるのでButtonAコンポーネントも再実行されました。RUNNING」が表示されていないのは、この時点ではtoggleParagraphがuseCallbackのおかげで同じpropsとみなされているので、ButtonBコンポーネントは再実行されていません。RUNNING」の全てが出力されます。RUNNING」はshowParagraphの値が変わるのでApp.jsが再実行されます。ChildコンポーネントもshowParagraphがfalseからtrueに変わっているのでReact.memo()を書いているが実行されてしまいます。したがってその子であるGrandchildコンポーネントも実行されます。RUNNING」だけが表示されるのはallowToggleHandlerが反応して出力されています。見た目は同じだけど、違うものとして判断されるのでした。ボタン」を押すと先ほどと同じ結果になります。次にApp.jsをこのように変更してみます。App;toggleParagraph()の中にif文を入れました。allowToggleの値がtrueの時にだけ動くようにしています。ボタン」の順番に3回クリックします。ボタン」を押しても何も反応しません。理由はallowToggleの初期値がfalseのままだからです。RUNNING」が出力されます。これは先ほどの例の1回目と同じです。重要なポイントはここでallowToggleの値がfalseからtrueに変更されたという点です。ボタン」を押すと、allowToggleがtrueに変更されているにも関わらず何も起きません。これはなぜでしょう,,,?ここでクロージャーが出てきます。先ほど説明したようにクロージャーは実行した関数の値を保存しておく仕組みです。toggleParagraph()の中にあるallowToggleは初期値ではfalseになっています。この状態は「AllowToggle」を押してsetAllowToggleによってallowToggleがtrueになったとしても、クロージャーによって、toggleParagraph()内のallowToggleはfalseの状態で保存されています。ボタン」を押しても何も起きないのです。ここでuseCallback()の第二引数の登場です。[allowToggle]);第二引数にallowToggleを入れることで、クロージャーの保存された値ではなく、常に最新の値を取得できるようになります。ボタン」の順番に3回クリックしてみます。ボタン」は何も反応しません。これは先ほどと同じ理由でallowToggleの値がfalseだからです。RUNNING」がそれぞれ出力されています。これはallowToggleがfalseからtrueに変わったのでApp.jsが実行されたのと、それに伴って子コンポーネントである2つのButtonA、ButtonBコンポーネントが再実行されたからです。ButtonBコンポーネントはuseCallback()を使っていますが、第二引数でallowToggleを設定しているので、これがtrueに変わったので実行されています。RUNNING」が出力されます。これで本当に以上となります。最後に今回は子コンポーネントの再実行を制御する方法を解説しました。長くなりましたが、中規模以上のアプリを作るのであれば必ず必要な知識なりますので、ぜひ理解しましょう!このコンテンツでは一般のエンジニアがどのように言語(今回はフレームワークになりますが)を学習し、つまずき、乗り越えていっているのかを垣間見えるようにしていこうと思います。できるだけ画像、動画などを盛り込んで視覚的に理解できるように工夫しています。これからプログラミングを学ぼうと考えている方の為になる発信をできれば思いますので、温かい目で見てもらえると嬉しいです。今回学習のメイン教材として使用しているのは、Udemyのコースになります。興味があればぜひ使ってみてください。React学習ログ【React学習ログ2】Keyについて調べまくって役割を言語化してみたReactでデータをリスト表示にするときに使う「key」について詳しく解説しています。React学習ログ【React学習ログ4】Reduxをどう理解したかを解説|図解あり私がReduxをどう理解したかを解説します。Hooksの一つのuseContext()について詳細に解説いたします。Hooks基礎】useContextを理解する【React学習ログ4】Reduxをどう理解したかを解説|図解ありフロントエンドエンジニアYamato(やまと)7年目のフロントエンドです。サンフランシスコ留学の際にITの素晴らしさに触れエンジニアになると決心。独学で未経験からアルバイトで採用され入社。経験し2022年8月に独立しました。HTML/CSS・JS・WordPress・Shopifyを触っています。YAMATOdevlog。