React の Context の更新による不要な再レンダリングを防ぐ 〜useContext を利用した時に発生する不要な再レンダリングを防ぐ方法に関して〜

## はじめに

React（v16.12.0） の Context の更新による不要な再レンダリングを防ぐ方法についての備忘録です。

`useContext`フックなどで利用する Context のデメリットとして

**「Context を更新したら、その Context を利用しているコンポーネントがすべて再レンダリングされてしまう」**

ということが記載されている時がありますが、**関数コンポーネントであれば再レンダリングを防げます**（クラスコンポーネントでもできるかも）。

ということで、この記事は関数コンポーネントを対象としています。

また、デモは CodeSandbox 上に置いてあります。編集して動作を確認してみると理解が深まると思います。

## useContext を利用する上で、理解しておく必要がある概念（用語）

- Context（コンテキスト）
- Context オブジェクト
- Provider（プロバイダ）
- Consumer（コンシューマ）

### Context（コンテキスト）

文脈によって意味合いが異なるが、React に関しては以下のいずれかを指していることが多い。

1. Props を利用せずに様々な階層のコンポーネントに値を共有する React の仕組みや API のこと
2. Context オブジェクトのこと
3. Context オブジェクトの値のこと

「1.」の意味の場合、「React Context」、「React Context API」や「Context API」などと記載されていることもある。

#### 「Context を利用する」とは

前述の「1.」の意味の「Context を利用する」という表現を、より具体的な表現にすると以下の通り。

- 「Context という React の仕組み（API）を利用して、Props を利用せずに様々な階層のコンポーネントに値を渡せるようにする」
- 「`React.createContext`で Context オブジェクトと Provider を定義し、`useContext`で Consumer を定義して、Props を利用せずに様々な階層のコンポーネントに値を渡せるようにする」

### Context オブジェクト

`React.createContext`という React の API（メソッド）の戻り値。

Context オブジェクトと`useContext`を利用することで、Props を利用せずに様々な階層のコンポーネントに値を渡せる（関数コンポーネントの場合）。

### Provider（プロバイダ）

Consumer に値を共有するコンポーネントのこと。Context オブジェクトが保持している。

「Provider コンポーネント」と記載されていることもある。

以下の場合、`MyContext.Provider`コンポーネントが Provider に当たる。

```jsx
import React, { createContext } from "react";

const MyContext = createContext();

export default function App() {
  const name = "soarflat";

  return <MyContext.Provider value={name}></MyContext.Provider>;
}
```

`value`プロパティの値が Context オブジェクトが保持している値であり、`useContext`を利用することで、この値を取得できる。

### Consumer（コンシューマ）

`useContext`などを利用して Context オブジェクトから値を取得するコンポーネントのこと。

「Consumer コンポーネント」と記載されていることもある。

以下の場合、`Child1`コンポーネントが Consumer に当たる。

```jsx
import React, { createContext, useContext } from "react";

const MyContext = createContext();

// Consumer
function Child1() {
  const name = useContext(MyContext);

  return <h1>{name}</h1>;
}

// Consumer ではない
function Child2() {
  return <h2>Not Consumer</h2>;
}

export default function App() {
  const name = "Consumer";

  return (
    <MyContext.Provider value={name}>
      <Child1 />
      <Child2 />
    </MyContext.Provider>
  );
}
```

`Child2`コンポーネントは Context オブジェクトから値を取得していないので、Consumer ではない。

## Provider 内のすべての Consumer は、Provider の`value`プロパティが更新される度に再レンダリングされる

以下は Context（Context オブジェクトの値）の更新が原因で、不要な再レンダリングが発生しているデモ。

Provider 内のすべての Consumer は、Provider の`value`プロパティ(Context オブジェクトの値)が更新される度に再レンダリングされる。

![extra-rerenders-with-react-context.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69667/c09ed5a9-536e-60d6-d372-a64935db6d6e.gif)
[デモを見る](https://codesandbox.io/s/extra-rerenders-with-react-context-uq55o)

```jsx
import React, { createContext, useContext, useReducer } from "react";

const CountContext = createContext();

function countReducer(state, action) {
  switch (action.type) {
    case "increment": {
      return { count: state.count + 1 };
    }
    case "decrement": {
      return { count: state.count - 1 };
    }
    default: {
      throw new Error(`Unhandled action type: ${action.type}`);
    }
  }
}

function CountProvider({ children }) {
  const [state, dispatch] = useReducer(countReducer, { count: 0 });
  const value = {
    state,
    dispatch
  };

  return (
    // value（state か dispatch のどちらか）が更新したら、
    // CountContext.Provider 内のすべての Consumer が再レンダリングされる。
    <CountContext.Provider value={value}>{children}</CountContext.Provider>
  );
}

function Count() {
  console.log("render Count");
  // CountContext からは state のみを取得しているが、
  // dispatch が更新されても再レンダリングされる
  const { state } = useContext(CountContext);

  return <h1>{state.count}</h1>;
}

function Counter() {
  console.log("render Counter");
  // CountContext からは dispatch のみを取得しているが、
  // state が更新されても再レンダリングされる
  const { dispatch } = useContext(CountContext);

  return (
    <>
      <button onClick={() => dispatch({ type: "decrement" })}>-</button>
      <button onClick={() => dispatch({ type: "increment" })}>+</button>
    </>
  );
}

export default function App() {
  return (
    <CountProvider>
      <Count />
      <Counter />
    </CountProvider>
  );
}
```

`Counter`コンポーネントが取得している`dispatch`は更新されることはないが、`state`が更新される度に`Counter`コンポーネントも再レンダリングされてしまう。

上記のデモは特に問題ないが、以下の場合、パフォーマンスの問題を引き起こす可能性がある。

- 不要に再レンダリングされる Consumer の数が多い
- 不要に再レンダリングされる Consumer や、その Consumer の子コンポーネントのレンダリングコストが高い

## Context（Context オブジェクトの値）の更新による不要な再レンダリングを防ぐ方法

Context の更新による不要な再レンダリングを防ぐ方法は以下の３つ。

1. Context（Context オブジェクト）を分割する
2. `React.memo`を利用する
3. `useMemo`を利用する

### Context（Context オブジェクト）を分割する

以下は Context を分割して不要な再レンダリングを防いでいるデモ。

![split-contexts.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69667/3422139d-f56a-966e-fc1f-7ff57710347f.gif)
[デモを見る](https://codesandbox.io/s/split-contexts-snjbs)

```jsx
import React, { createContext, useContext, useReducer } from "react";

const CountStateContext = createContext();
const CountDispatchContext = createContext();

function countReducer(state, action) {
  switch (action.type) {
    case "increment": {
      return { count: state.count + 1 };
    }
    case "decrement": {
      return { count: state.count - 1 };
    }
    default: {
      throw new Error(`Unhandled action type: ${action.type}`);
    }
  }
}

function CountProvider({ children }) {
  const [state, dispatch] = useReducer(countReducer, { count: 0 });

  // CountStateContext.Provider の value が更新したら、
  // CountStateContext の値を取得している全ての Consumer が再レンダリングされる。
  // CountDispatchContext.Provider の value が更新したら、
  // CountDispatchContext の値を取得している全ての Consumer が再レンダリングされる。
  return (
    <CountStateContext.Provider value={state}>
      <CountDispatchContext.Provider value={dispatch}>
        {children}
      </CountDispatchContext.Provider>
    </CountStateContext.Provider>
  );
}

function Count() {
  console.log("render Count");
  // state と dispatch を保持する Context オブジェクトが異なるので、
  // dispatch が更新されてもこのコンポーネントは再レンダリングされない。
  const state = useContext(CountStateContext);

  return <h1>{state.count}</h1>;
}

function Counter() {
  console.log("render Counter");
  // state と dispatch を保持する Context オブジェクトが異なるので、
  // state が更新されてもこのコンポーネントは再レンダリングされない。
  const dispatch = useContext(CountDispatchContext);

  return (
    <>
      <button onClick={() => dispatch({ type: "decrement" })}>-</button>
      <button onClick={() => dispatch({ type: "increment" })}>+</button>
    </>
  );
}

export default function App() {
  return (
    <CountProvider>
      <Count />
      <Counter />
    </CountProvider>
  );
}
```

`state`と`dispatch`を保持する Context を分割したので、`state`を更新しても`Counter`コンポーネントは再レンダリングされない。

何らかの理由で Context を分割できない場合、後述の`React.memo`か`useMemo`を利用した方法で再レンダリングを防ぐ。

### React.memo を利用する

以下のデモは`React.memo`を利用して不要な再レンダリングを防いでいるデモ。

![using-memo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69667/165d6b1f-9ff5-578c-84a5-67e58d3f5618.gif)
[デモを見る](https://codesandbox.io/s/using-reactmemo-mm36x?file=/src/App.js)

```jsx
import React, { createContext, useContext, useReducer } from "react";

const CountContext = createContext();

function countReducer(state, action) {
  switch (action.type) {
    case "increment": {
      return { count: state.count + 1 };
    }
    case "decrement": {
      return { count: state.count - 1 };
    }
    default: {
      throw new Error(`Unhandled action type: ${action.type}`);
    }
  }
}

function CountProvider({ children }) {
  const [state, dispatch] = useReducer(countReducer, { count: 0 });
  const value = {
    state,
    dispatch
  };

  return (
    // value（state か dispatch のどちらか）が更新したら、
    // CountContext.Provider 内のすべての Consumer が再レンダリングされる。
    <CountContext.Provider value={value}>{children}</CountContext.Provider>
  );
}

function Count() {
  console.log("render Count");
  // CountContext からは state のみを取得しているが、
  // dispatch が更新されても再レンダリングされる
  const { state } = useContext(CountContext);

  return <h1>{state.count}</h1>;
}

function Counter() {
  console.log("render Counter");
  // CountContext からは dispatch のみを取得しているが、
  // state が更新されても再レンダリングされる
  const { dispatch } = useContext(CountContext);

  // CountContext.Provider の value の更新による Counter コンポーネントの
  // 再レンダリングは避けられない。そのため、このコンポーネントは CountContext から値を
  // 取得するだけにして、メモ化したコンポーネントに取得した dispatch を渡すようにする。
  return <DispatchButton dispatch={dispatch} />;
}

// dispatch を Props として受け取るコンポーネントをメモ化し、不要な再レンダリングを防ぐ
const DispatchButton = React.memo(({ dispatch }) => {
  console.log("render DispatchButton");

  return (
    <>
      <button onClick={() => dispatch({ type: "decrement" })}>-</button>
      <button onClick={() => dispatch({ type: "increment" })}>+</button>
    </>
  );
});

export default function App() {
  return (
    <CountProvider>
      <Count />
      <Counter />
    </CountProvider>
  );
}
```

Context を分割していないため、`state`を更新しても`Counter`コンポーネントは再レンダリングされる。

そのため、`Counter`コンポーネントは`CountContext`の値を取得するだけにする。

そして、`CountContext`の値を利用するコンポーネント（`DispatchButton`）を切り出し、`React.memo`でメモ化する。

このようにすれば、`state`を更新しても`DispatchButton`コンポーネントは再レンダリングされない。

### useMemo を利用する

以下のデモは`useMemo`を利用して不要な再レンダリングを防いでいるデモ。

![using-memo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69667/165d6b1f-9ff5-578c-84a5-67e58d3f5618.gif)
[デモを見る](https://codesandbox.io/s/using-use-memo-mb1jp?file=/src/App.js)

```jsx
import React, { createContext, useContext, useReducer, useMemo } from "react";

const CountContext = createContext();

function countReducer(state, action) {
  switch (action.type) {
    case "increment": {
      return { count: state.count + 1 };
    }
    case "decrement": {
      return { count: state.count - 1 };
    }
    default: {
      throw new Error(`Unhandled action type: ${action.type}`);
    }
  }
}

function CountProvider({ children }) {
  const [state, dispatch] = useReducer(countReducer, { count: 0 });
  const value = {
    state,
    dispatch
  };

  return (
    // value（state か dispatch のどちらか）が更新したら、
    // CountContext.Provider 内のすべての Consumer が再レンダリングされる。
    <CountContext.Provider value={value}>{children}</CountContext.Provider>
  );
}

function Count() {
  console.log("render Count");
  // CountContext からは state のみを取得しているが、
  // dispatch が更新されても再レンダリングされる
  const { state } = useContext(CountContext);

  return <h1>{state.count}</h1>;
}

function Counter() {
  console.log("render Counter");
  // CountContext からは dispatch のみを取得しているが、
  // state が更新されても再レンダリングされる
  const { dispatch } = useContext(CountContext);

  // CountContext.Provider の value の更新による Counter コンポーネントの
  // 再レンダリングは避けられない。そのため dispatch を利用するレンダリング結果（計算結果）を
  // メモ化し、不要な再レンダリングを防ぐ。
  return useMemo(() => {
    console.log("rerender Counter");
    return (
      <>
        <button onClick={() => dispatch({ type: "decrement" })}>-</button>
        <button onClick={() => dispatch({ type: "increment" })}>+</button>
      </>
    );
  }, [dispatch]);
}

export default function App() {
  return (
    <CountProvider>
      <Count />
      <Counter />
    </CountProvider>
  );
}
```

Context を分割していないため、`state`を更新しても`Counter`コンポーネントは再レンダリングされる。

そのため、`useMemo`で`CountContext`の値を利用するレンダリング結果をメモ化する。

このようにすれば、`state`を更新して`Counter`コンポーネントが再レンダリングされた時は、メモ化されたレンダリング結果を返す。

結果として、`state`を更新しても不要な再レンダリングは発生しない。

## 終わり

Context は便利な API ですが、使い方によってはパフォーマンスの問題を引き起こす可能性があります。

そのため、再レンダリングが発生する条件と、再レンダリングを防ぐ方法を理解した上で利用しましょう。

本記事以外にも React に関連する記事を書いておりますので、興味があればそちらもどうぞ。

- [React.memo / useCallback / useMemo の使い方、使い所を理解してパフォーマンス最適化をする](https://qiita.com/soarflat/items/b9d3d17b8ab1f5dbfed2)

## 参考

- [Preventing rerenders with React.memo and useContext hook. #15156](https://github.com/facebook/react/issues/15156#issuecomment-474590693)

## お知らせ

Udemy で webpack の講座を公開したり、Kindle で技術書を出版しています。

Udemy:
[webpack 最速入門](https://www.udemy.com/course/practical-webpack/?couponCode=B6D5AE3897554CBBDAD7)（**~~10,800 円~~ -> 2,000 円**）

Kindle（Kindle Unlimited だったら無料）:
[React Hooks 入門](https://www.amazon.co.jp/gp/product/B08JTXQ2M6)（500 円）

興味を持ってくださった方はご購入いただけると大変嬉しいです。よろしくお願いいたします。