再レンダリングと一緒に理解するReact.memo

30563 ワード

こんにちは👋

今回はReact.memoを再レンダリングと絡めて使い方と使い所なんかをイメージしやすく簡単に説明していきたいと思います。

再レンダリングが行われるシーン

再レンダリングが行われる条件としては主に以下の3つがあります。

  1. stateが更新されたとき
  2. propsが変更されたとき
  3. 親コンポーネントが再レンダリングされたとき(親コンポーネントが再レンダリングされたとき、全ての子コンポーネントが再レンダリングされる)

Reactで規模の大きいアプリを作成する際、上記の条件を意識しないと余計なコンポーネントまで再レンダリングされてしまい、画面がもっさりしてしまう原因となってしまいます。

メモ化とは?

Reactには前述のような無駄な計算を行わないようにするためにメモ化と呼ばれる概念があります。
メモ化は、何らかの計算によって得られた値を記憶しておき、その値が再度必要になったときに再計算を行うことなく記憶しておいた値を再利用するといったものになります。

React.memoとは?

React.memoは、あるコンポーネントに新しく渡されたpropsが前回渡されたpropsと同じであった場合、再レンダリングを行わないようにするための機能です。

それでは、実際に使用してその効果を確認してみましょう。

React.memoを体験する

React.memoを使用しない場合

まずはReact.memoを使用していない場合の再レンダリングについて確認したいので、以下のようなサンプルを作成します。

app.js
import { useState } from "react";
import styled from "styled-components";

// Component style
const StParentContainer = styled.div`
  height: 400px;
  width: 500px;
  background-color: #ffb6b9;
  margin: 0 auto;
`;

const StChildContainer = styled(StParentContainer)`
  height: 150px;
  width: 300px;
  background-color: #8ac6d1;
`;

const StAreaName = styled.p`
  font-size: 2rem;
  text-align: center;
  color: white;
`;

const StCounterContainer = styled.div`
  display: flex;
  justify-content: center;
  align-items: center;
  flex-direction: column;
  gap: 10px;
`;

// Components
const ChildArea = ({ isDisplay }) => {
  console.log("Child Areaがレンダリングされました");

  return isDisplay ? (
    <StChildContainer>
      <StAreaName>Child Area</StAreaName>
    </StChildContainer>
  ) : null;
};

export const App = () => {
  console.log("Parent Areaがレンダリングされました");

  const [count, setCount] = useState(0);
  const [isDisplay, setIsDisplay] = useState(false);

  const onClickCountUp = () => {
    setCount(count + 1);
    if (count % 5 === 4) {
      setIsDisplay(!isDisplay);
    }
  };

  return (
    <StParentContainer>
      <StAreaName>Parent Area</StAreaName>
      <StCounterContainer>
        <p>{count}</p>
        <button onClick={onClickCountUp}>Count Up</button>
      </StCounterContainer>
      <ChildArea isDisplay={isDisplay} />
    </StParentContainer>
  );
};


下のCodeSandboxと合わせて説明しますが、ピンク色のエリアを親コンポーネント(Parent Area)、水色のエリアを子コンポーネント(Child Area)として、Count Upボタンが1クリックされるごとにsetCount()によってcountが更新され、Parent Areaに描画されている数字が1ずつ増えていきます。次に、countが5で割り切れる数字になることを条件として、setIsDisplay()によってisDisplayの真理値が反転し、isDisplayが子コンポーネントであるChildAreaに渡され、isDisplayの値によってChildAreaの表示、非表示が切り替わるようになっています。そして最後に、レンダリングが行われた事が分かるようにconsole.log()でコンソールにメッセージを表示するようにしました。

それでは、実際にCount UpボタンをクリックしてConsoleに表示されるメッセージを確認してみましょう。



いかがだったでしょうか。ボタンを押すたびにParent AreaChild Areaの両方が再レンダリングされていることが確認できたかと思います。

ここでよく考えてみると、isDisplayに変化がない場合はChildAreaに変化がないので、isDisplayの値が変わる場合以外はボタンが押されるたびにChildAreaについて無駄な再計算が行われていることになります。

そこで、ChildAreaが受け取っているpropsの値に変化があった場合のみ再レンダリングが行われるように、React.memoを使用してパフォーマンスの改善をしていきたいと思います。


React.memoを使用したパフォーマンス改善

React.memoの使い方はとても簡単で、メモ化したいコンポーネントをReact.memoでラップしてあげるだけです。

今回の場合はChildAreaに対して無駄な再計算が行われることを回避したいので、ChildAreaReact.memoでラップします。

app.js
import { memo, useState } from "react"; // ① memoのインポートを追加
import styled from "styled-components";

// Component style
const StParentContainer = styled.div`
  height: 400px;
  width: 500px;
  background-color: #ffb6b9;
  margin: 0 auto;
`;

const StChildContainer = styled(StParentContainer)`
  height: 150px;
  width: 300px;
  background-color: #8ac6d1;
`;

const StAreaName = styled.p`
  font-size: 2rem;
  text-align: center;
  color: white;
`;

const StCounterContainer = styled.div`
  display: flex;
  justify-content: center;
  align-items: center;
  flex-direction: column;
  gap: 10px;
`;

// Components
// ② ChildAreaをmemoでラップしてあげる
const ChildArea = memo(({ isDisplay }) => {
  console.log("Child Areaがレンダリングされました");

  return isDisplay ? (
    <StChildContainer>
      <StAreaName>Child Area</StAreaName>
    </StChildContainer>
  ) : null;
});

export const App = () => {
  console.log("Parent Areaがレンダリングされました");

  const [count, setCount] = useState(0);
  const [isDisplay, setIsDisplay] = useState(true);

  const onClickCountUp = () => {
    setCount(count + 1);
    if (count % 5 === 4) {
      setIsDisplay(!isDisplay);
    }
  };

  return (
    <StParentContainer>
      <StAreaName>Parent Area</StAreaName>
      <StCounterContainer>
        <p>{count}</p>
        <button onClick={onClickCountUp}>Count Up</button>
      </StCounterContainer>
      <ChildArea isDisplay={isDisplay} />
    </StParentContainer>
  );
};


コメントアウトで①、②としている部分がReact.memoの実装部分になります。
それでは、CodeSandboxでどんな変化があったか確認してみましょう。



ボタンを押してもisDisplayの値に変更がない場合はChildAreaの再計算が行われず、isDisplayの値に変化があった場合のみChild Areaがレンダリングされましたとコンソールに表示されると思います。

最後に

今回はReact.memoを使用することで、コンポーネントが受け取ったpropsの値に変化がないときに無駄な再計算を行わないようにできることを確認しました。

こうなると色んなコンポーネントに対してReact.memoを使用したくなりますが、メモ化にもコストはかかっています。なので、パフォーマンスチューニングの際にはメモ化のコストとコンポーネントの再計算のコストをしっかり比較して実装を行うようにすると良いと思います。

以上で再レンダリングとReact.memoについての説明を終わります。

最後までお読みいただきありがとうございました。