読者です 読者をやめる 読者になる 読者になる

Reduxまとめ(2)超訳

Actions

Actionsはアプリからstoreにデータを送る情報のペイロード。storeに対する唯一のソースだ。

新しいtodo itemを追加することを表すactionの例を示す。

const ADD_TODO = 'ADD_TODO'
{
    type: ADD_TODO,
    text: 'Build my first Redux app'
}

actionsはプレーンなJavaScriptオブジェクトであり、必ずtypeプロパティを持つ。typeは基本的に文字列の定数で定義される。アプリが大規模になれば、別のモジュールに入れて管理すると良い。

import { ADD_TODO, REMOVE_TODO } from '../actionTypes'

完了したtodoにチェックをつけるactionを追加する。配列にtodoを格納しているので、indexで特定のtodoを参照する。

{
    type: COMPLETE_TODO,
    index: 5
}

最後に現在表示されているtodoの変更に対するaction typeを追加する。

{
    type: SET_VISIBILITY_FILTER,
    filter: SHOW_COMPLETED
}

Action Creators

action creatorsは読んで字のごとくactionを作る関数である。

Fluxではこのようにaction creatorsはdispatchのトリガーを担う。

function addTodoWithDispatch(text) {
    const action = {
        type: ADD_TODO,
        text
    }
    dispatch(action)
}

対照的にReduxのaction creatorsはシンプルにactionを返す。

function addTodo(text) {
    return {
        type: ADD_TODO,
        text
    }
}

あるいは自動でdispatchを行うbound action creatorsを作る。

const boundAddTodo = (text) => dispatch(addTodo(text))
const boundCompleteTodo = (index) => dispatch(completeTodo(index))

そうすると、直接呼び出すことができる。

boundAddTodo(text)
boundCompleteTodo(index)

dispatch()関数はstoreからstore.dispatch()として直接アクセスできるが、react-reduxconnect()を使ってアクセスすることの方がありそうだ。たくさんのaction creatorsを自動的にまとめてdispatch()関数にまとめるためにbindActionCreators()を使うこともできる。

Source Code

actions.js

/*
 * action types
 */

export const ADD_TODO = 'ADD_TODO'
export const COMPLETE_TODO = 'COMPLETE_TODO'
export const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER'

/*
 * other constants
 */

export const VisibilityFilters = {
  SHOW_ALL: 'SHOW_ALL',
  SHOW_COMPLETED: 'SHOW_COMPLETED',
  SHOW_ACTIVE: 'SHOW_ACTIVE'
}

/*
 * action creators
 */

export function addTodo(text) {
  return { type: ADD_TODO, text }
}

export function completeTodo(index) {
  return { type: COMPLETE_TODO, index }
}

export function setVisibilityFilter(filter) {
  return { type: SET_VISIBILITY_FILTER, filter }
}

Reducers

Actionsは何かが起こった事実を描写するが、その反応としてどのようにアプリケーションのstateが変化するのかを特定しない。その仕事はreducersが担う。

Designing the State Shape

Reduxでは、stateは全てシングルオブジェクトに格納される。コードを書く前に、その形状を考えてみる。オブジェクトとしてのstateの最小の表現はなんだろうか。

todoアプリでは、次の2つを格納する。

  • The currently selected visibility filter
  • The actual list of todos
{
    VisibilityFilters: 'SHOW_ALL',
    todos: [
        {
            text: 'Consider using Redux',
            completed: true
        },
        {
            text: 'Keep all state in a single tree',
            completed: false
        }
    ]
}

Handling actionTypes

stateオブジェクトを決めた次は、reducerを書く。reducerは前のstateとactionを取り、次のstateを返す、ピュア関数。

(previousState, action) => newState

Array.prototype.reduce(reducer, ?initialValue)に渡す関数のタイプなのでreducerと呼ばれる。reducerがピュアであることはとても重要だ。そのためには、次のことをreducer内で行ってはならない。

  • 引数の変更
  • APIの呼び出しやルーティングの遷移といった副作用
  • ピュアでない関数の呼び出し(e.g.Date.nowあるいはMath.random())

副作用(side effect)について:advanced workthrough

reduxははじめにundefinedstateとともにreducerを呼び出す。ここでアプリの最初のstateを返すことができる。

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を使えば、もっとコンパクトに記述できる。

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を扱う。state上でvisibilityFilterを変更する。

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

注意すべきは次の2点。 1. stateを変更してはならない。 2. defaultcaseでは前のstateを返す。

Handling More Actions

もう2つ、扱うべきactionがある。reducerをADD_TODOを扱うようにしてみよう。

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
    }
}
case COMPLETE_TODO:
    return Object.assign({}, state, {
        todos: [
            ...state.todos.slice(0, action.index),
            Object.assign({}, state.todos[action.index], {
                completed:true
            }),
            ...state.todos.slice(action.index + 1)
        ]
    })
    ```
    配列を変化させることなく、特定のアイテムを更新したいので、アイテム前後をsliceする必要がある。この操作をよく使うなら、[react-addons-update](https://facebook.github.io/react/docs/update.html)や[updeep](https://github.com/substantial/updeep)、あるいは[Immutable](http://facebook.github.io/immutable-js/)といったライブラリを使うと良い。

    ### Splitting Reducers
    ```js
    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 COMPLETE_TODO:
      return Object.assign({}, state, {
        todos: [
          ...state.todos.slice(0, action.index),
          Object.assign({}, state.todos[action.index], {
            completed: true
          }),
          ...state.todos.slice(action.index + 1)
        ]
      })
    default:
      return state
  }
}

todosvisibilityFilterは完全に独立して更新されているように見える。stateは時に互いに依存し、複雑になるが、ここでは簡単に更新するtodosを分離した関数に分けることができる。

function todos(state = [], action) {
    switch (action.type) {
        case ADD_TODO:
            return [
                ...state,
                {
                    text: action.text,
                    completed: false
                }
            ]
        case COMPLETE_TODO:
            return [
                ...state.slice(0, action.index),
                Object.assign({}, state[action.index], {
                    completed: true
                }),
                ...state.slice(action.index + 1)
            ]
        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 COMPLETE_TODO:
            return Object.assign({}, state, {
                todos: todos(state.todos, action)
            })
        default:
            return state
    }
}

これはreducer compositionと呼ばれ、Reduxアプリのビルドの基礎パターンになる。

さらにreducer compositionを見ていこう。次のようにvisibilityFilterを管理するreducerを引き抜くことができる。

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

そうすると、メインのreducerをそれぞれのstateを管理するreducersを呼び出す関数として書き換え、それらを1つのオブジェクトにまとめることができる。全体のinitialStateeも知る必要はない。最初にundefinedを与えられた時、子のreducersがそれぞれのinitial stateを返すためだ。

function todos(state = [], action) {
    switch (action.type) {
        case ADD_TODO:
            return [
                ...state,
                {
                    text: action.text,
                    completed: false
                }
            ]
        case COMPLETE_TODO:
            return [
                ...state.slice(0, action.index),
                Object.assign({}, state[action.index], {
                    completed: true
                }),
                ...state.slice(action.index + 1)
            ]
        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)
    }
}

それぞれのreducersはグローバルなstateの自身の部分を管理している。stateパラメーターはreducerごとに異なり、管理するstateの部分に対応している。

これは既に管理しやすそうだ。アプリが大規模になれば、reducersを別ファイルに分けて、独立性を保ったまま異なるデータドメインを管理させることができる。

最後に、reduxはtodoApp が行っているボイラープレートのロジックを行うcombineReducers()を呼び出すユーティリティーを備えている。

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()はキーに対応して選択されたstateの部分を持つreducersを呼び出す関数を作り、再びシングルオブジェクトにそれらの結果をまとめ格納する。It's not magic

Source Code

reducers.js

import { combineReducers } from 'redux'
import { ADD_TODO, COMPLETE_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 COMPLETE_TODO:
      return [
        ...state.slice(0, action.index),
        Object.assign({}, state[action.index], {
          completed: true
        }),
        ...state.slice(action.index + 1)
      ]
    default:
      return state
  }
}

const todoApp = combineReducers({
  visibilityFilter,
  todos
})

export default todoApp

Store

前のセクションでは、”何が起こったのか”についての事実を表現するactionとそれらactionsに対応したstateを更新するreducersを定義した。

storeはこの2つを合わせるオブジェクトである。storeは次の義務を持つ。

  • アプリのstateを保持する
  • getState()でのstateへのアクセスを許可する
  • dispatch(action)でのstateの更新を許可する
  • subscribe(listener)でのlistenerの登録を行う

reduxアプリには1つしかstoreを持つことはできない。ロジックを扱うデータを分割したいなら、storeを複数持つのではなく、reducer compositionを使うようにする。

reducerがすでにあるなら、storeを作るのは簡単。前のセクションで幾つかのreducersを1つにまとめるためにcombineReducers()使った。ここでそれをimportし、createStore()に渡す。

import { createStore } from 'redux'
import todoApp from './reducers'
let store = createStore(todoApp)

オプションでcreateStore()の二番目の引数に最初のstateを渡すこともできる。

let store = createStore(todoApp, window.STATE_FROM_SERVER)

Dispatching Actions

UIがなくても、今のままでロジックをテストすることができます。

import { addTodo, completeTodo, setVisibilityFilter, VisibilityFilters } from './actions'

// Log the initial state
console.log(store.getState())

// Every time the state changes, log it
let unsubscribe = store.subscribe(() =>
    console.log(store.getState())
)

// Dispatch some actions
store.dispatch(addTodo('Learn about actions'))
store.dispatch(addTodo('Learn about reducers'))
store.dispatch(addTodo('Learn about store'))
store.dispatch(completeTodo(0))
store.dispatch(completeTodo(1))
store.dispatch(setVisibilityFilter(VisibilityFilters.SHOW_COMPLETED))

// Stop listening to state updates
unsubscribe()

storeにおいて、stateがどのように変更されるのかを確認することができる。

Source Code

index.js

import { createStore } from 'redux'
import todoApp from './reducers'

let store = createStore(todoApp)

Data Flow

Reduxアーキテクチャは、一方方向のみのデータフローを原則とする。

つまり、アプリケーションのすべてのデータは同じライフサイクルを持つので、アプリのロジックが理解しやすくなる。

データライフサイクルは次の4つである。

  1. store.dispatch(action)を呼び出す。

actionは何が起こったのかを説明する、プレーンなオブジェクト

{ type; 'LIKE_ARTICLE', articleId: 42 }
{ type: 'FETCH_USER_SUCCESS', response: { id: 3, name: 'Mary'} }
{ type: 'ADD_TODO', text: 'Read the Redux docs.' }

ニュースの断片を考えてみる。"Mary liked article 42."や、"Read the Redux docs."がtodosリストに追加された。

アプリ内のどこからでもstore.dispatch(action)を呼び出すことができる。

  1. Redux storeは与えたreducer関数を呼び出す。

storeはreducerに現在のstate treeとactionの2つの引数を渡す。例えば、todoアプリではルートに位置するreducerは次のようなものを受け取る。

// The current application state (list of todos and chosen filter)
let previousState = {
    visibleTodoFilter: 'SHOW_ALL',
    todos: [
        {
            text: 'Read the docs.',
            complete: false
        }
    ]
}

// The action being performed (adding a todo)
let action = {
    type: 'ADD_TODO',
    text: 'Understand the flow.'
}

// Your reducer returns the next application state
let nextState = todoApp(previousState, action)

reducerはピュア関数であることに注意する。次のstateのみを計算する。それは予測が可能であるべきだ。つまり、同じインプットで何回も呼び出しても、同じアウトプットを返すようにする。APIの呼び出しやルート遷移のような副作用を機能させるべきではない。それらはactionがdispatchされる前に行われるべきだ。

  1. ルートのreducerは複数のreducersのアウトプットを1つのstate treeにまとめることができる。

ルートreducerはストラクチャは開発者次第。combineReducers()を用いて、ルートreducerをstate treeの1つの枝を管理する関数に分割できる。

どのようにcombineReducers()が作用するのか。todosのリストと現在選択されているフィルターの設定のための2つのreducerがあるとする。

function todos(state = [], action) {
    // Somehow calculate it...
    return nextState
}

function visibleTodoFilter(state = 'SHOW_ALL', action) {
    // Somehow calculate it...
    return nextState
}

let todoApp = combineReducers({
    todos,
    visibleTodoFilter
})

combineReducersによって返されたtodoAppは2つのreducersを呼び出す。

let nextTodos = todos(state.todos, action)
let nextVisibleTodoFilter = visibilityFilter(state.visibilityFilter, aciton)

1つのstate treeに一連の結果をまとめることできる。

return {
    todos: nextTodos,
    visibleTodoFilter: nextVisibleTodoFilter
}
  1. storeはroot reducerの返す完全なstate treeを格納する。 store.subscribe(listener)で登録されたどのlistenerも、現在のstateを取得するためにstore.getState()を呼び出すことができる。

Usage with React

ReactとReduxは何の関係もない。ReduxアプリはReactでもAngularでもEmberでもjQueryでもバニラのJavaScriptでも書くことができる。

とは言うものの、reduxはReactDekuといったフレームワークと特に相性が良い。それらはUIをstateの関数として描写し、reduxはactionsの応答としてstateの更新を行うからである。

Installing React Redux

npm install --save react-redux

Container and Presentational Components

separating container and presentational

| | Constainer Components | Presentational Components | | | |___ | |Location |Top level, route handlers|Middle and leaf components | |Aware of Redux |Yes |No | |To read data |Subscribe to Redux state |Read data from props | |To change data |Dispatch Redux actions |Invoke callbacks from props|

todoアプリではビュー階層のトップに1つcontainer componentを持つ。

Designing Component Hierarchy

todoアイテムのリストを見せたい。todoaアイテムはクリック時に完了した印として横線が入る。ユーザーが新しいtodoを追加するフィールドを見せたい。フッタには全て/完了したものだけ/まだ完了していないものだけのtodoを切り替えるスイッチを見せたい。

  • AddTodo is an input field with a button.
      - ``onAddClick(text: string)`` is a callback to invoke when a button is pressed.
    
  • TodoList is a list showing visible todos.
      - ``todos: Array`` is an array of todo items with { text, completed } shape.
      - ``onTodoClick(index: number)`` is a callback to invoke when a todo is clicked.
    
  • Todo is a single todo item.
      - ``text: string`` is the text to show.
      - ``completed: boolean`` is whether todo should appear crossed out.
      - ``onClick()`` is a callback to invoke when a todo is clicked.
    
  • Footer is a component where we let user change visible todo filter.
      - ``filter: string`` is the current filter: 'SHOW_ALL', 'SHOW_COMPLETED' or 'SHOW_ACTIVE'.
      - ``onFilterChange(nextFilter: string)``: Callback to invoke when user chooses a different filter.
    

これらは全てpresentational componentsであり、データがどこからくるのかどのように変化させるのかに関与しません。

W Presentational Componets

components/AddTodo.js

import React, { Component, PropTypes } from 'react'

export default class AddTodo extends Component {
  render() {
    return (
      <div>
        <input type='text' ref='input' />
        <button onClick={e => this.handleClick(e)}>
          Add
        </button>
      </div>
    )
  }

  handleClick(e) {
    const node = this.refs.input
    const text = node.value.trim()
    this.props.onAddClick(text)
    node.value = ''
  }
}

AddTodo.propTypes = {
  onAddClick: PropTypes.func.isRequired
}

components/Todo.js

import React, { Component, PropTypes } from 'react'

export default class Todo extends Component {
  render() {
    return (
      <li
        onClick={this.props.onClick}
        style={{
          textDecoration: this.props.completed ? 'line-through' : 'none',
          cursor: this.props.completed ? 'default' : 'pointer'
        }}>
        {this.props.text}
      </li>
    )
  }
}

Todo.propTypes = {
  onClick: PropTypes.func.isRequired,
  text: PropTypes.string.isRequired,
  completed: PropTypes.bool.isRequired
}

components/TodoList.js

import React, { Component, PropTypes } from 'react'
import Todo from './Todo'

export default class TodoList extends Component {
  render() {
    return (
      <ul>
        {this.props.todos.map((todo, index) =>
          <Todo {...todo}
                key={index}
                onClick={() => this.props.onTodoClick(index)} />
        )}
      </ul>
    )
  }
}

TodoList.propTypes = {
  onTodoClick: PropTypes.func.isRequired,
  todos: PropTypes.arrayOf(PropTypes.shape({
    text: PropTypes.string.isRequired,
    completed: PropTypes.bool.isRequired
  }).isRequired).isRequired
}

components/Footer.js

import React, { Component, PropTypes } from 'react'

export default class Footer extends Component {
  renderFilter(filter, name) {
    if (filter === this.props.filter) {
      return name
    }

    return (
      <a href='#' onClick={e => {
        e.preventDefault()
        this.props.onFilterChange(filter)
      }}>
        {name}
      </a>
    )
  }

  render() {
    return (
      <p>
        Show:
        {' '}
        {this.renderFilter('SHOW_ALL', 'All')}
        {', '}
        {this.renderFilter('SHOW_COMPLETED', 'Completed')}
        {', '}
        {this.renderFilter('SHOW_ACTIVE', 'Active')}
        .
      </p>
    )
  }
}

Footer.propTypes = {
  onFilterChange: PropTypes.func.isRequired,
  filter: PropTypes.oneOf([
    'SHOW_ALL',
    'SHOW_COMPLETED',
    'SHOW_ACTIVE'
  ]).isRequired
}

これらが正しく動いているかどうかを確かめるために、ダミーのAppを書く。 containers/App.js

import React, { Component } from 'react'
import AddTodo from '../components/AddTodo'
import TodoList from '../components/TodoList'
import Footer from '../components/Footer'

export default class App extends Component {
  render() {
    return (
      <div>
        <AddTodo
          onAddClick={text =>
            console.log('add todo', text)
          } />
        <TodoList
          todos={
            [
              {
                text: 'Use Redux',
                completed: true
              },
              {
                text: 'Learn to connect it to React',
                completed: false
              }
            ]
          }
          onTodoClick={index =>
            console.log('todo clicked', index)
          } />
        <Footer
          filter='SHOW_ALL'
          onFilterChange={filter =>
            console.log('filter change', filter)
          } />
      </div>
    )
  }
}

<App />を描画するとこうなる。

Connectiong to Redux

まずはじめにProviderreact-reduxからインポートする必要がある。そして、描画する前に<Provider>でroot componentを囲む。

index.js

import React from 'react'
import { render } from 'react-dom'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import App from './containers/App'
import todoApp from './reducers'

let store = createStore(todoApp)

let rootElement = document.getElementById('root')
render(
    <Provider store={store}>
        <App />
    </Provider>,
    rootElement
)

これでcomponentでstoreのインスタンスが利用可能になる。(内部的にはこれはReactの"context"によって行われる。)

次にReduxと繋ぎたいcomponentsを、react-reduxからの関数であるconnect()で囲む。トップレベルのcomponentかルートハンドラのみでこれは行う。技術的にはどのcomponentもReduxとconnect()が可能ではあるが、データフローの追跡を容易にするためにこれを複雑にすべきではない。

connect()でラップされたどのcomponentも、dispatch関数をpropsとして受け取り、グローバルstateから必要とするものを選び取る。第一の引数のみをconnect()に渡すことが最もよくある。これはselectorと呼ばれる関数で、グローバルなstoreのstateを取り、コンポーネントに必要なpropsを返す。
reselect
containers/App.js

import React, { Component, PropTypes } from 'react'
import { connect } from 'react-redux'
import { addTodo, completeTodo, setVisibilityFilter, VisibilityFilters } from '../actions'
import AddTodo from '../components/AddTodo'
import TodoList from '../components/TodoList'
import Footer from '../components/Footer'

class App extends Component {
  render() {
    // Injected by connect() call:
    const { dispatch, visibleTodos, visibilityFilter } = this.props
    return (
      <div>
        <AddTodo
          onAddClick={text =>
            dispatch(addTodo(text))
          } />
        <TodoList
          todos={visibleTodos}
          onTodoClick={index =>
            dispatch(completeTodo(index))
          } />
        <Footer
          filter={visibilityFilter}
          onFilterChange={nextFilter =>
            dispatch(setVisibilityFilter(nextFilter))
          } />
      </div>
    )
  }
}

App.propTypes = {
  visibleTodos: PropTypes.arrayOf(PropTypes.shape({
    text: PropTypes.string.isRequired,
    completed: PropTypes.bool.isRequired
  }).isRequired).isRequired,
  visibilityFilter: PropTypes.oneOf([
    'SHOW_ALL',
    'SHOW_COMPLETED',
    'SHOW_ACTIVE'
  ]).isRequired
}

function selectTodos(todos, filter) {
  switch (filter) {
    case VisibilityFilters.SHOW_ALL:
      return todos
    case VisibilityFilters.SHOW_COMPLETED:
      return todos.filter(todo => todo.completed)
    case VisibilityFilters.SHOW_ACTIVE:
      return todos.filter(todo => !todo.completed)
  }
}

// Which props do we want to inject, given the global state?
// Note: use https://github.com/faassen/reselect for better performance.
function select(state) {
  return {
    visibleTodos: selectTodos(state.todos, state.visibilityFilter),
    visibilityFilter: state.visibilityFilter
  }
}

// Wrap the component to inject dispatch and state into it
export default connect(select)(App)

Example: Todo List

Source Code