🕌

Reduxの仕組みを理解する

2021/05/07に公開

こんにちは、webディベロッパーの羽田です。
フロントをコーディングする際にReduxで状態管理をすることが多いのですが、コア部分の処理を理解しきれていないと感じていたので記事にまとめます。

はじめに

当記事はReduxについてまとめていますが、仕組みを理解することにフォーカスしていますので実装については触れません。実装方法に関しては公式のドキュメントが豊富でわかりやすいです。
Redux - A predictable state container for JavaScript apps.

Reduxとは

Redux(リダックス)とは2015年にDan Abramov氏とAndrew Clark氏によって作成された状態管理ライブラリ。後述するFluxアーキテクチャの影響を受けており、フロントエンドでのデータ管理を容易かつ堅牢なものにしてくれます。

Redux自体は純粋なJSで書かれているため、Vanila JSやjQueryで使えるのはもちろん、React専用のAPIが用意された『react-redux』も存在し、幅広いフレームワーク上で動作します。

Fluxとは

FaceBook社が提唱している、アプリケーション上でのデータフロー管理のためのアーキテクチャパターン。

View, Action, Dispatcher, Storeの4つの要素で構成され、単方向へデータが流れていくことでデータの流れを追いやすくなるという特徴があります。

Reduxの何がいいのか

求められる要件が複雑であればある程、Webアプリケーションの状態管理も難しくなってきます。

例えば小中規模のWebサイトなら比較的少ないstate管理で済みますが、多くの機能を想定したWebアプリケーションなら数多くのStateを抱えることになり、管理が大変かつ煩雑になりがちです。

そこでReduxを実装することでフロントエンド上でのstate管理を簡素なものにし、堅牢故にバグも発生しにくくなるなど多くのメリットをもたらしてくれます。

Reduxのコアコンセプト

Reduxはその複雑化していくアプリケーションを安全に管理するための三大原則を説明しています。
Core Concepts | Redux

一つ目は信頼できる唯一の情報源であること。
Reduxではアプリケーションの状態をstateと呼ばれるオブジェクトツリーとして管理し、Store内に格納します。stateは宣言的であり、可視化されるのでどのようなデータが存在し、どのように使用されているか容易に把握できます。

state = {
  menuState: true,
  modalState: false,
  count: 0
}

二つ目は状態はRead Only(読み取り専用) であること。
基本的にstateに直接アクセスして値を書き換えることは許可されておらず、変更するにはActionと呼ばれるオブジェクトを発行する必要があります。これにより値の変更方法は一元化され、いたずらに不具合が起きる可能性が少なくなります。

Action = {
  type: 'ACTION_TYPE_NAME'
}

最後に状態の変更は純粋関数で行われることです。
stateをどのように変更するかはReducerという純粋関数(値の入出力の過程で一切の変化を与えない値不変の関数)で指定します。Reducerに引き渡されたActionとStore内に保持しているstateを使用し、新しいstateを生成します。

reducer = function(state, action){
  switch (action.type) {
    case 'ACTION_TYPE_NAME':
      return {
          ...state,
          value: 'CHANGE_VALUE'
      }
    default:
      return state;
    }
}

データフローを理解する

Reduxに登場する主な要素は以下です。

  • Store(stateを保存しておく要素、内部にReducerを保持する)
  • Reducer(Actionを受け取り、既存のstateとActionを利用して新たなstateを作成する要素)
  • Dispatch(ViewなどからActionを受け取り、Store(正しくは内部のReducer)へ送信する要素)
  • Action(変更内容を内部に含む要素であり、Dispatchによって送信される。基本的にオブジェクト形式)
  • state(Store内で保持される要素。アプリケーションの状態を模したオブジェクトとして扱われる)

それぞれの要素が役割を持ち、単方向のデータフローを形成しています。

まずViewやAPIなどから変更内容を記したActionが発行され、Dispatchに引き渡されます。(ActionはReducerの処理に使用するtypeプロパティを保持する必要があります。)

//dispatchメソッドの引数にActionを渡してStoreへ送信
//dispatchはstoreオブジェクトのメソッドとして提供されます

store.dispatch({type: 'ACTION_TYPE_NAME'});

Actionを受け取ったStoreは内部のReducerにActionを引き渡し、Actionオブジェクトのtypeプロパティによって変更する値を決定します。

const reducer = function(state, action){
  switch (action.type) {
    case 'ACTION_TYPE_NAME':
      return {
          ...state,
          value: 'CHANGE_VALUE'
      }
    default:
      return state;
    }
}

// 常に最新のstateを取得する例
// stateを変更するとsubscribe()の引数に指定されたコールバック関数が実行される
let appState = store.getState();
store.subscribe(() => {appState = store.getState()});

そうして変更されたstateはstore.getState()によって取得することができ、storeで変更が起きた際に通知を行うstore.subscribe()を併用することで常に最新のstateを使用することができます。

createStore関数を覗いてみる

Reduxの処理のコア部分のほとんどがcreateStore関数に格納されています。 我々がReduxを使用する際にはこのcreateStore関数の引数にreducer、初期state、ミドルウェアを指定(reducer以外は省略可能)してStoreを生成するのですが、中身を覗くことで理解が深まると思うので実際にコードを処理ブロックごとに切り分けながら見ていきます。

引数チェックと変数への代入

ここでは引数に指定された値が処理に適した型であるかのチェックや省略された引数の順序の入れ替え、変数への代入を行っています。

引数のうち、reducerとpreloadedStateはそのまま変数に代入されますが、enhancerに関数を指定するとcreateStoreをラップした関数が返却され、そのまま実行されます。

この際、enhancerに指定される関数は戻り値が関数である必要があるのですが、使用する際は大抵Reduxライブラリ内部のapplyMiddleWare()を使いますので自分で関数を書くことはあまりないかと思います。

function createStore(reducer, preloadedState, enhancer) {
    var _ref2;
  
    if (typeof preloadedState === 'function' && typeof enhancer === 'function' || typeof enhancer === 'function' && typeof arguments[3] === 'function') {
      throw new Error('It looks like you are passing several store enhancers to ' + 'createStore(). This is not supported. Instead, compose them ' + 'together to a single function.');
    }
  
    if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
      enhancer = preloadedState;
      preloadedState = undefined;
    }
  
    if (typeof enhancer !== 'undefined') {
      if (typeof enhancer !== 'function') {
        throw new Error('Expected the enhancer to be a function.');
      }
  
      return enhancer(createStore)(reducer, preloadedState);
    }
  
    if (typeof reducer !== 'function') {
      throw new Error('Expected the reducer to be a function.');
    }
  
    var currentReducer = reducer;
    var currentState = preloadedState;
    var currentListeners = [];
    var nextListeners = currentListeners;
    var isDispatching = false;

getState関数

getState関数によってstore内部のstateを取得することが可能です。currentState変数に格納された値をそのまま返却しているだけの単純な処理です。

function getState() {
      if (isDispatching) {
        throw new Error('You may not call store.getState() while the reducer is executing. ' + 'The reducer has already received the state as an argument. ' + 'Pass it down from the top reducer instead of reading it from the store.');
      }
  
      return currentState;
    }

subscribe関数

subscribe関数を使用することによってstateが変更された際に、引数に指定したlistener(関数)を実行することが可能です。

ensureCanMutateNextListeners()によってnextListener変数にcurrentListener変数のシャローコピーが代入され、引数に指定されたlistenerをnextListener変数に代入しています。

そうすることで保持したlistenerをunsubscribeで削除することが可能です。

function ensureCanMutateNextListeners() {
      if (nextListeners === currentListeners) {
        nextListeners = currentListeners.slice();
      }
    }

function subscribe(listener) {
      if (typeof listener !== 'function') {
        throw new Error('Expected the listener to be a function.');
      }
  
      if (isDispatching) {
        throw new Error('You may not call store.subscribe() while the reducer is executing. ' + 'If you would like to be notified after the store has been updated, subscribe from a ' + 'component and invoke store.getState() in the callback to access the latest state. ' + 'See https://redux.js.org/api-reference/store#subscribelistener for more details.');
      }
  
      var isSubscribed = true;
      ensureCanMutateNextListeners();
      nextListeners.push(listener);
      return function unsubscribe() {
        if (!isSubscribed) {
          return;
        }
  
        if (isDispatching) {
          throw new Error('You may not unsubscribe from a store listener while the reducer is executing. ' + 'See https://redux.js.org/api-reference/store#subscribelistener for more details.');
        }
  
        isSubscribed = false;
        ensureCanMutateNextListeners();
        var index = nextListeners.indexOf(listener);
        nextListeners.splice(index, 1);
        currentListeners = null;
      };
    }

dispatch関数

dispatch関数はstateを変更できる唯一の関数です。actionを引数にとり、currentReducer変数にcurrentStateとactionを渡して新しいstateを生成します。

また、store.subscribe()でlistenerが登録されている場合はlistenerを実行します。

function dispatch(action) {
      if (!isPlainObject(action)) {
        throw new Error('Actions must be plain objects. ' + 'Use custom middleware for async actions.');
      }
  
      if (typeof action.type === 'undefined') {
        throw new Error('Actions may not have an undefined "type" property. ' + 'Have you misspelled a constant?');
      }
  
      if (isDispatching) {
        throw new Error('Reducers may not dispatch actions.');
      }
  
      try {
        isDispatching = true;
        currentState = currentReducer(currentState, action);
      } finally {
        isDispatching = false;
      }
  
      var listeners = currentListeners = nextListeners;
  
      for (var i = 0; i < listeners.length; i++) {
        var listener = listeners[i];
        listener();
      }
  
      return action;
    }

replaceReducer関数

こちらはとても単純な関数で、現在のreducerを引数に指定したreducerに置き換えるというものです。あまり使う機会はないかと思います。

if (typeof nextReducer !== 'function') {
	throw new Error('Expected the nextReducer to be a function.');
}
  
currentReducer = nextReducer;
  
dispatch({type: ActionTypes.REPLACE});

各関数をまとめたオブジェクトを返却

createStore()を実行して返却されるのがこちらのオブジェクトです。
このオブジェクトを使用してstore内の値を利用するわけですね。

return _ref2 = {
  dispatch: dispatch,
  subscribe: subscribe,
  getState: getState,
  replaceReducer: replaceReducer
};

まとめ

今回はReduxの仕組みを理解するということで、概念やコードの内容を分析していきました。Reduxはミドルウェアとして適用可能なライブラリ群やreact用に拡張したreact-reduxなど関連ライブラリが豊富です。これらも分解して中の処理を見てみるのも面白いかもしれません。

ご閲覧ありがとうございました〜。

それでは👋

Discussion