React.memo
React.memoの基本
処理した結果をキャッシュとして保持することを「メモ化」と呼ぶ。React.memoはそのメモ化をするための機能。
メモ化することでコンポーネントの再レンダーを回避し、パフォーマンスを上げることができる。レンダリングに時間がかかるコンポーネントや、親が頻繁に再レンダーされる子コンポーネントなどがある場合に効果的。
逆に上記のようなコンポーネント以外では使っても意味がない。
使い方
以下のように使うことで、コンポーネントの再レンダーを避けることができる。
const Hello = React.memo(({ name }) => {
return <h1>Hello {name}>/h1>;
});
再レンダーの判断は、Propsが等価かどうかでチェックされる。上記の場合は、nameが変更されない限り、再レンダーされることはない。
以下の例ではChildコンポーネントに無駄なループをセットしているため、React.memoの効果を理解できる。
import React, { useState } from "react";
const Child = React.memo(({ count }) => {
let i = 0;
while (i < 300000000) i++; //無駄なループを設定しておく
console.log('render Child')
return <p>Child: {count}</p>
})
export default function App() {
console.log('render App')
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
return (
<>
<button onClick={() => setCount1(count1 + 1)}>countup App count</button>
<button onClick={() => setCount2(count2 + 1)}>countup Child count</button>
<p>App: {count1}</p>
<Child count={count2} />
</>
)
}
React.memoを定義せずに実行した場合は、count1が更新されたとしてもChildコンポーネントも再レンダーされ、count2には関係なくても毎回時間がかかることになってしまう。
上記の例ではChildコンポーネントをReact.memoで定義しているため、Childコンポーネントのprops(count2)が更新された時だけ再レンダーされる。count1を更新しても再レンダーされず、素早くレンダリングが完了させることができる。
React.memoではカバーできない部分
React.memoにコールバック関数を渡す場合は、React.memoを使っても再レンダーされてしまう。
// import 省略
// React.memoに関数を渡している
const Child = React.memo(({ handleClick }) => {
console.log('render Child');
return <button onClick={handleClick}>Child</button>
});
export default function App() {
console.log('render App');
const [count, setCount] = useState(0);
const handleClick = () => {
console.log('click');
};
return (
<>
<p>Counter: {count}</p>
<button onClick=() => setCount(count + 1)}>Increment count</button>
<Child handleClick={handoleClick} />
);
}
これは、関数がAppコンポーネントの再レンダリングの度に再生成されるため。オブジェクトとしては前回と異なるものと判断され、Childコンポーネントも再レンダーされるという結果になる。
この問題を解消するためには、useCallbackを使って関数をメモ化する必要がある。
useCallback
useCallbackの基本
useCallbackは関数を受け取り、コンポーネントの再レンダーを防ぐ目的で利用される。React.memoでメモ化したコンポーネントに、useCallbackでメモ化したコールバック関数をPropsとして渡すことで、不要な再レンダーを回避できるようになる。
使い方
書き方は、const 変数 = useCallback(関数, 依存配列)
となる。
一つ前の例を書き換えると以下のようになり、関数がメモ化され再レンダーされなくなる。
// import省略
const Child = React.memo(({ handleClick }) => {
console.log('render Child');
return <button onClick={handleClick}>Child</button>
});
export default function App() {
console.log('render App');
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
console.log('click');
}, []);
return (
<>
<p>Counter: {count}</p>
<button onClick=() => setCount(count + 1)}>Increment count</button>
<Child handleClick={handoleClick} />
);
}
ただし、上記の場合は依存配列を空で設定しているためuseCallbackで生成した関数は初回しかレンダリングされない。
コールバック関数が他の状態に依存する場合は、依存配列に状態変数(下の例だとcount)を追加する必要がある。
const Button = React.memo((props) => {
return <button onClick={props.onClick}>Click Me</button>
});
const App = () => {
const [count, setCount] = useState(1);
const onClick = useCallback(() => {
alert(count);
setCount(count+1);
}, [count]);
return <Button onClick={onClick}/>
};
また、React.memoしていないコンポーネントにuseCallbackでメモ化した関数を渡しても再レンダーのスキップはできないので注意。
useMemo
useMemoの基本
useMemoはメモ化された値を返すフック。
書き方は以下。
useMemo(() => 計算ロジック, 依存配列);
依存配列は計算ロジックが依存している値を格納することで、値が変わると再計算される。 また、計算ロジックだけではなくレンダー結果もメモ化できるため、React.memoと同じようにコンポーネントの再レンダーをスキップできる。
例は以下。
// import省略
export default function App() {
console.log('render App')
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
const double = (count) => {
let i = 0;
while (i < 100000000) i++; // 無駄なループをセットしてわざと時間をかけている
return count * 2;
};
const Counter = useMemo(() => {
console.log('render Counter');
const doubleCount = double(count2);
return (
<p>
Counter: {count2}, {doubledCount}
</p>
);
}, [count2]);
// Counterはcount2にしか依存しないため、count1が更新され、Appコンポーネントが再レンダーされてもCounterは再レンダーされない。
return (
<>
<h2>Increment count1</h2>
<p>Counter: {count1}</p>
<button onClick={() => setCount1(count1 + 1)}>count1 up</button>
<h2>Increment count2</h2>
{Counter}
<button onClick={() => setCount2(count2 + 1)}>count2 up</button>
</>
);
}
React.memo / useCallback / useMemoの違い
- React.memo
const コンポーネント = React.memo((props) => { return <p> props.hoge </p> })
メモ化する対象はコンポーネント。
コンポーネントを定義するときに使い、そのコンポーネントの不要なレンダーを回避する目的で使われる。
再レンダーはpropsが更新された時だけ実行される。
- useCallback
const 変数 = useCallback(() => {hoge}, [依存変数など])
メモ化する対象は関数。
React.memoのpropsが関数の場合に、そのコンポーネントの不要なレンダーを回避する目的で使われる。
関数を定義しているコンポーネントが再レンダーされると、関数は等価ではなくなるので、その関数をpropsとして受け取っている別コンポーネントはReact.memoを使っていても再レンダーされてしまう。
React.memoしているコンポーネントにuseCallbackでメモ化した関数を渡すことで、別コンポーネントの不要なレンダーを回避できる。
ただ、React.memoとか関係なくなるべく使え、って話もある。
useCallbackはとにかく使え! 特にカスタムフックでは - uhyo/blog
- useMemo
const 変数 = useMemo(() => {hoge}, [依存変数など])
メモ化する対象は計算結果の値(数値やレンダー結果)。
計算結果やレンダー結果をメモ化し、所属するコンポーネントが再レンダーされても、メモ化した変数内容が再計算されるのを回避する目的で使われる。他と違って必ずしも再レンダーの回避だけが目的ではない。
再計算のコストとuseMemoの実行コストを比較検討して、使うかどうかを判断する。
- まとめ
useCallbackはuseCallback自身がpropsとして設定されたコンポーネントの再レンダーを回避する目的で使うのに対し、useMemoはuseMemo自身に設定した関数の再実行を回避する目的で使う。