前回までのあらすじ
Unstated Nextは、複数コンポーネントにより組み立てられたツリーの中で、状態を共有して管理するライブラリです。基本的な使い方は「React + Unstated Next: 複数コンポーネントのツリーの中で状態を共有して管理する」で、簡単なカウンターのサンプルをつくりながらご説明しました(図001)。
図001■Unstated Nextを使ったカウンター
本項では、カウンターのサンプルにリデューサ(reducer)を加えます。そのうえで、コンテナに変換して、状態の操作と保持のロジックを分離してみます。上記にリンクしたカウンターのサンプルに手を加えるかたちで進めましょう。
リデューサを使う
リデューサを使うと、コンテナから値の保持が切り分けられます。用いるフックはuseReducer
です。引数にはつぎのように、リデューサ関数と初期状態(オブジェクト)を渡します。戻り値は配列で、要素は現行の状態(state
)とアクション配信関数(dispatch
)のふたつです。
const [state, dispatch] = useReducer(リデューサ関数, 初期状態);
useReducer
は、コンテナモジュールsrc/useCounter.js
からつぎのように呼び出します。dispatch
は、アクションと呼ばれるオブジェクトをリデューサに送る関数です。アクションには何が起こったかを示すtype
プロパティが含まれ、必要に応じてその他のデータも加えられます(今回用いたアクションはtype
プロパティしかもちません)。プロバイダから子コンポーネントに渡したい状態は、useReducer
が返した配列要素のstate
から参照してフックの戻り値に加えてください。
// 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フックで状態の保持を切り分ける
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;
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
に渡して、コンテナをつくってください。
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
)が丸ごと直に触れないことです。何を参照してよいかは、リデューサコンテナが決められます。
// 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
)はリデューサを参照しますので、リデューサの子にしなければなりません。
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■リデューサをコンテナに変換した
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);
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);
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;