propsにobjectを渡すとメモ化してくれない
import React, { useState, memo } from "react";
import ReactDOM from "react-dom";
function App() {
const [cnt, setCnt] = useState(1);
const obj = { hoge: "hoge" };
return (
<div className="App">
{cnt}
<button onClick={() => setCnt(cnt + 1)}>button</button>
<Test arr={obj} />
</div>
);
}
const Test = memo(props => {
console.log("test", props.arr);
return <div>test</div>;
});
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
上記の例で行くと、buttonを押下したときにtest
は表示して欲しくない。
けど、実際にはボタンを押すたびにtest
が表示されるのでメモ化できていないということになる。
{hoge: "hoge"}を毎回渡しているのでpropsは変化していないにも関わらず。
React.memoのshallowEqual
React.memoは第二引数に比較用の関数を渡さなければshallowEqualが実行される。
shallowEqual、、名前だけ聞くとオブジェクトの一階層の値が同じであれば同じobjectと判断してくれそうに感じる。
が、上にあげた例の通り実際には違うobjectと判断する。
shallowEqualはReactが用意した関数なので、中身をのぞいてみる。
https://github.com/facebook/react/blob/cbbc2b6c4d0d8519145560bd8183ecde55168b12/packages/shared/shallowEqual.js
オブジェクトの比較は以下でやってそうである。
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
if (keysA.length !== keysB.length) {
return false;
}
// Test for A's keys different from B.
for (let i = 0; i < keysA.length; i++) {
if (
!hasOwnProperty.call(objB, keysA[i]) ||
!is(objA[keysA[i]], objB[keysA[i]])
) {
return false;
}
}
なるほど、やっぱりオブジェクトの中身まで見てくれてるじゃん。
と、思ったがよく見るとそうではなかった。
props = {arr: {hoge: 'hoge'}};
のObject.keys(props)
の結果は['arr']
である。
なので、!is(objA[keysA[i]], objB[keysA[i]]
が必ずfalse
となるのです
ちなみに、isは同じファイルの中に書いてある比較用関数。今回のケースでは===
でobject同士が比較されてfalseとなる。
const hoge = {hoge: 'hoge'};
const fuga = {hoge: 'hoge'};
hoge === fuga; // false
結果、shallowEqualはfalseを返し、React.memoが思った様に動作してくれないのでした。
useMemoで回避
上記例でいうと、 const obj
のあたりをuseMemoでメモ化した値を使う様にすることでReact.memoは思った通りに動作してくれる様になる。
備考
ちなみに、React.memoを使わずにuseMemoだけで行けると思って試した下記はダメだった。
import React, { useState, memo, useMemo } from "react";
import ReactDOM from "react-dom";
function App() {
const [cnt, setCnt] = useState(1);
const obj = { hoge: "hoge" };
console.log("app");
return (
<div className="App">
{cnt}
<button onClick={() => setCnt(cnt + 1)}>button</button>
<Test arr={obj} />
</div>
);
}
const Test = props => {
return useMemo(() => { // ここだけ変えた
console.log("test", props.arr);
return <div>test</div>;
}, [props.arr]);
};
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
ボタンを押下するたびにApp()が実行されて、objが毎回生成されるのでprops.arrに詰めるobjの参照が毎回違うためです。
さらに備考
下記は同じ様に見えて違う
function App() {
.....~~
.....~~
return (
<div className="App">
{cnt}
<button onClick={() => setCnt(cnt + 1)}>button</button>
<Test arr={obj} ccc={obj2} /> {/* 渡すのはどちらもuseMemoでメモ化したもの */}
</div>
);
}
// memoされる
const Test = memo(({ arr, ...rest }) => {
console.log("test", arr, rest);
return <div>test</div>;
});
// memoされない
const Test = ({ arr, ...rest }) => {
return useMemo(() => {
console.log("test", arr, rest);
return <div>test</div>;
}, [arr, rest]);
};
React.memoの方はprops
とprops
で比較を行う。
だから、rest parametersを使用していようがいまいが関係ない。
useMemoの方はarr
とarr
、rest
とrest
を比較する。
arrの比較は同じと判断されるが、restはTestが実行されるたびに毎回作成されるので参照先が違う。
参照: https://aloerina01.github.io/blog/2018-10-25-1
メモ
useMemoの比較は以下でやっているっぽい。
https://github.com/facebook/react/blob/901d76bc5c8dcd0fa15bb32d1dfe05709aa5d273/packages/react-reconciler/src/ReactFiberHooks.js#L299
Object.isのpolyfillを自分で用意して===
で比較しテイるぽい。
shallowEqualは使用していなさそうだけど、結局やってることは全く同じに見える。