JavaScript
入門
redux

Redux 入門 〜Reduxの基礎を理解する〜

はじめに

今更ですが、Reduxに関する備忘録になります。

Reduxの基本的な考え方や公式のサンプルコードを読み解いていき、基礎を理解することを目的とした記事です。

そのため、公式ドキュメントで基礎を理解できるのであればこの記事を読む必要はありません。

Reduxの入門記事ですので、Reactは出てこないです。

Reduxとは

state(状態)を容易に管理をするためのフレームワークのこと。Reactと相性が良いため、セットで紹介されていることが多いが、Redux自体は独立したものなので単体で利用することも可能。

stateとは?

以下のようなViewに表示されているデータやUIの状態などのアプリケーションが保持している情報のことを指す。

state.png

そのため、「stateの変更」はViewに表示されているデータやUIの状態などが変更されることを指す。

state2.png

なぜReduxを利用するのか

  • stateを管理し易くなるため
  • テストが容易になるため
  • stateの変更を遡れる拡張(Redux DevTool)など、便利なツールを使った開発が可能なため

stateの管理が複雑な大規模アプリケーションなどので利用すると便利。

小規模なアプリケーションやちょっとしたサービスを作る程度なら必要ない。

Reduxの基礎

Reduxの主な要素(機能)

  • Action
  • Reducer
  • Store

Action

アクション(何が起きたのか)とそれに付随する情報を持つオブジェクト。

ActionをStoreへdispatch(送信)すると、Storeのstateが変更される。stateの変更は必ずActionを経由して行う(理由は後述)。

以下はActionの例。ADD_TODOというタイプのアクションとそれに付随する情報であるtext: 'Go to swimming pool'を持つ。

const action = {
  type: 'ADD_TODO',
  text: 'Go to swimming pool'
};

アクションのタイプを示すtypeプロパティは必須。

Actionはstateの変更に必要だが、イベントとそれに付随する情報しか持たないため、stateがどのような変更をされるのかは知らない

Reducer

Storeから受け取ったActionとstateに応じて、変更されたstateを返す純粋関数(同じ引数を渡されたら必ず同じ結果を返す関数)。

以下はReducerの例。ADD_TODOというタイプのアクションがdispatchされた時に、stateを変更して返している。

function todos(state = [], action) {
  switch (action.type) {
    case 'ADD_TODO':
      return state.concat([{ text: action.text, completed: false }]);

    default:
      return state;
  }
}

Store

アプリケーションの全てのstateを保持するオブジェクト。

ActionをStoreにdispatchする手段(store.dispatch())を提供する。また、stateとdispatchされたActionを、指定したReducerに渡してstateを変更する。

以下はStoreの例。Reducerを指定し、アクションをdispatchしている。

// Action
const action = {
  type: 'ADD_TODO',
  text: 'Go to swimming pool'
};

// Reducer
function todos(state = [], action) {
  switch (action.type) {
    case 'ADD_TODO':
      return state.concat([{ text: action.text, completed: false }]);

    default:
      return state;
  }
}

// Store
// createStore()の第1引数に渡したReducerがActionをdispatchする度に実行される
// 今回はtodosを渡しているため
// Actionをdispatchする度にtodosが実行され、Actionに応じたstateを返す
const store = Redux.createStore(todos);

// Actionをdispatchする
// Reducerであるtodosが実行され、Storeが保持しているstateが変更される。
store.dispatch(action);

// stateを取得する
console.log(store.getState()); // =>  [{text: "Go to swimming pool", completed: false}]

Reduxの三原則(守るべきルール)

  • Single source of truth(信頼できる唯一の情報源)
  • State in read-only(stateは読み取り専用にする)
  • Changes are made with pure functions(変更はすべて純粋関数で行われる)

Single source of truth(信頼できる唯一の情報源)

アプリケーションの全てのstateを単一のStoreで保持する。

以下のようにStoreを複数生成してはいけない。

const store = Redux.createStore(todos);
const store2 = Redux.createStore(todos);

State in read-only(stateは読み取り専用にする)

stateの変更は必ずActionを経由して行う。それ以外の手段で変更をしてはいけない。

何故なら、Actionを経由すればどこでstateの変更が起こっているか、どのような変更の種類があるのかを明確に理解できるから

stateの変更をどこでもできるようにしてしまうと、意図しない箇所でstateが変更されてしまったり、バグの特定が困難になる。

Changes are made with pure functions(変更はすべて純粋関数で行われる)

stateがどのActionによってどのように変更されるかは、Reducerで定義する。つまり変更は全てReducerで行われる。

定義するReducerは純粋関数(同じ引数を渡されたら必ず同じ結果を返す関数)でなくてはならない。そのため、Reducer内で以下を決して実行してはいけない。

  • 引数を変更する
  • API呼び出しなどを実行する
  • 純粋関数ではない関数(Date.now()Math.random()など)を呼び出す

Reduxを利用したサンプル

上記を理解した上で公式のサンプルを読み解いていく。

デモ

counter.gif

シンプルなカウンター。以下の機能を持つ。

  • カウントを加算(「+」ボタン)
  • カウントを減算(「-」ボタン)
  • カウントが奇数の場合、カウントを加算(「Increment if odd」ボタン)
  • 1秒後にカウントを加算(「Increment async」ボタン)

コードは以下の通り。

<!DOCTYPE html>
<html>
<head>
<title>Redux basic example</title>
<!-- Reduxの読み込み -->
<script src="https://unpkg.com/redux@latest/dist/redux.min.js"></script>
</head>
<body>
  <div>
    <p>
      Clicked: <span id="value">0</span> times
      <button id="increment">+</button>
      <button id="decrement">-</button>
      <button id="incrementIfOdd">Increment if odd</button>
      <button id="incrementAsync">Increment async</button>
    </p>
  </div>
  <script>
    // Reducer
    function counter(state, action) {
      if (typeof state === 'undefined') {
        return 0;
      }

      // Actionに応じたstateを返す
      switch (action.type) {
        case 'INCREMENT':
          return state + 1;
        case 'DECREMENT':
          return state - 1;
        default:
          return state
      }
    }

    // Storeを生成する、第1引数にReducerを渡す
    // 今回、Reducerとしてcounterを渡しているため
    // Actionをdispatchする度に、counterが実行され、Actionに応じたstateを返す
    const store = Redux.createStore(counter);
    const valueEl = document.getElementById('value');

    // state(今回はカウント)を描画する関数
    function render() {
      valueEl.innerHTML = store.getState().toString();
    }

    render();

    // change listener(Actionがdispatchされる度に呼ばれる関数)を追加する
    // そのため、stateの変更がある度にrenderが呼ばれる
    store.subscribe(render);

    document.getElementById('increment')
      .addEventListener('click', () => {
        // Action({ type: 'INCREMENT' })をStoreにdispatch
        store.dispatch({ type: 'INCREMENT' })
      });
    document.getElementById('decrement')
      .addEventListener('click', () => {
        // Action({ type: 'DECREMENT' })をStoreにdispatch
        store.dispatch({ type: 'DECREMENT' })
      });
    document.getElementById('incrementIfOdd')
      .addEventListener('click', () => {
        if (store.getState() % 2 !== 0) {
          // Action({ type: 'INCREMENT' })をStoreにdispatch
          store.dispatch({ type: 'INCREMENT' })
        }
      });
    document.getElementById('incrementAsync')
      .addEventListener('click', () => {
        setTimeout(() => {
          // Action({ type: 'INCREMENT' })をStoreにdispatch
          store.dispatch({ type: 'INCREMENT' })
        }, 1000);
      });
  </script>
</body>
</html>

サンプルコードでやっていること

  • Reducerの定義
  • Storeを生成し、Actionをdispatchした時に実行されるReducerを指定する
  • change listener(Actionがdispatchされる度に呼ばれる関数)を追加する
  • 各ボタンをクリック時に、Actionをdispatchするイベントハンドラを追加

Reducerの定義

// Reducer
function counter(state, action) {
  if (typeof state === 'undefined') {
    return 0;
  }

  // Actionに応じたstateを返す
  switch (action.type) {
    case 'INCREMENT':
      return state + 1;
    case 'DECREMENT':
      return state - 1;
    default:
      return state
  }
}

Storeを生成し、Actionをdispatchした時に実行されるReducerを指定する

// Storeを生成する、第1引数にReducerを渡す
// 今回、Reducerとしてcounterを渡しているため
// Actionをdispatchする度に、counterが実行され、Actionに応じたstateを返す
const store = Redux.createStore(counter);

change listener(Actionがdispatchされる度に呼ばれる関数)を追加する

const valueEl = document.getElementById('value');

// state(今回はカウント)を描画する関数
function render() {
  valueEl.innerHTML = store.getState().toString();
}

render();

// change listener(Actionがdispatchされる度に呼ばれる関数)を追加する
// そのため、stateの変更がある度にrenderが呼ばれる
store.subscribe(render);

各ボタンをクリック時に、Actionをdispatchするイベントハンドラを追加

document.getElementById('increment')
  .addEventListener('click', () => {
    // Action({ type: 'INCREMENT' })をStoreにdispatch
    store.dispatch({ type: 'INCREMENT' })
  });
document.getElementById('decrement')
  .addEventListener('click', () => {
    // Action({ type: 'DECREMENT' })をStoreにdispatch
    store.dispatch({ type: 'DECREMENT' })
  });
document.getElementById('incrementIfOdd')
  .addEventListener('click', () => {
    if (store.getState() % 2 !== 0) {
      // Action({ type: 'INCREMENT' })をStoreにdispatch
      store.dispatch({ type: 'INCREMENT' })
    }
  });
document.getElementById('incrementAsync')
  .addEventListener('click', () => {
    setTimeout(() => {
      // Action({ type: 'INCREMENT' })をStoreにdispatch
      store.dispatch({ type: 'INCREMENT' })
    }, 1000);
  });

サンプルコードの処理の流れ

例えば「+」ボタン をクリック時、Viewに表示されている数字が0から1になる処理の流れは以下の通り。

  1. 「+」ボタン をクリック時、StoreにAction({ type: 'INCREMENT' })をdispatchする
  2. StoreがdispatchされたActionと現在のstateである0(初期値)をReducerに渡す
  3. Reducer(counter())が受け取ったActionとstateに応じて、新しいstateをStoreに返す(今回dipatchされたアクションのタイプはINCREMENTのため、0に1を加算した1を返す)
  4. StoreはReducerから返されたstateを保持する、この時点でStoreが保持するstateは0から1になった
  5. Actionがdispatchされた後、store.subscribe()で指定した関数が呼ばれる、今回はrender()が実行される
  6. render()でStoreから現在のstateである1を取得して描画する

図で表すと以下のイメージ(あくまで今回の処理の流れのイメージであり、Reduxのデータフロー図ではないため注意)

redux-flow (1) (3).png

終わり

Reduxは複雑はイメージを持たれがちですが、基礎自体はシンプルで理解しやすいかと思います。

今回記載していない高度な機能を利用したり、Reactと併用すると複雑になって学習が難しくなります。

とは言えども、複雑になってもRedux自体の基本的な考え方は変わらないので、まずはそこをしっかり理解することが大事だと思います。

基礎を理解したら、公式のサンプル集にReactと併用したものや、より複雑なサンプルがありますので、それらを見てみると良いと思います。