0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

React入門 - Tips12 - React.memo, useCallback, useMemoで無駄なレンダリングを抑制

Last updated at Posted at 2021-10-06

React.memo, useCallBack, useMemoを使う理由

パフォーマンス改善のために使う。です。

React.memo, useCallback, useMemoをちゃんと使うとメモ化され無駄なレンダリングが抑制されます。

メモ化とは、以下です。

  • 再レンダリングされても同じ結果になるものは、初回のものを記録する。
  • 値を記録し再レンダリングされても値を計算しない。 ※値が変化するときのみ処理する。

よく分からないですね。
後半のサンプル動かしながら覚えていきましょう。


Reactをやり始めの頃は、ただただ関数にuseCallbackを付けたら、良いんだろ? と思い実装していました。

上記考え、やり方は無駄な実装です。
(厳密にいうと逆に、useCallbackを処理する分、パフォーマンス悪くなるとの意見もあります。)
ちゃんと理解して使用しましょうということです。

レンダリングが起こる条件

ここここがとても分かりやすかったです。特にここのサンプルを動かすと理解が深まると思います。

Reactコンポーネントの再レンダリングは、おおよそ以下3つの条件で発生します。

  1. propsの更新
  2. stateの更新(自コンポーネントのstate)
  3. 親コンポーネントが再レンダリングされた時

※一つでも条件に当てはまれば、再レンダリングされるということです。

素のサンプルコードを動かしてみよう

メモ化を施していない素のソースです。

import React, { useState } from "react";

/**
 * 親コンポ
 * @returns
 */
export const App: React.FC = () => {
  const [countStateA, setCountStateA] = useState(0);
  const [countStateB, setCountStateB] = useState(0);

  //Aボタンのstateセット用関数
  const incrementACounter = () => setCountStateA(countStateA + 1);

  //Bボタンのstateセット用関数
  const incrementBCounter = () => setCountStateB(countStateB + 1);

  return (
    <>
      <Count text="A ボタン" countState={countStateA} />
      <Count text="B ボタン" countState={countStateB} />
      <button onClick={incrementACounter}>A ボタン</button>
      <button onClick={incrementBCounter}>B ボタン</button>
    </>
  );
};
export default App;

/**
 * 子コンポ
 */
const Count: React.FC<{ text: string; countState: any }> = ({
  text,
  countState,
}) => {
  console.log("Count child component:", text);
  return (
    <p>
      {text}:{countState}
    </p>
  );
};

Aボタン押下時に、Bボタンも描画されています。
Bボタン押下時に、Aボタンも描画されています。

react-memo1.gif

React.memoを使用してみます。

先程のソースの子コンポをReact.memoでラップします。
へい!よー!

import React, { useState } from "react";

/**
 * 親コンポ
 * @returns
 */
export const App: React.FC = () => {
  const [countStateA, setCountStateA] = useState(0);
  const [countStateB, setCountStateB] = useState(0);

  //Aボタンのstateセット用関数
  const incrementACounter = () => setCountStateA(countStateA + 1);

  //Bボタンのstateセット用関数
  const incrementBCounter = () => setCountStateB(countStateB + 1);

  return (
    <>
      <Count text="A ボタン" countState={countStateA} />
      <Count text="B ボタン" countState={countStateB} />
      <button onClick={incrementACounter}>A ボタン</button>
      <button onClick={incrementBCounter}>B ボタン</button>
    </>
  );
};
export default App;

/**
 * 子コンポ
 */
// const Count: React.FC<{ text: string; countState: any }> = ({ ★ ここを変えました。
const Count: React.FC<{ text: string; countState: any }> = React.memo(
  ({ text, countState }) => {
    console.log("Count child component:", text);
    return (
      <p>
        {text}:{countState}
      </p>
    );
    // }; ★ ここを変えました。
  }
);

Aボタン押下時に、Bボタンは描画されなくなりました。
Bボタン押下時に、Aボタンは描画されなくなりました。

react-memo2.gif

無駄な描画を抑制できました。


propsに関数がある場合

先程のReact.memoで無駄な描画を抑制できましたが、
原理を簡単に言うとpropsの中身が変わらなければ描画しない。
ということです。

しかし、propsが関数になると、オブジェクト?なので、毎度前回とは等価ではないとReactは判断するようです。

以下のソースを試してみましょう。
(前のソースを一部書き換えます。)

import React, { MouseEventHandler, useState } from "react";

/**
 * 親コンポ
 * @returns
 */
export const App: React.FC = () => {
  const [countStateA, setCountStateA] = useState(0);
  const [countStateB, setCountStateB] = useState(0);

  //Aボタンのstateセット用関数
  const incrementACounter = () => setCountStateA(countStateA + 1);

  //Bボタンのstateセット用関数
  const incrementBCounter = () => setCountStateB(countStateB + 1);

  return (
    <>
      {/* ★ ここから書き換えます。 start */}
      {/* <Count text="A ボタン" countState={countStateA} />
      <Count text="B ボタン" countState={countStateB} />
      <button onClick={incrementACounter}>A ボタン</button>
      <button onClick={incrementBCounter}>B ボタン</button> */}
      <p>A ボタン: {countStateA} </p>
      <p>B ボタン: {countStateB} </p>
      <Button counterState={incrementACounter} buttonValue="Aボタン" />
      <Button counterState={incrementBCounter} buttonValue="Bボタン" />
      {/* ★ end */}
    </>
  );
};
export default App;

// ★ 追加 start
/**
 * 子コンポ(Button)※React.memoされています。
 */
const Button: React.FC<{
  counterState: MouseEventHandler<HTMLButtonElement> | undefined;
  buttonValue: string;
}> = React.memo(({ counterState, buttonValue }) => {
  console.log("Button child component:", buttonValue);
  return <button onClick={counterState}>{buttonValue}</button>;
});
// ★ 追加 end

// ★ 以下削除
// /**
//  * 子コンポ
//  */
// // const Count: React.FC<{ text: string; countState: any }> = ({ ★ ここを変えました。
// const Count: React.FC<{ text: string; countState: any }> = React.memo(
//   ({ text, countState }) => {
//     console.log("Count child component:", text);
//     return (
//       <p>
//         {text}:{countState}
//       </p>
//     );
//     // }; ★ ここを変えました。
//   }
// );

また、
Aボタン押下時に、Bボタンも描画されています。
Bボタン押下時に、Aボタンも描画されています。
の状態になりました。

react-memo3.gif

あかん。。

関数をuseCallbackでメモ化

してみます。

import React, { MouseEventHandler, useState, useCallback } from "react";

/**
 * 親コンポ
 * @returns
 */
export const App: React.FC = () => {
  const [countStateA, setCountStateA] = useState(0);
  const [countStateB, setCountStateB] = useState(0);

  //Aボタンのstateセット用関数
  // const incrementACounter = () => setCountStateA(countStateA + 1); //★ 書き換え
  const incrementACounter = useCallback(
    () => setCountStateA(countStateA + 1),
    [countStateA]
  );

  //Bボタンのstateセット用関数
  // const incrementBCounter = () => setCountStateB(countStateB + 1); // ★ 書き換え
  const incrementBCounter = useCallback(
    () => setCountStateB(countStateB + 1),
    [countStateB]
  );

  return (
    <>
      <p>A ボタン: {countStateA} </p>
      <p>B ボタン: {countStateB} </p>
      <Button counterState={incrementACounter} buttonValue="Aボタン" />
      <Button counterState={incrementBCounter} buttonValue="Bボタン" />
    </>
  );
};
export default App;

/**
 * 子コンポ(Button)※React.memoされています。
 */
const Button: React.FC<{
  counterState: MouseEventHandler<HTMLButtonElement> | undefined;
  buttonValue: string;
}> = React.memo(({ counterState, buttonValue }) => {
  console.log("Button child component:", buttonValue);
  return <button onClick={counterState}>{buttonValue}</button>;
});

今度はちゃんと
Aボタン押下時に、Bボタンは描画されなくなりました。
Bボタン押下時に、Aボタンは描画されなくなりました。
に出来ました。

react-memo4.gif

useMemoを使ってみる。

先程は、useCallbackを使って関数をメモ化しました。
今度は、値自体をメモ化します。

以下書きかけ

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?