【React】memo関数とは?使い方と効果をわかりやすく解説!

Reactアプリを開発していると、「なんだか動作が重くなってきたな…」と感じることはありませんか?

その原因のひとつとして挙げられるのが「不要な再レンダリング」です。たとえば、「ある子コンポーネントが表示内容を変えていないにもかかわらず、親コンポーネントが更新されたことで巻き込まれて毎回再描画されてしまう」というケースがあります。

こうした無駄な再レンダリングは、アプリのパフォーマンス低下に直結します。

そんなときに役立つのが、Reactのmemo関数(React.memo()です。

この記事では「Reactのmemo関数(React.memo()」について、以下の内容をサンプルコード付きでわかりやすく解説します。

  • memo関数とは?
    • memo関数を使わなかった場合のサンプルコード
    • memo関数を使った場合のサンプルコード
  • memo関数の注意点
    • useCallbackuseMemoを使わなかった場合のサンプルコード
    • useCallbackと一緒に使った場合のサンプルコード
    • useMemoと一緒に使った場合のサンプルコード

memo関数とは?

Reactのmemo関数(React.memo())はコンポーネントの無駄な再レンダリングを防ぐ関数です。

通常、親コンポーネントが再レンダリングされると、その中の子コンポーネントも自動的に再レンダリングされます。子コンポーネントに渡すpropsの内容がまったく同じでも、再レンダリングが発生してしまうのです。

そこでReact.memo()を使うと、子コンポーネントに渡されたpropsが前回と同じであれば、再レンダリングをスキップし、前回の表示内容をそのまま使ってくれます。

memo関数を使わなかった場合

まず、memo関数を使わないサンプルコードを見てみましょう。

import { useState } from 'react';

const Child = () => {
  console.log('Childコンポーネントが再レンダリングされました');
  return <div>子コンポーネント</div>;
};

export default function App() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <h1>カウント: {count}</h1>
      <button onClick={() => setCount(count + 1)}>+1</button>
      <Child />
    </div>
  );
}

上記のサンプルコードでは、+1ボタンを押すたびに子コンポーネント(Child)も再レンダリングされます。子コンポーネント(Child)の中の表示は一切変わっていないのに、親コンポーネント(App)が再レンダリングされると子コンポーネント(Child)も毎回再描画されてしまいます。

memo関数を使った場合

次に、子コンポーネント(Child)をmemo関数でラップしたサンプルコードを見てみましょう。

import { memo, useState } from 'react';

const Child = memo(() => {
  console.log('Childコンポーネントが再レンダリングされました');
  return <div>子コンポーネント</div>;
});

export default function App() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <h1>カウント: {count}</h1>
      <button onClick={() => setCount(count + 1)}>+1</button>
      <Child />
    </div>
  );
}

上記のサンプルコードでは、+1ボタンを押して親コンポーネント(App)が再レンダリングされても、子コンポーネント(Child)は再描画されません。コンソールに「Childコンポーネントが再レンダリングされました」が表示されないことで確認できます。

memo関数の効果

Reactアプリが大きくなってくると、「不要な再レンダリング」が増えて、動作が重くなることがあります。

たとえば、ある子コンポーネントの表示内容が変わっていないのに、親が再レンダリングされた影響で毎回描画されてしまうようなケースです

こういった「子コンポーネントが受け取るpropsが変わっていないのに再レンダリングされてしまう無駄」を防ぐために使えるのが、memo関数です。

memo関数を使えば、子コンポーネントが受け取るpropsが同じなら前回の描画結果を再利用してくれるので、無駄な処理を減らしてアプリのパフォーマンスを改善することができます。

memo関数の注意点

React.memo()を使えば、propsが変わらない限り子コンポーネントの再レンダリングを防げます。しかし、propsに「関数」や「オブジェクト」を渡す場合は注意が必要です。

なぜなら、JavaScriptでは関数やオブジェクトは毎回新しい参照として扱われるからです。

この問題を解決するには、useCallbackuseMemoを使って関数やオブジェクトの参照を固定する必要があります。

useCallbackと一緒に使った場合

useCallbackを使わないサンプルコードを見てみましょう。

import { memo, useState } from 'react';

const Child = memo(({ onClick }) => {
  console.log('Childが再レンダリングされました');
  return <button onClick={onClick}>子ボタン</button>;
});

export default function App() {
  const [count, setCount] = useState(0);

  // 毎回新しい関数になる
  const handleClick = () => {
    console.log('子ボタンがクリックされました');
  };

  return (
    <div>
      <h1>{count}</h1>
      <button onClick={() => setCount(count + 1)}>+1</button>
      <Child onClick={handleClick} />
    </div>
  );
}

親コンポーネント(App)が再レンダリングされるたびに、handleClick関数は毎回新しい関数オブジェクトとして作られます。そのため、子コンポーネント(Child)にとっては「propsonClick)の値が毎回変わっている」と判断されます。よって、memo関数を使っていても子コンポーネント(Child)は毎回再レンダリングされてしまうのです。

useCallbackを使って関数オブジェクトの参照を固定した際のサンプルコードを以下に示します。

import { memo, useCallback, useState } from 'react';

const Child = memo(({ onClick }) => {
  console.log('Childが再レンダリングされました');
  return <button onClick={onClick}>子ボタン</button>;
});

export default function App() {
  const [count, setCount] = useState(0);

  // useCallbackで関数の参照を固定
  const handleClick = useCallback(() => {
    console.log('子ボタンがクリックされました');
  }, []);

  return (
    <div>
      <h1>{count}</h1>
      <button onClick={() => setCount(count + 1)}>+1</button>
      <Child onClick={handleClick} />
    </div>
  );
}

useCallbackを使うことで、親コンポーネント(App)が再レンダリングされても、handleClick関数の参照が毎回変わらないため、子コンポーネント(Child)から見ると「propsが前回と同じ」と判断できます。その結果、memo関数が効いて、子コンポーネント(Child)の無駄な再レンダリングが防げます。

あわせて読みたい

useCallback』については下記の記事で詳しく説明しています。興味のある方は下記のリンクからぜひチェックをしてみてください。

useMemoと一緒に使った場合

useMemoを使わないサンプルコードを見てみましょう。

import { memo, useState } from 'react';

const Child = memo(({ user }) => {
  console.log('Childが再レンダリングされました');
  return <div>こんにちは、{user.name}さん!</div>;
});

export default function App() {
  const [count, setCount] = useState(0);

  // 毎回新しいオブジェクトになる
  const user = { name: '太郎' }; 

  return (
    <div>
      <h1>カウント: {count}</h1>
      <button onClick={() => setCount(count + 1)}>+1</button>
      <Child user={user} />
    </div>
  );
}

親コンポーネント(App)が再レンダリングされるたびに、userオブジェクトが毎回新しく作られます。そのため、子コンポーネント(Child)にとっては「propsuser)の値が毎回変わっている」と判断されます。よって、memo関数を使っていても子コンポーネント(Child)は毎回再レンダリングされてしまうのです。

useMemoを使ってオブジェクトの参照を固定した際のサンプルコードを以下に示します。

import { memo, useMemo, useState } from 'react';

const Child = memo(({ user }) => {
  console.log('Childが再レンダリングされました');
  return <div>こんにちは、{user.name}さん!</div>;
});

export default function App() {
  const [count, setCount] = useState(0);

  // useMemoでオブジェクトの参照を固定
  const user = useMemo(() => ({ name: '太郎' }), []);

  return (
    <div>
      <h1>カウント: {count}</h1>
      <button onClick={() => setCount(count + 1)}>+1</button>
      <Child user={user} />
    </div>
  );
}

useMemoを使うことで、親コンポーネント(App)が再レンダリングされても、userオブジェクトの参照が毎回変わらないため、子コンポーネント(Child)から見ると「propsが前回と同じ」と判断できます。その結果、memo関数が効いて、子コンポーネント(Child)の無駄な再レンダリングが防げます。

本記事のまとめ

この記事では「Reactのmemo関数(React.memo()」について、以下の内容を説明しました。

  • memo関数とは?
    • memo関数を使わなかった場合のサンプルコード
    • memo関数を使った場合のサンプルコード
  • memo関数の注意点
    • useCallbackuseMemoを使わなかった場合のサンプルコード
    • useCallbackと一緒に使った場合のサンプルコード
    • useMemoと一緒に使った場合のサンプルコード

お読み頂きありがとうございました。

スポンサーリンク