1
4

More than 3 years have passed since last update.

React.memo, useCallback, useMemoを理解する

Last updated at Posted at 2020-08-04

メモ化

React.memo, useCallback, useMemoに共通する目的はメモ化である。
メモ化とは広範な言い方をするとキャッシュのこと。

React.memo

ラップした関数コンポーネントの初回レンダー結果を記憶し、次回以降同じpropsが与えられた時に、記憶した結果を使用することで、無駄なレンダーをスキップできパフォーマンスを向上させることができる。

React.memoを使用しない場合

const CounterItem = ({ count, label }) => {
  console.log(`${label}がレンダリング`);
  return <div>{label}: {count}</div>;
};

const CounterList = () => {
  const [count1, setCount1] = useState(0);
  const increment1 = () => setCount1(c => c + 1);
  const [count2, setCount2] = useState(0);
  const increment2 = () => setCount2(c => c + 1);
  return (
    <>
      <button onClick={increment1}><CounterItem count={count1} label="左" /></button>
      <button onClick={increment2}><CounterItem count={count2} label="右" /></button>
    </>
  );
};

左右ボタンのどっちを押しても、CounterListコンポーネント内のstateが更新されるので左右のCounterItemコンポーネントを両方再レンダーしてしまう。左ボタンを押した場合は数字が変更される左のCounterItemコンポーネントのみレンダーしたい。右のコンポーネントは変更されてないのでレンダーの必要がない。

c.gif

React.memoを使用する場合

// React.memoで関数コンポーネントをラップ
const CounterItem = React.memo(({ count, label }) => {
  console.log(`${label}がレンダリング`);
  return <div>{label}: {count}</div>;
});

const CounterList = () => {
  const [count1, setCount1] = useState(0);
  const increment1 = () => setCount1(c => c + 1);
  const [count2, setCount2] = useState(0);
  const increment2 = () => setCount2(c => c + 1);
  return (
    <>
      <button onClick={increment1}><CounterItem count={count1} label="左" /></button>
      <button onClick={increment2}><CounterItem count={count2} label="右" /></button>
    </>
  );
};

propsが変更されたコンポーネントしかレンダーされない。

b.gif

useCallback

第一引数に渡したコールバック関数をメモ化する。第二引数に渡した値の配列が変更された場合のみ、コールバック関数が作り直される。React.memoと併用することでパフォーマンスを向上させることができる。

useCallbackを使用しない場合

const CounterItem = React.memo(({ count, label, onClick }) => {
  console.log(`${label}がレンダリング`);
  return <button onClick={onClick}><div>{label}: {count}</div></button>;
});


const CounterList = () => {
  const [count1, setCount1] = useState(0);
  const increment1 = () => setCount1(c => c + 1);
  const [count2, setCount2] = useState(0);
  const increment2 = () => setCount2(c => c + 1);
  return (
    <>
      <CounterItem count={count1} label="左" onClick={increment1} />
      <CounterItem count={count2} label="右" onClick={increment2} />
    </>
  );
};

左ボタンを押した場合、左右のCounterItemコンポーネントを両方再レンダーしてしまう。CounterItemコンポーネントをReact.memoでラップしてるのでpropsの変更がないコンポーネント(右ボタン)はレンダーをスキップして欲しいができていない。

d.gif

上記の問題は参照の同一性が関係している。

"a" === "a" // true
1 === 1 // true

const a = () => {}
const b = () => {} 
// 同じ処理であってもfalseが返る
a === b //false

stateの更新によりCounterListコンポーネントの再レンダリングが起きると、CounterListコンポーネント内のincrement1関数や increment2関数が再度定義される。再定義された関数は上記で示したように同じ参照ではなくなる。つまりCounterItemコンポーネントに渡す関数が同じ参照にはならない。よってReact.memoでラップしてもporpsの参照が違うのでレンダーがスキップされない。

useCallbackを使用する場合

useCallbackを使用することで上記の問題が解決される

const CounterItem = React.memo(({ count, label, onClick }) => {
  console.log(`${label}がレンダリング`);
  return <button onClick={onClick}><div>{label}: {count}</div></button>;
});

const CounterList = () => {
  const [count1, setCount1] = useState(0);
  const increment1 = useCallback(() => setCount1(c => c + 1), []);
  const [count2, setCount2] = useState(0);
  const increment2 = useCallback(() => setCount2(c => c + 1), []);
  return (
    <>
      <CounterItem count={count1} label="左" onClick={increment1} />
      <CounterItem count={count2} label="右" onClick={increment2} />
    </>
  );
};

うまくメモ化できていることが確認できる。

b.gif

useMemo

useCallbackはコールバック関数をメモ化したが、useMemoは値をメモ化する。
useMemoのユースケースは2つある。

useMemoのユースケース1

オブジェクトをコンポーネントのpropsとして渡す前に、オブジェクトをメモ化する

useMemoを使用しない場合

const CounterItem = React.memo(({ params }) => {
  console.log(`${params.label}がレンダリング`);
  return <button onClick={params.handler}><div>{params.label}: {params.value}</div></button>;
});

const CounterList = () => {
  const [count1, setCount1] = useState(0);
  const increment2 = useCallback(() => setCount2(c => c + 1), []);
  const [count2, setCount2] = useState(0);
  const increment1 = useCallback(() => setCount1(c => c + 1), []);

  const params1 = {
    label: '',
    value: count1,
    handler: increment1
  }

  const params2 = {
    label: '',
    value: count2,
    handler: increment2
  }

  return (
    <>
      <CounterItem params={params1} />
      <CounterItem params={params2} />
    </>
  );
};

上記のコードの場合も片方のボタンを押すと両方のボタンが再レンダリングされており、うまくメモ化できていない。左ボタンを押した際にはparams1のcount1が変更されるので左ボタンは再レンダリングされるべきだが、右ボタンの再レンダリングはスキップしたい。

d.gif

上記の問題も参照の同一性が関係している。オブジェクトの中身が同じでも同じ参照にはならない。

{'a': 1} === {'a': 1} // false
[1,2,3] === [1,2,3] // false

stateの更新によりCounterListコンポーネントの再レンダリングが起きると、params1とparams2が再度定義される。つまりCounterItemコンポーネントに渡すparamsが同じ参照にはならないため、例えuseCallbackでコールバック関数をメモ化しても、最終的に渡されるpropsの参照が違うのでレンダリングがスキップされない。

useMemoを使用する場合

上記の問題をuseMemoを使用することで解決できる。

const CounterItem = React.memo(({ params }) => {
  console.log(`${params.label}がレンダリング`);
  return <button onClick={params.handler}><div>{params.label}: {params.value}</div></button>;
});

const CounterList = () => {
  const [count1, setCount1] = useState(0);
  const increment2 = useCallback(() => setCount2(c => c + 1), []);
  const [count2, setCount2] = useState(0);
  const increment1 = useCallback(() => setCount1(c => c + 1), []);

  const params1 = useMemo(() => ({
    label: '',
    value: count1,
    handler: increment1
  }), [count1, increment1])

  const params2 = useMemo(() => ({
      label: '',
      value: count2,
      handler: increment2
  }), [count2, increment2])

  return (
    <>
      <CounterItem params={params1} />
      <CounterItem params={params2} />
    </>
  );
};

うまくメモ化できているのが確認できる。

b.gif

useMemoのユースケース2

重い計算結果をメモ化する。

useMemoを使用しない場合

// 何かしらの重い計算
const heavyCalc = (count) => {
  let i = 0
  while (i < 2000000000) i++
  return count
}

const Counter = () => {
  const [count1, setCount1] = useState(0);
  const increment1 = () => setCount1(c => c + 1);
  const [count2, setCount2] = useState(0);
  const increment2 = () => setCount2(c => c + 1);

  const result = heavyCalc(count1)

  return (
    <>
      <button onClick={increment1}><div>重い計算あり: {result}</div></button>
      <button onClick={increment2}><div>重い計算なし: {count2}</div></button>
    </>
  )
};

どちらのボタンを押しても、コンポーネント内の重い計算処理のせいで再レンダリングが遅れる。重い計算があるheavyCalc関数に渡しているのはcount1だけなので、右ボタンを押した場合はすぐ再レンダリングして欲しい。

c.gif

useMemoを使用する場合

上記の問題をuseMemoを使用することで解決できる。

// 何かしらの重い計算
const heavyCalc = (count) => {
  let i = 0
  console.log('重い計算開始')
  while (i < 2000000000) i++
  return count
}

const Counter = () => {
  const [count1, setCount1] = useState(0);
  const increment1 = () => setCount1(c => c + 1);
  const [count2, setCount2] = useState(0);
  const increment2 = () => setCount2(c => c + 1);

  const result = useMemo(() => heavyCalc(count1), [count1])

  return (
    <>
      <button onClick={increment1}><div>重い計算あり: {result}</div></button>
      <button onClick={increment2}><div>重い計算なし: {count2}</div></button>
    </>
  )
};

左ボタンを押した時は重い計算処理が走るが、右ボタンを押した場合はメモ化した値を返すため無駄な重い計算処理は走っていないので再レンダリングが早い。

a.gif

1
4
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
1
4