前置き
Reactを学習し始めたときに理解が浅かったmemo化についてまとめてみました。
再レンダリングとは
変更を検知してコンポーネントを再処理することです。
画面をリロードしたわけでもないのに、処理によって画面が変更されるのは、コンポーネントが再レンダリングされている為です。
再レンダリングが起きる条件
- stateが更新された時
- Propsが変更された時
- 再レンダリングされたコンポーネント配下の子コンポーネント全て
3は中々イメージがつきにくいですが、以下の例のようにルートコンポーネントであるApp.jsxがレンダリングされるとその配下のコンポーネントが全てレンダリングされてしまいます。
変更する必要のないコンポーネントまでレンダリングされてしまうと、アプリ自体のパフォーマンスが落ちてしまいます。
React.memo, useCallbackを使って不要なレンダリングを防ぐ
ここから実際にコードを使って解説します。今回の例では、App.jsを親、子コンポーネントをChild.jsxとします。
// App.js
import { useState } from "react";
import Child from "./Child";
import "./styles.css";
export default function App() {
const [number, setNumber] = useState(0);
const onClickNumber = () => setNumber(number + 1);
console.log("レンダリングしてます");
return (
<div className="App">
<button onClick={onClickNumber}>ボタン</button>
<h1>{number}</h1>
<Child />
</div>
);
}
// Child.jsx
const Child = () => {
return (
<>
{console.log("child")}
<div>childです</div>
</>
);
};
export default Child;
親コンポーネントにあるボタンをクリックすると、子供であるChild.jsxに仕込んだconsole.logもクリックする度に発火してしまいます。
不要なレンダリングが起きているので、子コンポーネントは変更があった場合のみレンダリングさせたいです。
React.memo
コンポーネント自体をmemoで囲うだけでpropsに変更がない限りそのコンポーネントで再レンダリングが起きなくなります。
// child.jsx
// 追加
import { memo } from "react";
// memoで囲うだけ!
const Child = memo(() => {
return (
<>
{console.log("child")}
<div>childです</div>
</>
);
});
export default Child;
ボタンをクリックしても子コンポーネントにあるconsole.logが発火しなくなりました。
useCallback
子コンポーネントにアロー関数を渡すと、memo化していてもレンダリングされるようになります。
これをuseCallbackを活用することで一度定義したアロー関数が更新されない限り、同じものを使い回すという処理を実現できます。
// 書き方
useCallback(アロー関数, [依存配列]);
試しにボタンをクリックすると、子コンポーネントを表示非表示するような実装に変更します。
その際、親コンポーネントにあるアロー関数をpropsで渡してあげます。
// App.js
import { useState } from "react";
import Child from "./Child";
import "./styles.css";
export default function App() {
const [number, setNumber] = useState(0);
const [show, setShow] = useState(false);
const onClickNumber = () => setNumber(number + 1);
// 追加
const onClickShow = () => setShow(!show);
const onClickClose = () => setShow(false);
return (
<div className="App">
<button onClick={onClickNumber}>ボタン</button>
<h1>{number}</h1>
<button onClick={onClickShow}>show/hide</button>
{/* アロー関数をpropsで渡す */}
<Child show={show} onClickClose={onClickClose} />
</div>
);
}
// Child.jsx
import { memo } from "react";
const Child = memo((props) => {
const { show, onClickClose } = props;
return (
<>
{console.log("child")}
{show ? (
<>
<div>childです</div>
<button onClick={onClickClose}>閉じる</button>
</>
) : null}
</>
);
});
export default Child;
コンソールを見ると子コンポーネントにあるconsole.logが再び発火してることが分かります。
memo化したにも関わらず、親コンポーネントにあるボタンをクリックすると、関係のない子コンポーネントも再びレンダリングされるようになってしまいました。
これはアロー関数がレンダリングされる度に再生成されるので、毎回違う関数として認識されてしまいます。
その為、propsが変わっていると判断されるので子コンポーネントも再レンダリングがおきるようになります。
処理の流れとしては以下のようになります。
- 親にあるボタンをクリックする
2. stateが変更される為、親コンポーネントがレンダリングされる - レンダリングすると親コンポーネントのアロー関数が再生成される
- 再生成したアロー関数を子コンポーネントに受け渡す
5. propsが変更されていると判断される為、子コンポーネントもレンダリングされる
これをuseCallbackを使うことで、不要なレンダリングを防ぐことができます。
// App.js
// 追加
import { useState, useCallback } from "react";
import Child from "./Child";
import "./styles.css";
export default function App() {
const [number, setNumber] = useState(0);
const [show, setShow] = useState(false);
const onClickNumber = () => setNumber(number + 1);
const onClickShow = () => setShow(!show);
// 追加
const onClickClose = useCallback(() => setShow(false), [setShow]);
return (
<div className="App">
<button onClick={onClickNumber}>ボタン</button>
<h1>{number}</h1>
<button onClick={onClickShow}>show/hide</button>
<Child show={show} onClickClose={onClickClose} />
</div>
);
}
これで不要なレンダリングを防ぐことができました!