5
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Contextの最適化

Last updated at Posted at 2020-02-29

この記事について

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

Context について

Context とは

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

Context の注意点

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

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

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

useReducer ・ useState について

useState とは

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

useReducer とは

useReducerとはInitialStateReducerを渡すことで、Redux のReducerのような動きを実装できる関数である。この関数を実行すると、statedispatchが返される。

注目すべきなのはdispatchで、dispatchは変更される事がない値なので、再レンダリングされる事がないという事である。

Contextを正しく使う

ContextuseReducer(useState)を合わせて使う時に、ドキュメントには以下のように書かれている。

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

つまり、Contextを使う時にはdispatch用のContextstate用の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>
  );
};

以上で完成である。

まとめ

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

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

参考

React ドキュメント - useReducer

React ドキュメント - FAQ

How to use React Context effectively - Kent C. Dotts

How to optimize your context value - Kent C. Dotts

Application State Management with React - Kent C. Dotts

5
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?