React + Unstated Next: リデューサを使ったうえでコンテナに変換する

## 前回までのあらすじ
[Unstated Next](https://github.com/jamiebuilds/unstated-next#unstated-next)は、複数コンポーネントにより組み立てられたツリーの中で、状態を共有して管理するライブラリです。基本的な使い方は「[React + Unstated Next: 複数コンポーネントのツリーの中で状態を共有して管理する](https://qiita.com/FumioNonaka/items/95603658819108da7552)」で、簡単な[カウンターのサンプル](https://codesandbox.io/s/react-unstated-next-counter-example-refactored-j65q4?file=/src/useCounter.js)をつくりながらご説明しました(図001)。

#### 図001■Unstated Nextを使ったカウンター
![2009001_001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/857a2ed8-98ae-8665-31d0-c2189bc4ab26.png)

本項では、カウンターのサンプルにリデューサ(reducer)を加えます。そのうえで、コンテナに変換して、状態の操作と保持のロジックを分離してみます。上記にリンクしたカウンターのサンプルに手を加えるかたちで進めましょう。


# リデューサを使う
リデューサを使うと、コンテナから値の保持が切り分けられます。用いるフックは[`useReducer`](https://ja.reactjs.org/docs/hooks-reference.html#usereducer)です。引数にはつぎのように、リデューサ関数と初期状態(オブジェクト)を渡します。戻り値は配列で、要素は現行の状態(`state`)とアクション配信関数(`dispatch`)のふたつです。

```react
const [state, dispatch] = useReducer(リデューサ関数, 初期状態);
```

`useReducer`は、コンテナモジュール`src/useCounter.js`からつぎのように呼び出します。`dispatch`は、アクションと呼ばれるオブジェクトをリデューサに送る関数です。アクションには何が起こったかを示す`type`プロパティが含まれ、必要に応じてその他のデータも加えられます(今回用いたアクションは`type`プロパティしかもちません)。プロバイダから子コンポーネントに渡したい状態は、`useReducer`が返した配列要素の`state`から参照してフックの戻り値に加えてください。

```react:src/useCounter.js
// import { useCallback, useState } from "react";
import { useCallback, useReducer } from 'react';

const useCounter = (initialState = 0) => {
	// const [count, setCount] = useState(initialState);
	const [state, dispatch] = useReducer(reducer, { count: initialState });
	// const decrement = useCallback(() => setCount((prevCount) => prevCount - 1), []);
	const decrement = useCallback(() => dispatch({type: 'decrement'}), []);
	// const increment = useCallback(() => setCount((prevCount) => prevCount + 1), []);
	const increment = useCallback(() => dispatch({type: 'increment'}), []);
	// return { count, decrement, increment };
	return { count: state.count, decrement, increment };
};
```

新たなリデューサモジュール`src/reducer.js`の記述は以下のコード001のとおりです。イベントと違って、アクションごとのハンドラはもちません。そのため、アクションの`type`プロパティに応じて処理を分ける`switch`文で組み立てます。アクションの配信と同じく、リデューサも状態(`state`)を直にはいじりません。改めた状態のプロパティをオブジェクトに収めて返すだけです。

これで、`useReducer`フックにより状態の保持が切り分けられました。書き直したカスタムフックのモジュール`src/useCounter.js`も、併せてコード001に掲げます。

#### コード001■useReducerフックで状態の保持を切り分ける
```react:src/reducer.js
const reducer = (state, action) => {
	switch (action.type) {
		case 'decrement':
			return {count: state.count - 1};
		case 'increment':
			return {count: state.count + 1};
		default:
			return state;
	}
};
export default reducer;
```

```react:src/useCounter.js
import { useCallback, useReducer } from 'react';
import { createContainer } from 'unstated-next';
import reducer from './reducer';

const useCounter = (initialState = 0) => {
	const [state, dispatch] = useReducer(reducer, { count: initialState });
	const decrement = useCallback(() => dispatch({type: 'decrement'}), []);
	const increment = useCallback(() => dispatch({type: 'increment'}), []);
	return { count: state.count, decrement, increment };
};

export const CounterContainer = createContainer(useCounter);
```


# リデューサをコンテナにする
さらに、リデューサ(`react:src/reducer.js `)もコンテナにしてみましょう。コンテナにすることで、ロジックがよりはっきり切り分けられます。

コンテナにするには、まずカスタムフックに書き替えなければなりません。フックの関数(`useCounterReducer`)を新たに加え、`useReducer`はその中から呼び出します。戻り値は、子コンポーネントに共有する参照が収められたオブジェクトです。カスタムフックを`createContainer`に渡して、コンテナをつくってください。

```react:src/reducer.js
import { useReducer } from 'react';
import { createContainer } from 'unstated-next';

const initialState = { count: 0 };
/* const reducer = (state, action) => {

}; */
const useCounterReducer = () => {
	const [state, dispatch] = useReducer(reducer, initialState);
	return {
		dispatch,
		count: state.count
	};
}

// export default reducer;
export default createContainer(useCounterReducer);
```

状態を操作するコンテナ(`src/useCounter.js`)は、もはや`useReducer`は用いず、リデューサコンテナ(`reducer`)に対して呼び出した`useContainer`から参照を得ます。注目していただきたいのは、`useReducer`と異なり状態(`state`)が丸ごと直に触れないことです。何を参照してよいかは、リデューサコンテナが決められます。

```react:src/useCounter.js
// import { useCallback, useReducer } from 'react';
import { useCallback } from 'react';

const useCounter = (initialState = 0) => {
	// const [state, dispatch] = useReducer(reducer, { count: initialState });
	const { count, dispatch } = reducer.useContainer();

	// return { count: state.count, decrement, increment };
	return { count, decrement, increment };
};
```

リデューサコンテナ(`reducer`)もプロバイダでコンポーネントツリーを包みます。状態を操作するコンテナ(`CounterContainer`)はリデューサを参照しますので、リデューサの子にしなければなりません。

```react:src/App.js
import reducer from './reducer';

function App() {
	return (
		<reducer.Provider>
			<CounterContainer.Provider>

			</CounterContainer.Provider>
		</reducer.Provider>
	);
}
```

状態操作のコンテナ(`src/useCounter.js`)に加えて、リデューサもコンテナ(`src/reducer.js`)にしました。書き替えた3つのモジュールの記述は、つぎのコード002にまとめたとおりです。他のモジュールの記述や動きについては、以下のサンプル002をご覧ください。

#### コード002■リデューサをコンテナに変換した
```react:src/reducer.js
import { useReducer } from 'react';
import { createContainer } from 'unstated-next';

const reducer = (state, action) => {
	switch (action.type) {
			case 'decrement':
					return {count: state.count - 1};
			case 'increment':
					return {count: state.count + 1};
			default:
					return state;
	}
};

const initialState = { count: 0 };
const useCounterReducer = () => {
	const [state, dispatch] = useReducer(reducer, initialState);
	return {
		dispatch,
		count: state.count
	};
}

export default createContainer(useCounterReducer);
```

```react:src/useCounter.js
import { useCallback } from 'react';
import { createContainer } from 'unstated-next';
import reducer from './reducer';

const useCounter = (initialState = 0) => {
	const { count, dispatch } = reducer.useContainer();
	const decrement = useCallback(() => dispatch({type: 'decrement'}), []);
	const increment = useCallback(() => dispatch({type: 'increment'}), []);
	return { count, decrement, increment };
};

export const CounterContainer = createContainer(useCounter);
```

```react:src/App.js
import React from 'react';
import reducer from './reducer';
import { CounterContainer } from './useCounter';
import CounterDisplay from './CounterDisplay';
import './App.css';

function App() {
	return (
		<reducer.Provider>
			<CounterContainer.Provider>
				<div className="App">
					<CounterDisplay />
				</div>
			</CounterContainer.Provider>
		</reducer.Provider>
	);
}

export default App;
```

#### サンプル001■リデューサをコンテナに変換したカウンター
[![2009001_002.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/ff53c6a1-7872-f96d-a3cd-560988b1a5c9.png)
\>> CodeSandboxへ](https://codesandbox.io/s/react-unstated-next-creating-container-from-reducer-17hww?file=/src/reducer.js)


#### 参考:「[Create React App フックによる状態管理 04: カスタムフックにUnstated Nextを組み合わせる](https://fumiononaka.com/Business/html5/FN2012001.html)」