sagantaf

メモレベルの技術記事を書くブログ。

React.memoとReact HooksのuseCallbackとuseMemo

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自身に設定した関数の再実行を回避する目的で使う。

参考

基礎から学ぶ React/React Hooks

基礎から学ぶ React/React Hooks

Amazon