LoginSignup
5
2

More than 3 years have passed since last update.

propsにobjectを使用するとReact.memoがメモ化できない

Last updated at Posted at 2020-02-07

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となるのです:point_up:
ちなみに、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の方はpropspropsで比較を行う。
だから、rest parametersを使用していようがいまいが関係ない。
useMemoの方はarrarrrestrestを比較する。
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は使用していなさそうだけど、結局やってることは全く同じに見える。

5
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2