Reactコンポーネントの再レンダリング条件検証


本記事は「フロントエンドの開発効率を向上するヒントを教え合おう！」イベントの参加記事です。

https://qiita.com/official-events/5fdc281997d5754d8ac9

Reactで再レンダリングを防止するのはパフォーマンス面で効果があると思いますが、コンポーネントが再レンダリングされる条件、特にmemo化したコンポーネントでのレンダリング条件がいまいちわかってなかったので整理してみました。

再レンダリングのチェック方法は↓の記事に書きましたので参照してください。

https://qiita.com/ckbys/items/7eddd79aeb2a16119739

# 親コンポーネントが再レンダリングされたら子コンポーネントも再レンダリングされる

次のような構造の場合、親コンポーネントがステートの変更で再レンダリングされると子コンポーネントも再レンダリングされました。

Parentコンポーネントでinputに何か入力すると`onChange`で`_count`がインクリメントされるようにしています。
ステートが変わってParentコンポーネントが再レンダリングされるとChildコンポーネントも再レンダリングされます。
また、Childコンポーネントに特にPropsは渡していないのでPropsに依らないことがわかりました。

```tsx
const Child = () => {
  return <div>child</div>;
};

const Parent = () => {
  const [_count, setCount] = useState(0);

  const onChange = useCallback(() => {
    setCount((prev) => prev + 1);
  }, [setCount]);

  return (
    <div>
      <input onChange={onChange} />

      <Child />
    </div>
  );
};

```

# 子コンポーネントをmemo化してみる

## 子コンポーネントにPropsを渡さない場合

この場合はChildコンポーネントはmemo化でキャッシュされるので再レンダリングされませんでした。

```tsx
const Child = memo(() => {
  return <div>child</div>;
});

```

## 子コンポーネントにPropsを渡す場合

### Propsがprimitiveな値で構成される場合

※ primitiveな値はここではnumberとかstringを指すことにします。

ParentコンポーネントからPropsに同じ値を渡してみました。
この場合もChildコンポーネントは再レンダリングされませんでした。

```tsx
const Child = memo((props: { num: number }) => {
  return <div>child</div>;
});

const Parent = () => {
  ... // 省略

  return (
    <div>
      <input onChange={onChange} />

      <Child num={1} />
    </div>
  );
```

### Propsを構成する値にオブジェクトが含まれる場合

#### オブジェクトをChildコンポーネント呼び出し時に定義する場合

Propsにオブジェクトが含まれる場合ですが、Childコンポーネントを呼び出す部分にベタ書きでオブジェクトを書いて渡してみました。
この場合はChildコンポーネントは再レンダリングされました。
値が変わらなくてもオブジェクト自体はParentコンポーネントがレンダリングされる度に生成されるので、React側では別オブジェクトとして判定されてしまいます。

```tsx
const Child = memo((props: { obj: { num: number }}) => {
  return <div>child</div>;
});

const Parent = () => {
  ... // 省略

  return (
    <div>
      <input onChange={onChange} />

      <Child obj={{num: 1}} />
    </div>
  );
```

#### オブジェクトをChildコンポーネント呼び出し前に定義する場合

次にChildコンポーネントを呼び出す前にParentコンポーネント内に定数として宣言して渡してみました。
この場合もChildコンポーネントは再レンダリングされました。
先ほどと同様、React側で別オブジェクトとして判定されてしまっています。

```tsx
const Child = memo((props: { obj: { num: number }}) => {
  return <div>child</div>;
});

const Parent = () => {
  ... // 省略

  const obj = {num: 1};

  return (
    <div>
      <input onChange={onChange} />

      <Child obj={obj} />
    </div>
  );
```

#### オブジェクトをuseMemoで定義する場合

次にオブジェクトをuseMemoで定義して渡してみました。
この場合はChildコンポーネントは再レンダリングされませんでした。
先ほどとは違い、React側でオブジェクトが変わっていないという判定をされているようです。

```tsx
const Child = memo((props: { obj: { num: number }}) => {
  return <div>child</div>;
});

const Parent = () => {
  ... // 省略

  const obj = useMemo(() => ({ num: 1 }), []);

  return (
    <div>
      <input onChange={onChange} />

      <Child obj={obj} />
    </div>
  );
```

#### オブジェクトをuseStateで定義する場合

最後によく使うだろうパターンとして、オブジェクトをuseStateで定義して渡してみました。
この場合もChildコンポーネントは再レンダリングされませんでした。

```tsx
const Child = memo((props: { obj: { num: number }}) => {
  return <div>child</div>;
});

const Parent = () => {
  ... // 省略

  const [obj] = useState({ num: 1 });

  return (
    <div>
      <input onChange={onChange} />

      <Child obj={obj} />
    </div>
  );
```

# React.memoのProps比較

React.memoが再レンダリングする条件については公式のドキュメントに書かれています。

https://ja.reactjs.org/docs/react-api.html#reactmemo

> React.memo は props の変更のみをチェックします。React.memo でラップしているあなたのコンポーネントがその実装内で useState、useReducer や useContext フックを使っている場合、state やコンテクストの変化に応じた再レンダーは発生します。

Propsのオブジェクトについては

> デフォルトでは props オブジェクト内の複雑なオブジェクトは浅い比較のみが行われます。比較を制御したい場合は 2 番目の引数でカスタム比較関数を指定できます。

というように書かれていて、[該当部分のコード](https://github.com/facebook/react/blob/6dbccb92493c3a4fea14b6832ceb4ac42e6330c1/packages/shared/shallowEqual.js#L18-L21)を見るとオブジェクトは[is関数](https://github.com/facebook/react/blob/6dbccb92493c3a4fea14b6832ceb4ac42e6330c1/packages/shared/objectIs.js#L10-L18)で`===`で比較されていました。

なのでオブジェクト内の値が一致しているかどうかではなく、そもそもオブジェクトが一致しないといけないということになります。

# まとめ

- 通常何もしなければ、親コンポーネントが再レンダリングされると子コンポーネントは再レンダリングされる。
- 子コンポーネントをmemo化して再レンダリングを防ごうとしても、Propsにオブジェクトが渡されている場合はHooks(useMemo、useState、useCallbackなど)で状態管理しないと再レンダリングされてしまう。

再レンダリングを防ぐことが必須ではないとは思いますが、この辺を意識すると思わぬ再レンダリングを防ぐことはできそうです！