はじめに
- Redux Basics (1): Actions 翻訳
- Redux Basics (2): Reducers 翻訳
- Redux Basics (3): Store 翻訳
- Redux Basics (4): Data Flow 翻訳
- Redux Basics (5): Usage with React 翻訳
ReduxのBasicsの翻訳の続きです。今回はReducersの翻訳となります。翻訳で間違いがあると思いますがそこはご了承ください。
翻訳は下記ページの10/31時点のものとなります。
http://redux.js.org/docs/basics/Reducers.html
翻訳ここから
Reducers
Actionsでは何かが起こった事実を述べましたが、どのようにアプリケーションの状態が応答で変化するかを具体的に述べていません。これはReducerの仕事です。
状態のかたちを設計する
Reduxでは、すべてのアプリケーションの状態は一つのオブジェクトで保存されます。どんなコードを書く前にもその形を考えることは良いアイデアです。一つのオブジェクトとしてあなたのアプリの状態を最小に表現するのは何でしょうか?
Todoアプリのために、我々は2つの異なるものを保存したいです。
- 現在選択されたvisibility filter
- 実際のTodoのリスト
状態ツリーの中で、あるUIの状態と同じように、あるデータを保存する必要があることにしばしば気づきます。これはすばらしいことですが、UIの状態からデータを分割し続けることを試みましょう。
{
visibilityFilter: 'SHOW_ALL',
todos: [
{
text: 'Consider using Redux',
completed: true,
},
{
text: 'Keep all state in a single tree',
completed: false
}
]
}
関係の注記
多くの複雑なアプリでは、お互いの参照のために異なる実体を欲します。我々は、ネストすることなしに、できるかぎり標準化された状態を保ち続けるよう提案します。IDをkeyとして保存されたオブジェクトの全ての実体を保ち続け、他の実体やリストから参照するためにIDを使いましょう。アプリの状態をデータベースとして考えましょう。この手法は詳細にnormalizrのドキュメントのなかで説明されています。例えば、状態のなかのtodosById: { id -> todo }
とtodos: array<id>
を保ちつづけることは、現実のアプリでは良いアイデアであり、我々は例をシンプルに保ち続けます。
Actionを取り扱う
我々の状態オブジェクトが何のように見えるかを決定した今、我々はReducerを書く準備ができます。Reducerは前の状態をActionを取り、次の状態を返す単純な関数です。
(previousState, action) => newState
Array.prototype.reduce(reducer, ?initialValue)へ渡す関数の種類のためReducerを呼び出します。Reducerが単純であることは大変重要です。Reducerの内部で決してすべきではならないことはこちらです:
- 引数を変化させる
- APIを呼び出したりルーティング遷移のような副作用を行う
-
Date.new()
やMath.dandom()
のような単純でない関数を呼び出す
我々はadvanced walkthroughのなかでどのように副作用を実行するかを探索しています。今は、ただReducerは単純でなければならないことを覚えてください。同じ引数が与えられ、次の状態が計算され返されるべきです。驚きはないです。副作用はないです。API呼び出しはないです。変化はないです。ただの計算です。
これはおいておき、我々が早期に定義したActionを理解するために、徐々に教えることでReducerを書き始めましょう。
特定の初期状態で初められます。Reduxは最初にundefined(未定義)
な状態でReducerを呼び出します。これは我々のアプリの中で初期状態を返すチャンスです:
import { VisibilityFilters } from './actions'
const initialState = {
visibilityFilter: VisibilityFilters.SHOW_ALL,
todos: []
}
function todoApp(state, action) {
if (typeof state === 'undefined') {
return initialState
}
// For now, don't handle any actions
// and just return the state given to us.
return state
}
一つの巧妙なトリックは、より多くのコンパクトな方法でこれを書くために[ES6 default arguments syntax](ES6 default arguments syntax)を使うことです:
function todoApp(state = initialState, action) {
// For now, don't handle any actions
// and just return the state given to us.
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
}
}
注意点:
- **我々は状態を変化させません。我々はObject.assign()でコピーを作ります。
Object.assign(state, { visibilityFilter: action.filter })
もまた間違っています: 最初の引数が変化します。あなたは最初のパラメータとして空のオブジェクトを提供しなければなりません。**またかわりに{ ...state, ...newState }
と書くことで、object spread operatorの提案を有効にもできます。 - **我々は
default
の場合には以前のstate
を返します。**どのような不明のActionのためにも以前のstate
を返すことは重要です。
Object.assign
の注意
Object.assign()はES6の一部ですが、多くのブラウザにはまだ実装されていません。あなたはポリフィルであるBabel pluginか、_.assign()のような他のライブラリからのヘルパーを使う必要があります。
switch
と文例の注意
switch
文は現実の文例ではありません。Fluxの現実の文例は概念上のものです: 更新を発行する必要、DispatcherとともにStoreを登録する必要、オブジェクトとなる(そしてあなたがユニバーサルなアプリを欲するときに困難化する)Storeの必要があります。Reduxはこれらの問題をイベント発行の代わりに単純なReducerを使うことで解決します。ドキュメントのなかで
switch
文が使われているかどうかを基礎とするフレームワークを多くのものがまだ選択していることは不幸です。もしあなたがswitch
を好きでないならば、あなたは“reducing boilerplate”で示されているようにhandler mapを許可する慣習のcreateReducer
関数を使うことができます。
より多くのActionを取り扱う
我々は取扱いのために2つのActionを持ちます。ADD_TODO
を取り扱うために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
}
]
})
default:
return state
}
}
ただ前のように、我々は決してstate
やフィールドを直接書きませんし、代わりに新しいオブジェクトを返します。新しいtodos
は一つの新しいアイテムを最後に連結された古いtodos
と同じです。新しい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
})
})
我々は変更に頼ることなしに配列の特定のアイテムを更新したいので、我々はindexのアイテムの除き同じアイテムを持つ新しい配列を作らなければなりません。もしあなたが自身でこのような処理に気づいたら、それはreact-addons-updateやupdeepのようなヘルパーや、深い更新をネイティブでサポートする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
}
}
理解するのに簡単になる方法はないでしょうか?todos
とvisibilityFilter
は完全に独立して更新されるように見えます。時々状態フィールドがほかに依存していたりするのでより検討が要求されますが、我々の場合では分割された関数へ更新した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:
case TOGGLE_TODO:
return Object.assign({}, state, {
todos: todos(state.todos, action)
})
default:
return state
}
}
todos
はまたstate
を受け取ることに注意してください。しかしそれは配列です!今todoApp
はただ管理のために状態のいち部分を与えるのみで、todos
はどのようにそのただ一部分を更新するかを知っています。これはReducer構成とよばれ、Reduxアプリの構築の基本的なパターンです
もっとReducer構築を探索しましょう。我々はまたvisibilityFilter
のみをReducerの管理から取り除くことができるでしょうか?できます:
function visibilityFilter(state = SHOW_ALL, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return action.filter
default:
return state
}
}
今、我々は状態のReducer管理部分と呼ばれる関数としての主となるReduverを書き換えることができ、一つのオブジェクトへそれらを組み合わせることができます。またこれ以上完全な初期状態を知る必要もありません。最初に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で異なり、管理する状態の一部と比較します。
これはすでによく見えます!アプリが大きいとき、我々は分割されたファイルにReduerを分割でき、それらを完全に独立し異なるデータドメインで管理するよう保ち続けることができます。
最後に、Reduxは、上で現在しているtodoApp
である文例ロジックと同じように動作するcombineReducers()とよばれるユーティリティを提供します。この助けで、我々はこのように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)
}
}
あなたはまた異なるkeyをそれらに与え、異なるように関数を呼ぶでしょう。組み合わされたReducerを書くこれらの2つの方法は完全に同等です。
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()のすることは、それらのkeyによって選択された状態のいち部分とあなたのReducerを呼び出す関数を生成し、再び一つのオブジェクトへ結果を組み合わせることです。魔法ではありません。
ES6の知識を持ったユーザーへの注意
combineReducers
がオブジェクトと思うので、我々は分割したファイルに全てのトップレベルのReducerを置くことができ、export
でそれぞれのReducer関数、そしてkeyとしてそれらの名前とともにオブジェクトとしてそれらを得るためにimport * as reducers
を使います。import { combineReducers } from 'redux' import * as reducers from './reducers' const todoApp = combineReducers(reducers)
import *
はまだ新しい構文なので、我々は混乱を避けるためにドキュメントのなかでこれ以上それを使いませんが、あるコミュニティのサンプルではそれに遭遇するかもしれません。
ソースコード
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
次のステップ
次では、我々は、状態を持ちそしてあなたがアクションを送るときにReducerを呼び出すのを世話するReduxのStoreの作成をどのようにするかを探索します。
翻訳ここまで
おわりに
要約すると下記のとおりかと思います。
- Reducer
- Actionによって状態を変化させる
- 以前の状態とActionを受取り、新しい状態を返す
- 上記以外の複雑な処理は決してしてはならない
- 状態は前の状態をコピーし、それに手を加えて返す
- 定義されていないActionが渡されたときのことも考慮する
- Reducerを単純になるよう分割する