Reduxの原則

Reactでは、コンポーネントの状態(State)が個別に扱われていますが、ログイン機能など、アプリケーション内でグローバルに持たせたいStateもあります。そういったケースでグローバルなStateを管理する機能を提供するのがReduxです。ReduxはFacebook社が提唱するFluxアーキテクチャに影響を受けています。

Reduxの3つの原則

<値の保管場所は一つだけ>
すべてのデータは「ストア」と呼ばれるところに保管されます。これは、アプリケーションごとに一つだけ用意しておき、すべての値はこの「Store」の中で保持されます。Storeが一つのオブジェクトに集約されてるので、デバッグもしやすくなります。

<値は読み取り専用>
「ストア」に用意されている値は、参照だけ可能で、変更は許可されていません。 変更する手段は、どんなイベントが起こったかを表現するActionのみです。

<変更は別途、関数で行う>
値を作成し直す純粋関数を用意することで、ストアの更新が可能となります。 FluxアーキテクチャのStoreにあるStateを変更する責務を切り出したものとして、Reducerという副作用の無い関数で行ます。前の状態とActionを引数にとって、新たなStateを返します。

Reducerを抽象化すると以下の式で表現されます。
PrevState(値の前の状態)とActionを引数にとって、新たなStateを返します。

(prevState, action) => newState

そもそも何故Reduxを使うのか?

色々と意見があるかと思いますが、Stateの見通しを良くし、どこからでもグローバルなStateを参照/変更可能にするためと考えています。

ReactのみでStateを管理しようとすると、Prop drilling問題と呼ばれる、Stateのバケツリレーが親コンポーネントから子孫コンポーネントの間で発生します。これは、コンポーネントの階層が深くなってくると表出する問題で、コンポーネント間でStateを渡すためのデータ受け渡しのインターフェースを余計に作らなければいけないコストが発生します。

Reactのコンポーネント内のみで、Stateを管理するのに比べ、Reduxを導入するとアプリケーション内で、Globalに一つ用意されたStoreでStateを管理することができます。そしてStore内のStateを更新したい場合、決められた手続きを踏めば、どのコンポーネントからでも、Stateを変更できることが出来るようになります。また、モジュールの疎結合も実現出来るようになるという点が、Reduxを導入する理由になるかと思います。

Reduxに影響を与えたFluxアーキテクチャとは

FluxPattern 参照元:Flux In-Depth Overview

Fluxは単方向データフローを実現するためのフロントエンドのアーキテクチャです。 登場する用語として以下が挙げられます。

Store アプリケーション全体で参照したいデータの保管場所

Action イベントにおける『対象のデータをどうしたいか』を表現したもの

Dispatcher Actionの種類を判別し、紐づいたStoreの更新処理を行うもの

ざっくりですが、以下のようなフローで単方向データフローの中で値の更新処理を行ます。

① 『対象のデータをどうしたいか』という意図を表現したオブジェクト(Action)を、Dispatcherに対して、発行する。

② DispatcherがActionの種類を判別し、それに対応する手順に従って Storeの値の状態を変更する。

③ Storeが書き換えられて、View(コンポーネント)に反映される。

処理の流れは上記図の通りです。Fluxは、__単方向データフローを徹底__することで、破綻しにくく予測可能な状態でアプリケーション開発が進められることを目指したものです。

Reduxで用意するもの

Store
値を保管しておくものです。ここに保管される値は「ステート」と呼ばれます。

Provider
ストアを他のコンポーネントに受け渡すための仕組みです。

Reducer
ストアに保管される「ステート(値)」を変更するための仕組みです。

ReducerはFluxでなく、Reduxで登場するモジュール です。
FluxのStoreの中に保管されているStateの変更をする責務を切り出し、DispatcherとStoreの相互依存を解決し、従来のFluxパターンをよりシンプルに書けるようにしています。 Reducerで呼び出す処理は常に、新しいステート(値)を最後に返すことを覚えておきましょう。

具体的な手順

① Actionの作成

Actionはどんなイベントが起こったかを表現するプレーンなオブジェクトです。

export const increment = () => {
  return {
    type: "INCREMENT"
  }
}

export const decrement = () => {
  return {
    type: "DECREMENT"
  }
}

② Reducerの作成

Reducerは副作用のない純粋関数です。
PrevState(値の前の状態)とActionを引数にとって、新たなStateを返します。

const reducer = (state, action) => {
  ///何らかの処理
}

__第一引数のstate__には、Storeに保管するState(値)を指定します。これは整数やテキスト、複数の値を扱いたい場合はオブジェクトを用意します。

__第二引数のAction__は、Reducerを実行する際に渡される値です。これはReducerが実行される時の内容をまとめたプレーンなオブジェクトです。
Reducer内では、要件によって、さまざまな処理を記述することになるのですが、この引数のアクションを使って、どのような処理を行うかを定義していきます。(簡単な例だと、increment,decrement,resetなど...)

必ず用意されるのは、「type」というプロパティで、これは、呼び出し時のactionの種類を示す値になっています。レデューサーは、typeを使い、「このtypeの時は、この処理。このtypeはあの処理。。。」と処理を振り分けていきます。

//Reducer
const counter_reducer = (state = state_value, action) => {
  switch(action.type) {
    case 'INCREMENT':
      return{
        counter: state.counter + 1,
        message: 'INCREMENT'
      }
    case 'DECREMENT':
      return {
        counter: state.counter - 1,
        message: 'DECREMENT'
      }
    default:
      return state;
  }
}

そして、新たにStoreに保管するState(値)を用意して、それをreturnすると、StoreのなかのState(値)が、更新されます。

reducer関数の決まりとして、以下の3点は覚えておきましょう。

1.引数はstateとactionの二つ
2.actionは、typeでstateをどう変更させるかを指定する
3.戻り値は、変更後のstate

③ ストアを作成する

Reducerが用意できたら、Reduxの核となるStoreを作成します。 改めて書くと、ストアは一つのアプリケーションにつき、一つのみです。

const store = createStore(reducer, initialState);

④ Providerを使って、storeをpropsとして渡す

そして初期化したStoreを最上位のコンポーネントでProviderを使って、propsとして渡す必要があります。これによって、初めて子コンポーネントの中で、Reduxの提供する機能が使えるようになります。これで複数の子コンポーネントからも、Storeの値にアクセスできるようになります。これはお約束事です。

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import { reducer, initialState } from 'reducer';
import App from './App';

const store = createStore(reducer, initialState);

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

上記を用意して初めて、Reduxを利用する準備が整います。
コンポーネントに、Actionをインポートして、viewから発行したactionはdispatcherによって、Reducerに渡されます。

function App() {

  const count = useSelector(state => state.count);
  const dispatch = useDispatch()

  return(
    <>
    <div>{count}</div>
    <button onClick={()=> dispatch(increment())}>click</button>
    <button onClick={()=> dispatch(decrement())}>click</button>
    </>
  )
}

Reduxを導入すると多くのファイルを作成する事になります。実際には、『Duck』や『re-ducsk』パターンを取り入れながら開発が進むことになると思います。

簡単にReduxのディレクトリ構成の例も書いてみたので、興味があったら参照してみて下さい。
Redux導入時のディレクトリ構成例