今回はReact.memo / useMemo / useCallback の違いについて記録を残していきたいと思います。
特に指定しなくてもアプリケーションとしては動くかなと思いますが、余計なレンダリングを避け、効率的な実装を実現するには積極的に取り入れていきたい機能です。
React.memoの使い方
まずはReact.memoを使わない実装。
import React, { useState, useMemo, useCallback } from "react";
const Child_1 = () => {
  return (
    <div>
      <p>Child_1コンポーネント1</p>
    </div>
  );
};
export default function Parent() {
  const [text, setText] = useState("");
  const handleChange = (e) => {
    setText(e.target.value);
  };
  return (
    <div>
      <p>親コンポーネント</p>
      <input type="text" onChange={handleChange} value={text} className="border-2" />
      <Child_1 />
    </div>
  );
}この場合、親コンポーネントのinputが書き換わるたびに子コンポーネントが再レンダリングされる。
inputが変わっても子コンポーネントが再レンダリングされる必要がない場合、memo化することで再レンダリングさせないようにすることができる。
memo化した実装
import React, { useState, useMemo, useCallback } from "react";
const Child_1 = React.memo(() => {
  return (
    <div>
      <p>Child_1コンポーネント1</p>
    </div>
  );
});
export default function Parent() {
  const [text, setText] = useState("");
  const handleChange = (e) => {
    setText(e.target.value);
  };
  return (
    <div>
      <p>親コンポーネント</p>
      <input type="text" onChange={handleChange} value={text} className="border-2" />
      <Child_1 />
    </div>
  );
}useMemoの使い方
次にuseMemo。useMemoは簡単に言うと「変数のメモ化」
親コンポーネントが更新されるたびに、子コンポーネントの複雑な処理も走ると処理が重くなる。
初期読み込み時と値が変わらない場合、複雑な処理をスキップすることができる。
useMemoを使わないコード
import React, { useState, useMemo, useCallback } from "react";
export default function Parent() {
  const [text, setText] = useState("");
  const [count, setCount] = useState(0);
  const double = (count: number) => {
    let i = 0;
    while (i < 30000000) i++;
    return count * 2;
  };
  const doubleCount = double(count);
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setText(e.target.value);
  };
  return (
    <div>
      <p>親コンポーネント</p>
      <input type="text" onChange={handleChange} value={text} className="border-2" />
      <p>親コンポーネントの重い処理</p>
      <p>
        count: {count},{doubleCount}
      </p>
      <button onClick={() => setCount(count + 1)}>クリック</button>
    </div>
  );
}
これだとinputを入力するたびにdouble関数の重い処理がレンダリングされる。
100点のコード
import React, { useState, useMemo, useCallback } from "react";
export default function Parent() {
  const [text, setText] = useState("");
  const [count, setCount] = useState(0);
  const double = (count: number) => {
    let i = 0;
    while (i < 30000000) i++;
    return count * 2;
  };
  const doubleCount = useMemo(() => double(count), [count]);
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setText(e.target.value);
  };
  return (
    <div>
      <p>親コンポーネント</p>
      <input type="text" onChange={handleChange} value={text} className="border-2" />
      <p>親コンポーネントの重い処理</p>
      <p>
        count: {count},{doubleCount}
      </p>
      <button onClick={() => setCount(count + 1)}>クリック</button>
    </div>
  );
}const doubleCount = useMemo(() => double(count), [count]);
と言うようにuseMemoで関数の呼び出し元を囲む。第二引数を指定し、countが更新された時だけdouble関数が走るようになる。
こうすることでinputを変更してもdouble関数は読み込まれない。
useCallbackの使い方
最後にuseCallback。useCallbackも関数をメモ化することで複雑な処理をスキップすることができる。useMemoと似ている。
useCallbackを使わない実装。
import React, { useState, useMemo, useCallback } from "react";
const Child_1 = React.memo(() => {
  return (
    <div>
      <p>Child_1コンポーネント1</p>
    </div>
  );
});
const Child_2 = React.memo((props: { handleClick: () => void }) => {
  return (
    <div>
      <p>Child_2コンポーネント1</p>
      <button onClick={props.handleClick}>click</button>
    </div>
  );
});
export default function Parent() {
  const [text, setText] = useState("");
  const [count, setCount] = useState(0);
  const double = (count: number) => {
    let i = 0;
    while (i < 30000000) i++;
    return count * 2;
  };
  const doubleCount = useMemo(() => double(count), [count]);
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setText(e.target.value);
  };
  const handleClick = () => {
    console.log("click");
  };
  return (
    <div>
      <p>親コンポーネント</p>
      <input type="text" onChange={handleChange} value={text} className="border-2" />
      <Child_1 />
      <Child_2 handleClick={handleClick} />
      <p>親コンポーネントの思い処理</p>
      <p>
        count: {count},{doubleCount}
      </p>
      <button onClick={() => setCount(count + 1)}>クリック</button>
    </div>
  );
}
inputが入力されると状態が変わる、親コンポーネントがレンダリングされる、レンダリングされるとhandleClickが生成される、毎回生成されると言うことはpropsが毎回生成される、child_2コンポーネントが毎回走る。
handleClickはクリックボタンを押した時だけ生成してくれれば良い
useCallbackを使った実装。
"use client";
import React, { useState, useMemo, useCallback } from "react";
// eslint-disable-next-line react/display-name
const Child_1 = React.memo(() => {
  return (
    <div>
      <p>Child_1コンポーネント1</p>
    </div>
  );
});
// eslint-disable-next-line react/display-name
const Child_2 = React.memo((props: { handleClick: () => void }) => {
  return (
    <div>
      <p>Child_2コンポーネント1</p>
      <button onClick={props.handleClick}>click</button>
    </div>
  );
});
export default function Parent() {
  const [text, setText] = useState("");
  const [count, setCount] = useState(0);
  const double = (count: number) => {
    let i = 0;
    while (i < 30000000) i++;
    return count * 2;
  };
  const doubleCount = useMemo(() => double(count), [count]);
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setText(e.target.value);
  };
  const handleClick = useCallback(() => {
    console.log("click");
  }, []);
  return (
    <div>
      <p>親コンポーネント</p>
      <input type="text" onChange={handleChange} value={text} className="border-2" />
      <Child_1 />
      <Child_2 handleClick={handleClick} />
      <p>親コンポーネントの思い処理</p>
      <p>
        count: {count},{doubleCount}
      </p>
      <button onClick={() => setCount(count + 1)}>クリック</button>
    </div>
  );
}
第二引数を指定でき、空は初回レンダリング時のみ。
以上がReact.memo / useMemo / useCallback の違いでした。