今回はパフォーマンス改善に有効なReact.memoについてサンプルを使ってまとめます。
一言で言えば、React.memoはコンポーネントの不要な再レンダリングをさせないために使える高階コンポーネントです。
この記事では簡単な例を用いて、React.memoの使い方を解説します。
ポイント
Functional Componentのみに用いられるもので、クラスベースでのコンポーネントには使えません。クラスベースのコンポーネントではshouldComponentUpdate()を使用します。(もしくはPureComponent)
Contents
前提となるプログラム
今回は説明のために簡単な数字のカウントプログラムを用意しました。
「+1」ボタンを押すとカウントが+1されていきます。結果発表のボタンを押すと以下のようにモーダルが表示され、「〜回押しました」とカウントが表示されます。
また「不要な再レンダリング」を再現するため、カウントのロジックとは何の関係もない処理として「Hello」と表示するだけのプログラムを用意しました。(3つ目のボタン)
プログラムは以下の通り。
App.jsとその子コンポーネントとして、Modal・Titleというコンポーネントが存在します。
Titleはタイトルを表示するだけでpropsは使用していません。Modal.jsはカウントの数を表示するためいくつかのpropsを受け取り、使用しています。
コードを読む必要はありませんが、以上の構造は理解しておいて下さい。
import React, { useState } from "react"; import "./App.css"; import Title from "./Title"; import Modal from "./Modal"; function App() { const [count, setCount] = useState(0); const [open, setOpen] = useState(false); const handleIncrementClick = () => { // 「+1」ボタンをクリックしたときの処理 setCount(count + 1); }; const handleModalOpen = () => { // モーダルを出す処理 setOpen(true); }; const handleModalClose = () => { // モーダルを閉じる処理 setOpen(false); }; /* === ここからCountと関係ない処理 === */ // テキストの表示・非表示を行うだけの処理 const [visible, setVisible] = useState(false); let hello = null; if (visible) { hello = <h2>Hello</h2>; } const handleHelloClick = () => { setVisible(!visible); }; /* === ここまでCountと関係ない処理 === */ console.log("=== App rendered ==="); return ( <div className="App"> <Title /> <h1>Count is {count}</h1> <button className="btn" onClick={handleIncrementClick}> +1 </button> <button className="btn--modal" onClick={handleModalOpen}> 結果発表〜 </button> <button className="btn" onClick={handleHelloClick}> 関係ない処理 </button> {hello} <Modal open={open} currentCount={count} handleClose={handleModalClose} /> </div> ); } export default App;
Modalのプログラムは以下の通り。現在のcountをprops.currentCountとして受け取っています。
import React from "react"; const Modal = props => { const { open, currentCount, handleClose } = props; console.log("=== Modal Rendered ==="); let modal = null; if (open) { modal = ( <div className="modal"> <p>あなたは「{currentCount}」回ボタンを押しました。</p> <button className="btn--modal" onClick={handleClose}> Close </button> </div> ); } return <React.Fragment>{modal}</React.Fragment>; }; export default Modal;
Titleはタイトルを表示するだけのコンポーネントです。propsの値は使用していません。
import React from "react"; const Title = () => { console.log("=== Title rendered ==="); return ( <React.Fragment> <h1>不要な再レンダリングをチェック!</h1> </React.Fragment> ); }; export default React.memo(Title);
このプログラムではいくつかの「不要な再レンダリング」が発生します。まずはそれを見てもらいます。
不要な再レンダリング
上のプログラムを実行すると、以下の3種類の不要な再レンダリングが行われます。
- タイトルを表示するだけのコンポーネントが再レンダリングされてしまう。
- 「関係ない処理」ボタンを押してもModalが再レンダリングされてしまう。
- Modalが表示されていないときでも「+1」ボタンを押すたびにModalが再レンダリングされる。
一口に「不要な再レンダリング」と言っても3つの再レンダリングには違いがあります。
第1のムダ
1つ目はpropsに関係なく行われる再レンダリングです。
App.jsが何らかの理由で再レンダリングされるとTitleコンポーネントも再レンダリングがされてしまいます。
直感的にはpropsを使用していないため再レンダリングされないと思ってしまいますが、実際にはされています。
第2のムダ
2つ目は意図せずpropsが変更されることによる再レンダリングです。
本来「数字のカウント」と何の関係もない処理をしたときにはModalは再レンダリングしてほしくありません。しかし意図せずpropsに変更が加えられ、再レンダリングが起こってしまうことがあります。
第3のムダ
3つ目は意図したpropsの変更による再レンダリングだが、ユーザからすると意味のない再レンダリングです。
Modalはpropsに現在のcountを受け取るので、countが変更されればその度に再レンダリングが行われます。これはある意味プログラム通りの処理なのですが、ユーザからすると見えていないModalが再レンダリングされているのは全く無駄です。
これらが起こる原因と対策方法を説明します。
原因と対策
propsに関係ない再レンダリング
原因
デフォルトでは親コンポーネントが再レンダリングされると子コンポーネントも再レンダリングされてしまいます。特別な理由はありません。仕様です。
対策
React.memoを使用すればそれだけで再レンダリングは回避できます。Titleコンポーネントのexport箇所に以下のように書きます。
export default React.memo(Title);
これだけで再レンダリングはされなくなります。
意図せずpropsが変更されることによる再レンダリング
原因
今回の例で説明するとModalのpropsには以下の3つのpropsが渡されています。
const { open, currentCount, handleClose } = props;
(カウントと)「関係ないボタン」を押すと、「Hello」を表示するためにApp.jsが再レンダリングされるのですが、このときhandleCloseも再び生成されるためModalのpropsが変更されたと判断され、再レンダリングが起こります。
ポイント
ここでopenとcurrentCountはなぜ変更されないの?と思うかも知れません。
これはuseStateの仕様で、値が変更されるまでは再びオブジェクトが生成されることはありません。
対策
Titleでの対策と同じようにReact.memoを使用しますが、それだけではありません。
まずModalをexportする箇所に以下のように記述します。
export default React.memo(Modal);
ただしこれだけではModalが再レンダリングされてしまいます。props.handleCloseが変更されるからです。
App.jsのhandleModalCloseの宣言箇所にuseCallbackを使います。
const handleModalClose = useCallback(() => { setOpen(false); }, []);
このように書くことでApp.jsが最初にレンダーされたときにのみhandleModalCloseは生成され、その後再レンダリングされても変更されません。
これで(countに)「関係ない処理」ボタンが押されたときには再レンダリングは行われなくなりました。
propsの変更による再レンダリングだが、意味のない再レンダリング
原因
「+1」ボタンを押せば、Modalに渡されるcountの値が変更されるので当然再レンダリングはされます。ただしユーザから見れば、無意味なレンダリングです。
対策
こちらもReact.memoを使うのですが、第2引数を使うことでもう少し柔軟な使い方ができます。
Modal.jsのexport部分を以下のように変更します。
const areEqual = (prevProps, nextProps) => { // これがtrueを返すときは再レンダリングされない return prevProps.open === nextProps.open; }; export default React.memo(Modal, areEqual);
このようにReact.memoの第2引数に関数を指定することで、その関数がtrueを返すときにはコンポーネントは更新されません。つまり「前の状態と同じかどうか」を返す関数を定義するということです。
この関数はデフォルトでprevProps,nextPropsを引数として使用することが出来るのでこれを利用します。
今回の例では「前の状態のopen」と「更新後のopen」が同じであるときに更新させないようにしてあります。言い換えるならば「openの値が変更されたら(モーダルが開閉されたときに)」再レンダリングするという処理になるということです。
以上で不要な再レンダリングをしないよう調整することが出来ました。
注意
このReact.memoはパフォーマンスの改善のために用いるもので、無闇やたらに使うものではありません。
Reactの公式ドキュメントでも以下のように述べています。
これはパフォーマンス最適化のためだけの方法です。バグを引き起こす可能性があるため、レンダーを「抑止する」ために使用しないでください。
今回のように軽いプログラムではまったく気にする必要はありません。
あくまでReact.memoの使い方を示すために書いたプログラムですので、React.memoはパフォーマンスが気になったら検討するというレベルのものです。
まとめ
今回はパフォーマンス改善に使えるReact.memoを紹介しました。一口に「不要なレンダリング」といっても厳密に言えばいくつか種類があり、対策法も異なります。
書いているプログラムが複雑になってきた場合は、最適化出来る箇所がないか確認してみてはいかがでしょうか。
===
こういったことは簡単な例を書いておくと頭が整理出来て良いですよ。