LoginSignup
6
1

More than 1 year has passed since last update.

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

Last updated at Posted at 2020-12-08

前回までのあらすじ

Unstated Nextは、複数コンポーネントにより組み立てられたツリーの中で、状態を共有して管理するライブラリです。基本的な使い方は「React + Unstated Next: 複数コンポーネントのツリーの中で状態を共有して管理する」で、簡単なカウンターのサンプルをつくりながらご説明しました(図001)。

図001■Unstated Nextを使ったカウンター

2009001_001.png

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

リデューサを使う

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

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

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

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フックで状態の保持を切り分ける

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;
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に渡して、コンテナをつくってください。

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)が丸ごと直に触れないことです。何を参照してよいかは、リデューサコンテナが決められます。

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)はリデューサを参照しますので、リデューサの子にしなければなりません。

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■リデューサをコンテナに変換した

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);
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);
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
>> CodeSandboxへ

参考:「Create React App フックによる状態管理 04: カスタムフックにUnstated Nextを組み合わせる

6
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
1