この記事について
この記事ではuseReducer
またはuseState
とContext
を使うことで値をグローバルに保持しつつ再レンダリングを減らす方法について解説している。
Context について
Context とは
Context
とは React において複数コンポーネント間で値を共有するための仕組みである。値を間接的に渡すことでprops
による受け渡しを減らし、コード量を減らす事ができる。
Context の注意点
Context
を使う事で、コンポーネントの再利用を難しくしてしまったり、可読性が悪くなる可能性があるため、使う時には慎重に使う必要がある。 しかし、contextを用途に合わせて使っていくのは悪くないので、使う時には以下の点に気をつける。
-
アプリケーション内の全てのstateを一つにまとめる必要はなく、責務によってcontextを分けるようにする。(いくつもProviderを作っても問題ない)
-
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
を扱うことになるため、利用者が安全に扱う事ができる。
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()];
}
次にカウントするためのボタンを実装する。
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>
</>
);
};
最後にこれまで実装してきたコンポーネントを組み込む。
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
参考
How to use React Context effectively - Kent C. Dotts