3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Redux Basics (2): Reducers 翻訳

Last updated at Posted at 2016-10-31

はじめに

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
  }
}

注意点:

  1. **我々は状態を変化させません。我々はObject.assign()でコピーを作ります。Object.assign(state, { visibilityFilter: action.filter })もまた間違っています: 最初の引数が変化します。あなたは最初のパラメータとして空のオブジェクトを提供しなければなりません。**またかわりに{ ...state, ...newState }と書くことで、object spread operatorの提案を有効にもできます。
  2. **我々は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-updateupdeepのようなヘルパーや、深い更新をネイティブでサポートする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:
    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を単純になるよう分割する

次は、Redux Basics (3): Store 翻訳です。

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?