Actions 描述了在应用里面发生的事情,但是应用的状态(state)具体应该怎么样响应这些动作是 Reducers 的任务。
设计状态
state(状态),指的就是数据。在 Redux 里,应用的所有的状态都会存储在唯一的一个对象里。写代码之前最好先想想这个对象的形状。
比如我们的任务列表应用,会存储两种东西:
- 当前所选的可见性过滤器
- 任务列表项目
在状态树里会存储数据还有一些 UI(界面) 状态,最好让数据与 UI 状态分开。
{ visibilityFilter: 'SHOW_ALL', todos: [ { text: 'Consider using Redux', completed: true, }, { text: 'Keep all state in a single tree', completed: false } ] }
处理动作
我们已经确定了状态对象的样子,现在可以去为它写个 Reducer 了。Reducer 是纯函数(Pure functions),Reducer 会接收两个东西:之前的状态还有一个动作,然后它会返回应用的下一个状态。
(previousState, action) => newState
Reducer 是纯函数,下面这些东西不应该在 Reducer 里出现:
- 改变了它的参数。
- 做了些带副作用的事,比如调用了 API。
- 调用不纯的函数,比如 Date.now() 或 Math.random() 。
以后会介绍怎么样做带副作用的事,现在你要记住的是 Reducer 必须是纯的。给它同样的参数,它要运算并返回下一个状态。无意外,不带副作用,不调用 API,没有改变,只是一个计算。
下面我们就一步一步的教会 Reducer 明白我们之前定义的动作。首先我们要定义一个初始的状态。Redux 第一次调用 Reducer 会带一个 undefined 状态,返回应用的初始状态:
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 }
也可以使用 ES2015 函数的默认参数,像这样:
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(),也可以使用对象的 Spread 操作符: { ...state, ...newState }。
- 在默认的情况下返回之前的状态。这样如果有未知的动作发生的时候,就会返回之前的状态。
处理更多动作
还有两个要处理的动作,跟处理 SET_VISIBILITY_FILTER
动作一样。下面我们要导入 ADD_TODO
还有 TOGGLE_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 } }
跟之前一样,不要直接改变状态,要返回一个新的对象,新的任务列表就是老的任务列表加上新的任务列表项目,新的任务列表项目用了在动作那里组织好的数据。
再处理下 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 }) })
分离 Reducers
代码现在是这样的:
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 TOGGLE_TODO: return Object.assign({}, state, { todos: todos(state.todos, action) }) default: return state } }
注意上面的 todos
同样会接收 state
(状态),不过这个 state
是一个数组。todoApp
只给它一部分状态去管理,todos 知道怎么样去更新这小部分状态。这就是 Reducer 组合,这是创建 Redux 应用的基础模式。
下面再抽离出一个 visibilityFilter :
const { SHOW_ALL } = VisibilityFilters;
然后再:
function visibilityFilter(state = SHOW_ALL, action) { switch (action.type) { case SET_VISIBILITY_FILTER: return action.filter default: return state } }
重新再写一下主 Reducer(todoApp),用它组合一下之前我们抽离出来的两个 Reducer(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 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 都会管理它们自己的那部分全局状态。每个 Reducer 的 state 参数是不一样的,对应的就是它管理的那部分 state。你也可以把 Reducer 放在单独的文件里。
Redux 提供了一个帮手方法:combineReducers()
,用它可以这样重写一下组合 Reducer 的代码:
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) } }
也可以给 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) } }
Source Code
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 todoAppRedux