Contextの最適化

# この記事について
この記事では`useReducer`または`useState`と`Context`を使うことで値をグローバルに保持しつつ再レンダリングを減らす方法について解説している。

# Context について

## Context とは
`Context`とは React において複数コンポーネント間で値を共有するための仕組みである。値を間接的に渡すことで`props`による受け渡しを減らし、コード量を減らす事ができる。

## Context の注意点
`Context`を使う事で、コンポーネントの再利用を難しくしてしまったり、可読性が悪くなる可能性があるため、使う時には慎重に使う必要がある。 しかし、contextを用途に合わせて使っていくのは悪くないので、使う時には以下の点に気をつける。

1. アプリケーション内の全てのstateを一つにまとめる必要はなく、責務によってcontextを分けるようにする。(いくつもProviderを作っても問題ない)

2. contextはグローバルにアクセスできる必要はなく、 必要に応じて小さい単位で保ち、必要な場所で正しく使うようにする。

# useReducer ・ useState について

## useState とは
`useState`とは`Function Component`において`state`を使用するための関数(Hooks)である。

## useReducer とは
`useReducer`とは`InitialState`と`Reducer`を渡すことで、Redux の`Reducer`のような動きを実装できる関数である。この関数を実行すると、`state`と`dispatch`が返される。
  
注目すべきなのは`dispatch`で、`dispatch`は変更される事がない値なので、再レンダリングされる事がないという事である。

# Contextを正しく使う
`Context`と`useReducer`(`useState`)を合わせて使う時に、ドキュメントには以下のように書かれている。

> アプリケーションの state については、props として渡していくか（より明示的）、あるいはコンテクスト経由で渡すか（深い更新ではより便利）を選ぶ余地が依然あります。もしもコンテクストを使って state も渡すことにする場合は、2 つの別のコンテクストのタイプを使ってください — dispatch のコンテクストは決して変わらないため、dispatch だけを使うコンポーネントは（アプリケーションの state も必要でない限り）再レンダーする必要がなくなります。

つまり、`Context`を使う時には`dispatch`用の`Context`と`state`用の`Context`を分けて宣言することで、`state`を使用していない`Component`では再レンダリングされる事がないため、パフォーマンスの改善になるのである。

# なぜ再レンダリングがよくないのか

前提として、`React`では再レンダリングをするときに`Virtual DOM`同士の差をとって、もし前回の`Virtual DOM`と違っていたら差分だけ更新を行う。この時、`差分を検知するための処理`と`差分を反映させるための処理`が走る。この2つの処理は再レンダリングが多ければ多いほど増えていき、重くなっていくのである。さらにJavaScriptはシングルスレッドで動作する言語であるため、一つの処理がメインスレッドを占有してしまうと、他の処理が動作しなくなってしまう。

つまり、再レンダリングによって無駄なスクリプトが走ることですぐに実行したいスクリプトの実行が遅れてしまうのである。

以上の理由から再レンダリングを抑制し、意図したタイミングで処理が実行できるようにする必要があるのである。

# 実践

今回はわかりやすいようにサンプルアプリケーションは深くならないように作っているが、実際のアプリケーションの場合はこの程度の深さであれば`Context`は不要である。

今回はサンプルアプリケーションとして`Counter`アプリを作成していく。

最初に`Context`を実装する。
ここでの注意点として、`Context`へのアクセスは`Hooks`を作ってそこからアクセスした方が操作しやすいし、可読性もよくなる。また、開発者が意図した方法で`Context`を扱うことになるため、利用者が安全に扱う事ができる。

```counterContext.jsx

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

/** 
 * **宣言時の注意**
 * - cerateContextにデフォルトの値を指定しなくて良い
 * - 指定しない事で、Contextが宣言されていない時には値がないので初期化されていない事がわかる
 * - defaultの値が必要になることはほとんどないので指定しない
 * - typescriptを使っている場合は`type Value = {count: number} | undefined`のように指定する
*/
const CounterStateContext = createContext();
const CounterDispatchContext = createContext();

const counterInitialState = {
  count: 0
};

const counterReducer = (state, action) => {
  switch(action.type) {
    case 'COUNT_UP':
      return {
        ...state,
        count: state.count + 1
      };
    case 'COUNT_DOWN':
      return {
        ...state,
        count: state.count - 1
      };
    default:
      throw new Error('Unhandled action type: ', action.type);
  }
}

// stateのContextとdispatchのContextを分ける事で、stateを使用していないComponentで再レンダリングを避ける事ができる
export const CounterProvider = ({ children }) => {
  const [state, dispatch] = useReducer(counterReducer, counterInitialState);

  return (
    <CounterStateContext.Provider value={state}>
      <CounterDispatchContext.Provider value={dispatch}>
        {children}
      </CounterDispatchContext.Provider>
    </CounterStateContext.Provider>
  );
}

// state用のContextから値を取得するためのHooks
export const useCounterState = () => {
  const state = useContext(CounterStateContext);
  if(state === undefined) {
    throw new Error('useCounterState must be used within a CounterProvider')
  }
  return state;
}

// dispatch用のContextから値を取得するためのHooks
export const useCounterDispatch = () => {
  const dispatch = useContext(CounterDispatchContext);
  if(dispatch === undefined) {
    throw new Error('useCounterState must be used within a CounterProvider')
  }
  return dispatch;
}

// 両方使いたい時用のHooks
export const useCounter = () => {
  return [useCounterState(), useCounterDispatch()];
}

```

次にカウントするためのボタンを実装する。

```CountButton.jsx

import React, { useCallback } from 'react';
import { useCounterDispatch } from '../contexts/CounterContext';

// dispatchのみを使用する事で再レンダリングを抑制する事ができる
export const CountButton = () => {
  const dispatch = useCounterDispatch();

  const countDown = useCallback(() => dispatch({ type: 'COUNT_DOWN' }));
  const countUp = useCallback(() => dispatch({ type: 'COUNT_UP' }));

  return (
    <>
      <button onClick={countUp} >count up</button>
      <button onClick={countDown} >count down</button>
    </>
  );
};

```

最後にこれまで実装してきたコンポーネントを組み込む。

```App.jsx

import React from 'react';
import { CounterProvider } from '../contexts/CounterContext';
import { Counter } from './Counter';
import { CountButton } from './CountButton';

export const App = () => {
  return (
    <CounterProvider>
      <div>
        <Counter />
        <CountButton />
      </div>
    </CounterProvider>
  );
};

```

以上で完成である。

# まとめ

- `Context`で`useReducer`や`useState`を使用する時には`state`と`dispatch`を分離する事で、再レンダリングを抑制する事ができる
- `Context`は責務を考えながら実装していく
- `Context`は開発者側で、利用者が意図した使い方ができるように制限する
- `Context`を分離する事で`React.memo()`を使用してレンダリングを制限する必要がなくなる

使用したコード ... https://github.com/keiya01/optimize-context

# 参考

[React ドキュメント - useReducer](https://ja.reactjs.org/docs/hooks-reference.html#usereducer)

[React ドキュメント - FAQ](https://ja.reactjs.org/docs/hooks-faq.html#how-to-avoid-passing-callbacks-down)

[How to use React Context effectively - Kent C. Dotts](https://kentcdodds.com/blog/how-to-use-react-context-effectively)

[How to optimize your context value - Kent C. Dotts](https://kentcdodds.com/blog/how-to-optimize-your-context-value)

[Application State Management with React - Kent C. Dotts](https://kentcdodds.com/blog/application-state-management-with-react)