2022-01-12

[React]Propsにchildrenとしてcomponentを渡している際に起こる不要な再レンダリング

blogimg

概要

WhyDidYouRenderという不要な再レンダリングを検知してコンソールに出してくれるライブラリを入れると、

different React elements (remember that whyDidYouRender.js:
the <jsx/> syntax always produces a *NEW* immutable React
element so a component that receives <jsx/> as props always re-
renders).

というものが出てきました。これがなんで起きているのか、どう解決したのかを自分用のメモとして残しておきます。

実際にこれが起こるサンプルを再現したソースコードを用意しましたのでこちらを基に解説します。

https://github.com/s-amano/rerender-sample

再レンダリングを再現

元となる表示のためのコンポーネント(App.tsx)

function App() {
	  const [visible, setVisible] = useState(false);
	  window.setTimeout(() => setVisible(true), 3000);
	  return (
	    <div className="App" style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
	      {visible ? <div>loaded</div> : <div>now loading...</div>}
	      <Parent>
	        <Children />
	      </Parent>
	    </div>
	  );
	}

visible という状態が変化すると、Appコンポーネントは正常な再レンダリングが起きます。ただし、この場合visible の状態に関係がないParentコンポーネントも不要に再レンダリングが起きてしまいます。

ここで注目してほしいのは、Parentコンポーネント自体は以下のようにmemo化ができているという点です。

親コンポーネント(Parent.tsx)

export interface Props {
	  children: JSX.Element;
}

const Parent: React.FC<Props> = React.memo(({ children }) => {  // ←ちゃんとmemo化している。
	  console.log('parent component');
	  return (
	    <div>
	      <p>Parent</p>
	      {children}
	    </div>
	  );
});

Propsとして、コンポーネントを渡しています。

子コンポーネント(Children.tsx)

const Children: React.FC = () => {
	  return <div>children component</div>;
};

なぜ不要な再レンダリングが起きるのか

まず前提として、親コンポーネントが再レンダリングされた時、その配下にある子コンポーネントは全て再レンダリングされてしまいます。

また、コンポーネントはPropsがオブジェクトの時、差分がなくても(内容が同じであっても)再レンダリングするという特性があります。

→参照: コードを読んでやっと理解できた React の shouldComponentUpdate

上記のことから、、、

Appコンポーネントに再レンダリングが起きるとその配下となるChildrenコンポーネントもレンダリング前と違うものと判定されてしまいます。すると、ChildrenコンポーネントをPropsとして持っているParentコンポーネントから見ると、「propsが変わった」となりParentコンポーネント自体にmemo化がされているにも関わらず再レンダリングしてしまうのです。

つまり、Parentコンポーネントに渡しているPropsであるChildrenコンポーネントはReact Elementであり、単なるオブジェクトであるから同様の内容であっても再レンダリングが走るということです。

解決方法

解決方法自体は至って単純です。

AppコンポーネントでChildrenコンポーネントをmemo化するだけです。

App.tsx

function App() {
  const [visible, setVisible] = useState(false);
  window.setTimeout(() => setVisible(true), 3000);
  const memorizedChildren = useMemo(() => <Children />, []);  //←ここでmemo化
  return (
    <div
      className="App"
      style={{
        display: "flex",
        justifyContent: "center",
        alignItems: "center",
      }}
    >
      {visible ? <div>loaded</div> : <div>now loading...</div>}
      <Parent>
        {memorizedChildren}
      </Parent>
    </div>
  );
}