サイトトップ

Director Flash 書籍 業務内容 プロフィール

HTML5テクニカルノート

Create React App フックによる状態管理 03: コンテクストにuseReducerを組み合わせる


Create React App フックによる状態管理 02: useContextでコンテクストを使う」では、ロジックが含まれたカスタムコンテクストのプロバイダで、アプリケーションを包みました。。具体的にでき上がったのは、前回のサンプル002です(以下に再掲)。今回はさらに、useReducerフックと組み合わせて、状態の操作と保持を切り分けます。

図001■カウンターのサンプル

図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.js

const 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.js

const 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>
	);
};

src/reducder.js

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」からの引用です。

通常、useReduceruseStateより好ましいのは、複数の値にまたがる複雑な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.