HTML5テクニカルノート
Create React App フックによる状態管理 03: コンテクストにuseReducerを組み合わせる
- ID: FN2011003
- Technique: ECMAScript 2015
- Library: React 17.0.1
「Create React App フックによる状態管理 02: useContextでコンテクストを使う」では、ロジックが含まれたカスタムコンテクストのプロバイダで、アプリケーションを包みました。。具体的にでき上がったのは、前回のサンプル002です(以下に再掲)。今回はさらに、useReducer
フックと組み合わせて、状態の操作と保持を切り分けます。
図001■カウンターのサンプル
Create React App フックによる状態管理 02: サンプル002■React state management 02-02: Creating provider component
01 コンテクストにuseReducerを使う
前回は、アプリケーションのコンポーネントから、ロジックをコンテクストに切り出しました。このロジックの中身をさらに見ると、状態の操作と保持に分けられます。新たな状態をつくって保持するのがリデューサ(reducer)の役割です。useReducer()
フックは、引数に受け取ったリデューサ関数(reducer
)と状態の初期値(initialState
)に対して、状態の参照(state
)とアクション(action)配信関数(dispatch
)が要素に収められた配列を返します。
const [state, dispatch] = useReducer(reducer, initialState);
状態が変更されるべきことを伝えるのはアクションです。配信関数がリデューサに送ります。リスナーイベントとは違って、アクションごとのハンドラはありません。リデューサがまとめて扱うため、アクションは何が起こったのか示すtype
プロパティをもつことが必須です。リデューサはswitch
文で、type
ごとの処理を行うことになります。
つぎのリデューサモジュールsrc/reducder.js
は、アクションのtype
をひとつだけ('increment'
)扱えるようにしたコードです。リデューサを用いた仕組みでは、引数から状態(state
)は参照できるものの、状態を直には変えません。リデューサは新たなプロパティ値のオブジェクトを返すだけで、これにより状態が改められるのです。
src/reducder.jsconst reducer = (state, action) => { switch (action.type) { case 'increment': return { count: state.count + 1 }; default: console.error('error'); } }; export default reducer;
コンテクストのモジュールsrc/AppContext.js
は、useReducer
フックの戻り値から受け取ったアクション配信関数(dispatch
)により、アクションをリデューサに送ります。アクションにはtype
のほかにも、リデューサが必要とするプロパティを含めて構いません。複数のプロパティをひとつのオブジェクト(よくpayload
と名づけられます)にまとめてもよいでしょう。今回はtype
だけで足ります。useReducer
は現在の状態も配列要素(state
)に返しますので、必要な値(count
)はそこから得てください。
src/AppContext.js// import React, { createContext, useState } from 'react'; import React, { createContext, useReducer, useState } from 'react'; import reducer from './reducder'; export const AppProvider = ({ children }) => { const [state, dispatch] = useReducer(reducer, { count: initialCount }); // const increment = () => setCount((prevCount) => prevCount + 1); const increment = () => dispatch({ type: 'increment' }); return ( // <AppContext.Provider value={{ count, reset, decrement, increment }}> <AppContext.Provider value={{ count: state.count, reset, decrement, increment }}> {children} </AppContext.Provider> ); };
リデューサモジュールsrc/reducder.js
には、あとふたつ処理したいアクションのがあります。case
文をつぎのようにふたつ加えましょう。コンテクストモジュールsrc/AppContext.js
の記述は、以下のコード001にまとめました。併せて、リデューサのコード全体も掲げます。それぞれのモジュールの具体的なコードと実際の動きは、CodeSandboxに公開したサンプル001でお確かめください。
src/reducder.jsconst reducer = (state, action) => { switch (action.type) { case 'reset': return { count: 0 }; case 'decrement': return { count: state.count - 1 }; } }; export default reducer;
コード001■useReducerフックで状態の保持をコンテクストから分ける
src/AppContext.js
import React, { createContext, useReducer } from 'react';
import reducer from './reducder';
const initialCount = 0;
export const AppContext = createContext(initialCount);
export const AppProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, { count: initialCount });
const reset = () => dispatch({ type: 'reset' });
const decrement = () => dispatch({ type: 'decrement' });
const increment = () => dispatch({ type: 'increment' });
return (
<AppContext.Provider value={{ count: state.count, reset, decrement, increment }}>
{children}
</AppContext.Provider>
);
};
const reducer = (state, action) => {
switch (action.type) {
case 'reset':
return { count: 0 };
case 'decrement':
return { count: state.count - 1 };
case 'increment':
return { count: state.count + 1 };
default:
console.error('error');
}
};
export default reducer;
サンプル001■React state management 03-01: useReducer with context
なぜリデューサを使うのか
つぎの説明は「フック API リファレンス」「useReducer」からの引用です。
通常、useReducer
がuseState
より好ましいのは、複数の値にまたがる複雑なstate
ロジックがある場合や、前のstate
に基づいて次のstate
を決める必要がある場合です。また、useReducer
を使えばコールバックの代わりにdispatch
を下位コンポーネントに渡せるようになるため、複数階層にまたがって更新を発生させるようなコンポーネントではパフォーマンスの最適化にもなります。
02 アクションの配信にuseCallback()フックを使う
前掲コード001でカウンターの動きはでき上がりです。つぎに、無駄を減らすことについて考えましょう。
コンポーネント内に定めた関数は、放っておけばレンダーのたびにつくり直されます。それを必要なときだけ行うのが、useCallback
フックです。第1引数に呼び出す関数(コールバック)を渡します。大切なのば第2引数です。配列要素として渡した参照の値(依存値)が変わったときに関数はつくり直されます。
import { useCallback } from 'react'; const メモ化された関数 = useCallback(関数, [...依存値]);
改めて、コンテクストモジュールsrc/AppContext.js
に定められた関数を見てみましょう。配信関数dispatch
に引数として渡すアクションは、type
プロパティだけで値は変わりません。このようなとき第2引数の依存配列は空[]
で与えればよいのです。なお、第2引数を省くとレンダーのたびにコールバックはつくり直されるので、意味がなくなることにご注意ください。したがって、dispatch
を呼び出す関数はすべてuseCallback
で包み、第2引数には空の依存配列[]
を与えるということです。
src/AppContext.js// import React, { createContext, useReducer } from 'react'; import React, { createContext, useCallback, useReducer } from 'react'; export const AppProvider = ({ children }) => { // const reset = () => dispatch({ type: 'reset' }); const reset = useCallback(() => dispatch({ type: 'reset' }), []); // const decrement = () => dispatch({ type: 'decrement' }); const decrement = useCallback(() => dispatch({ type: 'decrement' }), []); // const increment = () => dispatch({ type: 'increment' }); const increment = useCallback(() => dispatch({ type: 'increment' }), []); };
モジュールsrc/AppContext.js
の中身はつぎのコード002のように書き替えました。また、以下のサンプル002をCodeSandboxに公開しています。
コード002■アクション配信の関数にuseCallbackフックを使う
src/AppContext.js
import React, { createContext, useCallback, useReducer } from 'react';
import reducer from './reducder';
const initialCount = 0;
export const AppContext = createContext({ count: initialCount });
export const AppProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, { count: initialCount });
const reset = useCallback(() => dispatch({ type: 'reset' }), []);
const decrement = useCallback(() => dispatch({ type: 'decrement' }), []);
const increment = useCallback(() => dispatch({ type: 'increment' }), []);
return (
<AppContext.Provider value={{ count: state.count, reset, decrement, increment }}>
{children}
</AppContext.Provider>
);
};
サンプル002■React state management 03-02: useCallback to dispatch actions
作成者: 野中文雄
作成日: 2020年11月23日
Copyright © 2001-2020 Fumio Nonaka. All rights reserved.