Reducer

Action は、何かが起きた ということを示します。 しかし起きたことに反応して、どのようにアプリケーションの状態を変化させるかは明示しません。それはReducerのやることです。

状態の形をデザインする

Reduxでは、すべてのアプリケーションの状態は1つのオブジェクトとして保持されます。 コードを書く前に、状態の形について考えるのは良いアイデアです。1つのオブジェクトとして、あなたのアプリケーションの状態をとても簡単に表現するにはどうすれば良いでしょう?

私たちのTodoアプリでは、2つの異なる情報を保持します:

  • いま選択されているフィルター
  • 実際のTodoリスト

状態ツリーで、何かのデータとUIの状態を保持したいことがよくあると思います。それは構いません。ただ、データとUIの状態は常に分けるようにしましょう。

{
  visibilityFilter: 'SHOW_ALL',
  todos: [
    {
      text: 'Consider using Redux',
      completed: true,
    },
    {
      text: 'Keep all state in a single tree',
      completed: false
    }
  ]
}
参照についての注意

より複雑なアプリでは、異なるエンティティが互いを参照したいことがあるでしょう。そんな時はいつも、まったくネストせず、できるだけ平準化することをお勧めします。一つのオブジェクトの中にあるすべてのエンティティは、キーとしてIDを持つのです。このIDを使って、他のエンティティやそのリストを参照します。アプリの状態を、データベースとして考えてください。この考え方は、 normalizr's のドキュメントで詳しく説明されています。 例えば実際のアプリでは、 状態の中に todosById: { id -> todo }(訳注:IDをキーとする、Todo項目のオブジェクト) と todos: array<id>(訳注:IDの配列) を持つ方が良いでしょう。しかしこのTodoアプリは使用例なので、シンプルなままにしておきましょう。

Actionを処理する

先ほど、状態オブジェクトがどのようなものかを決めました。これでReducerを書く準備ができました。Reducerは前の状態とActionを取り、次の状態を返す純粋関数です。

(previousState, action) => newState

なぜReducerと呼ばれるのでしょう?それは、Array.prototype.reduce(reducer, ?initialValue) に渡すタイプの関数だからです。純粋関数であることは、とても大切です。Reducerで、 絶対に してはいけないこと:

  • 引数に手を加える
  • 副作用を起こす。例)APIコールやページ遷移
  • 純粋ではない関数を呼び出す。 例)Date.now()Math.random()

上級チュートリアルで、副作用の扱い方を探ります。今のところは、Reducerが必ず純粋でなければならないとだけ覚えていてください。 引数が与えられると、次の状態を計算して返すのです。びっくりすることはありません。副作用もありません。API呼び出しもありません。変更もありません。ただ計算するだけです。

これを頭に入れて、Reducerを書き始めましょう。先に説明したAction を理解できるよう、ゆっくり教えていきます。

まず、初期状態を明示しましょう。Reduxは最初、undefined状態とともにReducerを呼び出します。このときが、初期状態を返すチャンスです:

import { VisibilityFilters } from './actions'

const initialState = {
  visibilityFilter: VisibilityFilters.SHOW_ALL,
  todos: []
}

function todoApp(state, action) {
  if (typeof state === 'undefined') {
    return initialState
  }

  // 今のところ, 何のアクションも処理していない。
  // ただ、与えられた状態を返しているだけ。
  return state
}

よりコンパクトに書くには、 ES6 default arguments syntax を使うことです:

function todoApp(state = initialState, action) {
  // 今のところ, 何のアクションも処理していない。
  // ただ、与えられた状態を返しているだけ。
  return state
}

それでは、SET_VISIBILITY_FILTERを処理しましょう。 必要なのは、状態のvisibilityFilterを変えるだけ。簡単です:

function todoApp(state = initialState, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return Object.assign({}, state, {
        visibilityFilter: action.filter
      })
    default:
      return state
  }
}

注意事項:

  1. stateは書き換えていません。 Object.assign()でコピーを作っています。 Object.assign(state, { visibilityFilter: action.filter })も間違いです : これは最初の引数に手を加えています。最初の引数として、必ず 空のオブジェクトを渡してください。代わりに object spread operator proposal{ ...state, ...newState } と書くこともできます。

  2. default(既定) ケースとして、前のstate(状態)を返します すべての不明なActionには、前のstateを返すのが重要です。

Object.assignについての注意

Object.assign() はES6の一部です。古いブラウザでは、まだ対応していません。対応するためにはポリフィルか、Babel プラグイン、 または_.assign()のような別のライブラリのヘルパーが必要です。

switch と常用文についての注意

switch文は、本当の常用文ではありません。 Fluxの本当の常用文は、概念的です: 更新を発行する必要があるし、 DispatcherとともにStoreを登録する必要があるし、Storeは1つのオブジェクトにする必要があります(ユニバーサルアプリにしたいなら、複雑になります)。 Reduxはイベントを発行する代わりに純粋なReducerを使うことで、これらの問題を解決します。

多くの人がまだ、ドキュメントにswitch文が載っているかどうかでフレームワークを選んでいるのは残念です。もしswitchが好きでなければ、アクションの処理を対応づける(マッピングする)ために、 特別なcreateReducer関数を使うこともできます。この関数は “常用文の削減”で説明しています。

もっとActionを処理する

処理しなければいけない、さらに2つのActionがあります! SET_VISIBILITY_FILTERでやったように、ADD_TODOTOGGLE_TODOActionをインポートします。そしてADD_TODOを処理するためにReducerを拡張します。

import {
  ADD_TODO,
  TOGGLE_TODO,
  SET_VISIBILITY_FILTER,
  VisibilityFilters
} from './actions'

...

function todoApp(state = initialState, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return Object.assign({}, state, {
        visibilityFilter: action.filter
      })
    case ADD_TODO:
      return Object.assign({}, state, {
        todos: [
          ...state.todos,
          {
            text: action.text,
            completed: false
          }
        ]
      })
    default:
      return state
  }
}

先ほどと同じように、stateとその中身を直接書き換えてはいけません。代わりに新しいオブジェクトを返します。 新しいtodosは、古いtodosの最後に1つTodo項目を付け加えたのと同じです。 新たなTodo項目はActionのデータから作られます。

最後に、TOGGLE_TODOハンドラを実装します。何も驚くようなことはありません:

case TOGGLE_TODO:
  return Object.assign({}, state, {
    todos: state.todos.map((todo, index) => {
      if (index === action.index) {
        return Object.assign({}, todo, {
          completed: !todo.completed
        })
      }
      return todo
    })
  })

変更のためにソートし直すことなく、配列にある特定の項目を更新したいのです。そのためインデックスで示された項目以外は同じになる、新しい配列を作らないといけません。もしこのような処理を何度も書いていることに気づいたら、immutability-helperupdeepのようなヘルパーを使うと良いでしょう。またはImmutableのように、ネストされた更新にもともと対応しているライブラリもあります。とにかくstateの中に何かを割り当てるときは、まず最初に複製することを忘れないでください。

Reducerを分割する

ここまでのコードです。ごちゃごちゃしています:

function todoApp(state = initialState, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return Object.assign({}, state, {
        visibilityFilter: action.filter
      })
    case ADD_TODO:
      return Object.assign({}, state, {
        todos: [
          ...state.todos,
          {
            text: action.text,
            completed: false
          }
        ]
      })
    case TOGGLE_TODO:
      return Object.assign({}, state, {
        todos: state.todos.map((todo, index) => {
          if (index === action.index) {
            return Object.assign({}, todo, {
              completed: !todo.completed
            })
          }
          return todo
        })
      })
    default:
      return state
  }
}

もっと分かりやすくする方法はないでしょうか? todosvisibilityFilterは完全に独立して更新されているように見えます。状態のそれぞれが互いに依存しているような場合は、もっと考慮が必要です。 しかしこの例では、todosを簡単に別の関数に分割して更新できそうです:

function todos(state = [], action) {
  switch (action.type) {
    case ADD_TODO:
      return [
        ...state,
        {
          text: action.text,
          completed: false
        }
      ]
    case TOGGLE_TODO:
      return state.map((todo, index) => {
        if (index === action.index) {
          return Object.assign({}, todo, {
            completed: !todo.completed
          })
        }
        return todo
      })
    default:
      return state
  }
}

function todoApp(state = initialState, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return Object.assign({}, state, {
        visibilityFilter: action.filter
      })
    case ADD_TODO:
      return Object.assign({}, state, {
        todos: todos(state.todos, action)
      })
    case TOGGLE_TODO:
      return Object.assign({}, state, {
        todos: todos(state.todos, action)
      })
    default:
      return state
  }
}

気をつけてほしいのは、todosstateを受け取りますが、配列だということです!todoAppは状態の一部だけ渡します。そしてtodosは渡された状態についてのみ、どのように更新すべきか把握しています。これは Reducer合成 と呼ばれ、Reduxアプリを作る基本パターンです。

Reducer合成をもっと見てみましょう。ReducerからvisibilityFilterも切り出せないでしょうか?もちろんできます。

下記のインポートでは、SHOW_ALLを宣言するためにES6 Object Destructuringを使いましょう:

const { SHOW_ALL } = VisibilityFilters

そして:

function visibilityFilter(state = SHOW_ALL, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return action.filter
    default:
      return state
  }
}

これでルート(大元の)Reducerを書き換えることができます。このReducerは、状態の一部を処理するReducerを呼び出し、1つのオブジェクトとして合成する関数です。もう初期状態の全体を把握する必要はありません。ただ最初にundefinedが与えられると、配下のReducerがそれぞれの初期状態を返すことが分かっていれば良いのです。

function todos(state = [], action) {
  switch (action.type) {
    case ADD_TODO:
      return [
        ...state,
        {
          text: action.text,
          completed: false
        }
      ]
    case TOGGLE_TODO:
      return state.map((todo, index) => {
        if (index === action.index) {
          return Object.assign({}, todo, {
            completed: !todo.completed
          })
        }
        return todo
      })
    default:
      return state
  }
}

function visibilityFilter(state = SHOW_ALL, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return action.filter
    default:
      return state
  }
}

function todoApp(state = {}, action) {
  return {
    visibilityFilter: visibilityFilter(state.visibilityFilter, action),
    todos: todos(state.todos, action)
  }
}

それぞれのReducerは、グローバルの状態のうち自身が担当する部分だけを処理します。そのためstate引数はすべてのReducerで異なり、処理する状態だけが渡されます。

これで良くなりました!アプリが大きくなったら、Reducerを別のファイルに分割すると良いでしょう。ファイルを分けることで、異なるデータ領域の処理について完全な独立性を保てます。

最後に、ReduxはcombineReducers()という便利な関数呼び出しを用意しています。これは上記のtodoAppでやったのと同じ常用的なロジックです。この関数により、todoAppを下記のように書き換えられます:

import { combineReducers } from 'redux'

const todoApp = combineReducers({
  visibilityFilter,
  todos
})

export default todoApp

これは下記と同じです:

export default function todoApp(state = {}, action) {
  return {
    visibilityFilter: visibilityFilter(state.visibilityFilter, action),
    todos: todos(state.todos, action)
  }
}

異なるキーを渡したり、異なる関数を呼び出すこともできます。下記2つの合成したReducerは同等です:

const reducer = combineReducers({
  a: doSomethingWithA,
  b: processB,
  c: c
})
function reducer(state = {}, action) {
  return {
    a: doSomethingWithA(state.a, action),
    b: processB(state.b, action),
    c: c(state.c, action)
  }
}

combineReducers()がやるのは、複数のReducerを呼び出す関数の生成です。呼び出されたReducerには、キーによって対応づけられた状態の一部が渡されます。 そしてcombineReducers()はそれぞれのReducerが返した結果を1つのオブジェクトにまとめ直します。It's not magic.(これは魔法ではありません。)引数として渡されたすべてのReducerが状態を変えなければ、新しいオブジェクトは作られません。これは他のReducerと同じです。

ES6に精通したユーザーへの注意

combineReducersは一つのオブジェクトを待ち構えています。そのため最上位にある複数のReducerを別々のファイルに入れてexportできます。 こうするとimport * as reducersで、それぞれのReducerの関数名をキーにした一つのオブジェクトが得られます:

import { combineReducers } from 'redux'
import * as reducers from './reducers'

const todoApp = combineReducers(reducers)

import *はまだ新しい構文なので、このドキュメントではconfusion(混乱)を避けるために他では使いません。しかしどこかのコミュニティで、例として出くわすかもしれません。

ソースコード

reducers.js

import { combineReducers } from 'redux'
import {
  ADD_TODO,
  TOGGLE_TODO,
  SET_VISIBILITY_FILTER,
  VisibilityFilters
} from './actions'
const { SHOW_ALL } = VisibilityFilters

function visibilityFilter(state = SHOW_ALL, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return action.filter
    default:
      return state
  }
}

function todos(state = [], action) {
  switch (action.type) {
    case ADD_TODO:
      return [
        ...state,
        {
          text: action.text,
          completed: false
        }
      ]
    case TOGGLE_TODO:
      return state.map((todo, index) => {
        if (index === action.index) {
          return Object.assign({}, todo, {
            completed: !todo.completed
          })
        }
        return todo
      })
    default:
      return state
  }
}

const todoApp = combineReducers({
  visibilityFilter,
  todos
})

export default todoApp

次のステップ

次に、ReduxのStoreをつくる 方法を学びましょう。Storeは状態を保持します。そしてActionがDispatch(送信)されると、Reducerの呼び出しを処理します。

results matching ""

    powered by

    No results matching ""