目录
接下来要设计到以下的内容
- Actions & action creator
- Reducers
- Store
- Data Flow
- Usage with React
- Example: Todo List
动作及动作创建者
所谓动作就是普通 javascript 的对象, 它用来描述动作的内容,
他们从 app 发送到 store. 他们就是 store 里的数据的源头. 通过 store.dispatch()
来发送数据.
{
type: 'ADD_TODO',
text: 'Build my first Redux app'
}
通常我们把动作的类型定义为一个常量
const ADD_TODO = 'ADD_TODO'
其实所谓 动作 就是一个普通的 javascript 对象, 它必须有一个 type
属性, 用来表明它能做什么样的动作.而这个 type 必须是一个声明为常量的字符串对象. 而且一般app 的 action 都会很多,所以可以单独把他们放在一个文件. 然后可以这样引用:
import { ADD_TODO, REMOVE_TODO } from '../actionTypes'
接下来,我们将要创建一个新的动作,用来描述用户勾选某个 todo 为完成的动作. 我们会通过 index
来标记每一个 todo
.因为我们把他们放在 array
里,这里只是 demo, 而真实的 app 最好有专门的逻辑用来生成找个 id.
{
type: TOGGLE_TODO,
index: 5
}
在这,其实每一个动作尽可能让它携带最简化的数据集. 例如, 我们通过 index
就可以代表这个 todo对象.
{
type: SET_VISIBILITY_FILTER,
filter: SHOW_COMPLETED
}
动作
我们先说到这儿,接下来说说动作创建者
. 在 Redux 中, 动作的创建者 就是一个地道的函数, 用来创建并返回一个动作. 可能很容易在这里混淆动作
和动作创建者
.
动作创建者
的作用就是返回一个动作:
function addTodo(text){
return {
type: ADD_TODO,
text
}
}
在传统的 Flux 架构中, 一个动作者通常在定义一个 action, 之后还有
分发
到已经注册的某个 store 里?
function addTodoWithDispatch(text) {
const action = {
type: ADD_TODO,
text
}
dispatch(action)
}
而在 Redux 中不这么处理. 取而代之的是去调用 Store
的 dispatch()
函数. 把一个动作作为参数传递给它.
Store.dispatch(addTodo(text))
Store.dispatch(completeTodo(index))
或者,可以创建一个绑定动作创建者( bound action creator)
来自动实现动作的分发:
const boundAddTodo = (text) => dispatch(addTodo(text))
const boundCompleteTodo = (index) => dispatch(completeTodo(index))
现在可以这样使用他们:
boundAddTodo(text)
boundCompleteTodo(index)
这个分发 dispatch()
函数可以直接通过 store.dispatch()
在 store
里访问到, 或者直接通过 react-redux
的帮助类方法 connect()
, 使用 bindActionCreators()
来自动将这些动作创建者
绑定到对应的 dispatch()
函数上.
动作创建者还可以是异步的,但具有副作用.
You can read about async actions in the advanced tutorial to learn how to handle AJAX responses and compose action creators into async control flow.
说了这么多, 来看看全部的源码.
/*
* action types
*/
export const ADD_TODO = 'ADD_TODO'
export const TOGGLE_TODO = 'TOGGLE_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 toggleTodo(index) {
return { type: TOGGLE_TODO, index }
}
export function setVisibilityFilter(filter) {
return { type: SET_VISIBILITY_FILTER, filter }
}
接着说, 刚才我们说到了创建动作
和动作创建者
以及如何绑定和分发动作.
接下来我们看看 reducer
.
Reducer
现在我们考虑的问题就是, 动作分发了之后呢, 到哪儿去,干了什么!?
关于动作
, 它是实实在在定义了干什么. 但是它干什么怎么才能够影响到 app 的状态呢, 让 app 做出对应的响应呢, 这就是 reducer
的要做的.
设计状态结构
先来说说 state
,在 redux 里所有的状态都是保持在一个单个的对象里,现在我们来想想一下她的样子.
拿todo app
来说,我们要存储2样东西:
- todo 的 list
- 这个是最主要的,时刻都能够取到完整的 todo 数据.
- 当前被选中的显示过滤器(要显示什么状态的 todo)
- 这里我们只需要保存当前用户选中需要显式那个类型的 todo 就好,(如当前进行的或者已经完成的.)
可能对于一个 app 来说,我们通常下意识会认为有很多事情,数据需要处理. 有很多的 case. 但是对于这里所讲的状态
来说, 我们需要抽象,取出最主要的东西.
比如说, 我们只需要 care 这些数据就可以, 利用这些数据我们就能够正常运转 app
{
visibilityFilter: 'SHOW_ALL',
todos: [
{
text: 'Consider using Redux',
completed: true,
},
{
text: 'Keep all state in a single tree',
completed: false
}
]
}
在一个较复杂的 app 中,可能实体之间会相互引用.我们建议尽量把状态保持越简单越好. 不要有嵌套的关系存在.
尽量然后每一个对象有一个 id 能够识别. 通过 id 来引用.想象 app 的状态就和 database 里的状态类似. 这个可以查看 normalizr 的详细文档来看.
处理动作
上面我们大概知道了找个 todo app 里状态
对象样子, 接下来我们写 reducer
.
重要事情说三遍
reducer
就是一个纯函数
,接受2个参数, 第一个的当前的状态,第二个是动作, 然后返回最新的状态.
reducer
就是一个纯函数
,接受2个参数, 第一个的当前的状态,第二个是动作, 然后返回最新的状态.
reducer
就是一个纯函数
,接受2个参数, 第一个的当前的状态,第二个是动作, 然后返回最新的状态.
(previousState, action) => newState
这个我就称之为 reducer
, 因为它其实是这样一个实现:
Array.prototype.reduce(reducer, ?initialValue)
对于 reducer
来说保证它的纯洁性
是非常重要的.在 reducer 里保证不能够发生如下的事情:
- 改变参数
- 不能够做其他事情如调用 API 或者修改 route
- 调用不
pure
的函数如Date.now()
或者Math.random()
这里先不讨论副作用的东西之后高阶时候会说. 现在我们只记住对于 reducer 的 function 必须是 pure 的. 给一样的参数,总是返回同样的结果,没有副作用,没有 api 调用,没有突变,只是一个计算
.
接下来我们一步步的来理解之前定义的 action
. 首选我们初始化状态
. redux 会调用一个 reducer 而当前是 undefined
状态. 此时就是一个机会来初始化 app 的状态.
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
}
上面代码里的关于 state
可以用 es6
的参数默认值
语法来写就是:
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
}
}
注意
- 我们这里不会修改
state
,而是通过object.assign()
函数来拷贝这个对象.
2. 而Object.assgin(state, {visibilityFilyer: action.filter})
同样是错误的. 因为state
作为第一个参数传递, 它将要被修改. 你必须传递一个空的对象给第一个参数. 看这里 - 我们同时把默认的初始状态作为默认返回值. 这个很重要一旦遇到未知的 action的情况下还是要返回状态.
注意:
Object.assign()是 es6的一部分,目前还没有大部分实现支持.需要一个 polyfill 来支持.我们用 Babel 的一个插件.
处理更多动作
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
, 而是取而代之用一个新的 object 来接住之前的 state, 然后再 merge 最后的 todo, 这样做到不修改原有的状态.
最后 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
})
})
我们希望要修改一个array 里特定的 ite m 而不突变.我们需要利用当前的 array创建一个新的 array, 而除了那个即将要被修改的. 而这样的动作似乎经常发生. 我们就可以接住帮助类如 react--addons-update
,updeep
来完成. 甚至有一个库可以干这样的事情, [Immutable(http://facebook.github.io/immutable-js/), 记住别去动 state.
分隔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
他们是单独更新的.有时候 state
是需要更加的独立的.但是这里可以吧 todo 的更新先单独到一个函数里:
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
, 而它是一个 array. 现在 todoAPP
给了它一段 state 让他来管理(也就是todo list). 而 todos
知道如何来更新这个片段(怎么知道的?). 这个称作 复合reducer,而且它是构建redux app 的基础模式.
让我们来看看这个复合的 reducer, 我们是否可以把对于 visibilityFilter 的管理也抽出来作为 reducer manager 呢, 可以的:
function visibilityFilter(state = SHOW_ALL, action){
switch (action.type){
case SET_VISIBILITY_FILTER:
return action.filer
default:
reuturn state
}
}
现在我们可以重写主 reducer 来调用每一个单独的reducer manager. 然后把他们合并到一个单个的对象里. 从此它不需要再去知道完整的初始状态了. 只需要让子 reducer 来返回一个 undefined
的初始状态.
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
并不是一样的,他们只负责他们各自对应的 state片段.
到这里看上去就不错了. 当随着 app 不断复杂,我们可以把 reducer 分割到不同的文件里,最后在主 reducer 里组合就好. 而且 redux 提供一个工具 -- combineReducers()
通过这个可以更加方便的把零散的 reducer 组合起来,所以我们可以重构之前的 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)
}
}
甚至可以给每一个 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 来通过不同对应的键利用自己的一段 state.最后把结果合并起来赛道一个单个对象.
都能写成这样:
Note for ES6 Savvy Users
Because combineReducers expects an object, we can put all top-level reducers into a separate file, export each reducer function, and use import * as reducers to get them as an object with their names as the keys:
import { combineReducers } from 'redux'
import * as reducers from './reducers'
const todoApp = combineReducers(reducers)
Because import * is still new syntax, we don't use it anymore in the documentation to avoid confusion, but you may encounter it in some community examples.
最终的代码如下:
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
, 它是如何处理 state
还有如何 在指派一个 action 之后如何调用 reducer.
Store
上面我们接触到如何定义 action 来表示发生了什么变化, 还有 reducer 它是如何通过不同的 action 来修改 app 的 state.
下面我们要说的 Store
, Store 是什么,从字面意思看它是用来存储什么的, 对, 它就是存储 state tree
的.
Store
有这样的作用:
- 管理app的
state
- 可以通过
getState()
来访问state
- 通过
dispatch(action)
来修改 app 的state
- 通过
subscribe(listener)
来注册监听 - 处理没有注册的监听
再次重申: Redux app 的 state
是保持在一个单独的对象里. 当需要分割处理数据的逻辑时, 应该使用复合 reducer
来替代多个 store.
如果你有 reducer,很容易来创建 store
. 上面我们使用了combineReducer()
来组合多个 reducer, 现在我们引用它然后把它传递给 createStore()
.
import { createStore } from 'redux'
import todoApp from './reducers'
let store = createStore(todoApp)
你可以通过传递一个可选的参数给
createStore()
来给 app 一个初始的状态,用来处理客户端和服务器端不同的状态(×)
let store = createStore(todoApp, window.STATE_FROM_SERVER)
分发动作
至此, 我们已经创建了 Store
, 让我们来验证 app 是否工作,我们可以在没UI 的情况下测试更新逻辑:
import { addTodo, toggleTodo, setVisibilityFilter, VisibilityFilters } from './actions'
// Log the initial state
console.log(store.getState())
// Every time the state changes, log it
// Note that subscribe() returns a function for unregistering the listener
let unsubscribe = store.subscribe(() =>
console.log(store.getState())
)
// Dispatch some actions
// 通过 store.dispatch() 方法可以模拟用户交互, 出发 action
store.dispatch(addTodo('Learn about actions'))
store.dispatch(addTodo('Learn about reducers'))
store.dispatch(addTodo('Learn about store'))
store.dispatch(toggleTodo(0))
store.dispatch(toggleTodo(1))
store.dispatch(setVisibilityFilter(VisibilityFilters.SHOW_COMPLETED))
// Stop listening to state updates
unsubscribe()

> index.js
import { createStore } from 'redux'
import todoApp from './reducers'
let store = createStore(todoApp)
**接下来我们要了解 redux app 里的数据流**
刚才说到了 [Store ](http://qiita.com/xiangzhuyuan/items/9ce23a8c9f3b26c48838)
接下来说 **数据流**
Redux 使用**单向数据流**
这就意味着在 app 里所有的数据都会经过同样 的生命周期模式. 让你的程序逻辑更加可以预测和容易理解.对于数据的处理同样要优化精华. 而不是让你死在多份同样的数据, 你手里的数据同样能够被他人来修改. 都不知道自己怎么死的.
如果还是没有被说服,建议读一下 [1](http://redux.js.org/docs/introduction/Motivation.html) [2](https://medium.com/@dan_abramov/the-case-for-flux-379b7d1982c6) 同时建议读一下[ Redux 不简单是 Flux 的变种](http://redux.js.org/docs/introduction/PriorArt.html)它同样说明了一些为什么要单向数据流的好处.
Redux 应用里的数据大致会走4步:
1. 当你调用` store.dispatch(action). 触发了一个动作.
一个动作就是一个简单的对象,说明了会发生什么.
{ type: 'LIKE_ARTICLE', articleId: 42 }
{ type: 'FETCH_USER_SUCCESS', response: { id: 3, name: 'Mary' } }
{ type: 'ADD_TODO', text: 'Read the Redux docs.' }
描述了一个动作, 已经一些参数用来指明动作作用的对象.
好比这样说: 一个动作就是'某某喜欢找个文章', 看 redux 的文章`等等.
**你可以在 redux app 的任何地方使用` store.dispatch(action), 包括组件或者 XHR 的回调, 甚至是一个循环调用.**
2. Redux store 调用你给它的 reducer 函数
**store 是会传递两个参数给` reducer` 的. 第一个是当前的状态, 第二个是 action, 然后返回 action 作用之后的最新状态.** 再来看个例子:
let previousState = { // 当前的状态
visibleTodoFilter: 'SHOW_ALL', //要显示什么类型
todos: [ //所有的 todo
{
text: 'Read the docs.',
complete: false
}
]
}
// 这是一个添加 todo 的动作
let action = {
type: 'ADD_TODO',
text: 'Understand the flow.'
}
// 获取最新的状态
let nextState = todoApp(previousState, action)
一定要保证` reducer` 都是 **pure** 的函数,也就是说每次给同样的参数返回结果都是一样的.
3. **根 reducer** 可以组合多个 reducer 到一个单独的状态树里.
如何组织*根 reducer *完全是个人喜好, Redux 提供一个` combineReducers()` 函数用来帮助组织分散的 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
})
当你触发一个动作, 然后 todoAPP 就会调用由`combineReducers`返回的对应的 reducer:
let nextTodos = todos(state.todos, action)
let nextVisibleTodoFilter = visibleTodoFilter(state.visibleTodoFilter, action)
其实上面的情况就是返回
return {
todos: nextTodos,
visibleTodoFilter: nextVisibleTodoFilter
}
4. **Redux Store** 会把所有的 **reducer** 返回的 state 写会单独完整的 state 里.
至此,算是我们已经完成的 backend 逻辑, 接下来我们会学习如何结合` react` 完全 UI 部分.
### 使用 React
这里说明的就是其实 Redux 它完成的逻辑是不限于 react 来完成 UI 部分的. 你完全可以使用如`Angular` `Ember` 或者` jQuery` 等 frontend 框架来写. 而只是 Redux 可以更好地和 react 或者 deku 结合使用. 因为从 react 他们的概念定义来说, 描述 UI 的过程就是在说明一个状态的函数. 和 Redux 响应一个动作出发状态更新.
接下来我们使用 react 来构建这个 todoapp 的 UI 部分.
#### 安装 react redux
[react 绑定](https://github.com/reactjs/react-redux) 是单独与 redux 存在,我们现在安装它:
npm install --save react-redux
> If you don't use npm, you may grab the latest UMD build from unpkg (either a development or a production build).
> The UMD build exports a global called window.
> ReactRedux if you add it to your page via a \<script\> tag.
### 展示容器组件(用来画 UI, 无逻辑)
**React redux 绑定**封装了`展示和容器组件分离`的概念. [若不熟悉先读它](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0),
很重要,可以离开放下这里,等你.
回来...
我们总结下有什么不同之处:
| \ |展示组件|容器组件|
|:--:|:-----:|:----:|
|目的|看上去像什么|他们是怎么工作的|
|和 Redux通信|不|是|
|怎么读取数据|从属性 props|订阅 redux 的状态|
|怎么修改数据|通过属性里的回调|触发 redux 的动作|
|谁写的|自己用手|通常由react redux 会生成|
我们需要处理的大部分的都是展示组件, 当然也要处理一些容器组件, 他们用来和` reudx store` 连接起来.
技术上来说, 你可以通过调用` store.subscribe()`来写容器组件,但是不建议你那样做.
因为` React redux` 通常会做很多的优化, 他们都是手动无法完成的.
因为这个, 我们更加趋向于通过使用 react-reudx 的` connect()` 函数来生成他们.
### 设计组件体系结构
之前我们同样遇到一个环节就是在[动手之前设计我们的 state 体系](http://redux.js.org/docs/basics/Reducers.html). 现在我们要设计如何组织 UI 结构了. 这里就不是 redux 的范畴了. 可以通过[如何使用 react](https://facebook.github.io/react/docs/thinking-in-react.html) 来学习理解如何处理.
我们在这里的设计很简单了, 我们要显示一个 todo 的 list. 点击 todo 的 item, 它就标记为完成. 而且可以过滤显示没有完成的,或者已经完成的.
---
#### 展示组件
通过上面大致的讲述,我现在脑海里就是这样一些东西浮现;
- TodoList 用来表示可见的 todo 列表
- todos:Array 就是一组 todo 的 item, 大概内容就是 {id,text,completed}.
- onTodoClick(id:number) 是一个回调当一个 todo 的 item 被点击的时候.
- Todo 就是表示一个单独的 todo
- text:string 当前 todo 的内容
- completed:boolean 当前是否标记为完成
- onClick() 是一个回调, 当在 item 被点击的时候触发
- Link 回调用
- onClick() link 在被点击后触发的回调
- Footer 用来显示什么状态的 toggle
- App 就是当前 app 的根元素
他们完全不依赖redux, 当你把 redux 迁移到其他的框架上时, 这些东西完全不需要修改. 他们只是用来渲染你想要在 UI 上看到的东西
---
#### 容器组件
容器组件在这里充当的角色就是用它来连接展示组件和 redux, 让redux 的数能对应的显示在展示组件上. 比如刚才定义的展示组件` TodoList` 组件它就需要一个容器比如叫` VisibleTodoList`, 然后让它订阅到` Redux store`里的当前对应可见的` todo`, 为了修改可见过滤器,我们需要一个比如` FilterLink` 的容器来完成渲染一个`Link` 来让它完成一个动作的触发当点击它的时候:
- VisibleTodoList 通过当前的显示过滤条件拿到 todo list
- FilterLink 获取当前的过滤条件然后渲染一个 link
- Filter:string
#### 其他组件
有时候很难界定它到底是展示组件还是容器组件. 例如有时候一个表单或者一个函数他们精密的缠绕,如
- AddTodo 就是一个输入框加一个`添加`按钮
技术上讲我们可以把他们分开,但是呢现在这个初级阶段我们最好把它混合在一起. 因为组件的逻辑也不是很麻烦, 随着 app 的成长,如何分割他们就变得越来越明显.
### 动手实现组件
我们先从`展示组件`开始,因为我们不需要考虑如何和 redux 联系起来.
#### 展示组件
这些都是普通的 React 组件.我们也不会涉及太多,就是写一些无状态的纯函数.
除非我们还需要一些本地的状态或者生命周期的方法.当然也不是说组件必须是函数.只是用函数简单.
如何你之后设计到本地状态,生命周期的方法,或者性能优化的时候可以把它们写成`类`.
```components/Todo.js
import React, { PropTypes } from 'react'
// 定义单个 todo 对象
// 需要3个参数,1 click事件, 2 是否完成, 3 显式文本
const Todo = ({ onClick, completed, text }) => (
<li onClick={onClick} style={{ textDecoration: completed ? 'line-through' : 'none' }} > {text} </li>
)
// 验证3个参数
Todo.propTypes = {
onClick: PropTypes.func.isRequired,
completed: PropTypes.bool.isRequired,
text: PropTypes.string.isRequired
}
export default Todo
import React, { PropTypes } from 'react'
import Todo from './Todo'
const TodoList = ({ todos, onTodoClick }) => (
<ul>
{todos.map(todo =>
<Todo
key={todo.id}
{...todo}
onClick={() => onTodoClick(todo.id)}
/>
)}
</ul>
)
TodoList.propTypes = {
todos: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.number.isRequired,
completed: PropTypes.bool.isRequired,
text: PropTypes.string.isRequired
}).isRequired).isRequired,
onTodoClick: PropTypes.func.isRequired
}
export default TodoList
import React, { PropTypes } from 'react'
const Link = ({ active, children, onClick }) => {
if (active) {
return <span>{children}</span>
}
return (
<a href="#"
onClick={e => {
e.preventDefault()
onClick()
}}
>
{children}
</a>
)
}
Link.propTypes = {
active: PropTypes.bool.isRequired,
children: PropTypes.node.isRequired,
onClick: PropTypes.func.isRequired
}
export default Link
import React from 'react'
import FilterLink from '../containers/FilterLink'
const Footer = () => (
<p>
Show:
{" "}
<FilterLink filter="SHOW_ALL">
All
</FilterLink>
{", "}
<FilterLink filter="SHOW_ACTIVE">
Active
</FilterLink>
{", "}
<FilterLink filter="SHOW_COMPLETED">
Completed
</FilterLink>
</p>
)
export default Footer
import React from 'react'
import Footer from './Footer'
import AddTodo from '../containers/AddTodo'
import VisibleTodoList from '../containers/VisibleTodoList'
const App = () => (
<div>
<AddTodo />
<VisibleTodoList />
<Footer />
</div>
)
export default App
容器组件
现在是时候把这些视图组件 hook 到 redux 里通过创建一些容器组件. 技术上来说, 一个容器组件就是一个 React 的组件( component), 他们通过使用 store.subscribe()
来读取 Redux
状态树中的部分状态数据,然后给视图组件的 props
对象,之后展示的过程和逻辑. 你可以手敲一个容器组件, 但是不建议这么做有点傻,建议你通过使用 react redux 的库提供的 connect()
函数来生成这些容器组件.他们同时提供了很多优化来防止重复没用的渲染.
为了使用 connect()
,你需要定义一个特殊的函数名字叫做 mapStateToProps
, 它的作用就是告诉当前的 app 如何把 redux 里的特定的状态片段对应到特定组件的 props
属性上.举个例子, VisibleTodoList
需要计算 todos
, 把它传给 TodoList
. 所以我们需要定义一个函数,它能够过滤出 state.todos
, 通过 state.visibilityFilter
, 之后在 mapStateToProps
里是这样的:
const getVisibleTodos = (todos, filter) => {
switch (filter) {
case 'SHOW_ALL':
return todos
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed)
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed)
}
}
const mapStateToProps = (state) => {
return {
todos: getVisibleTodos(state.todos, state.visibilityFilter)
}
}
除了读取 redux 的状态,容器组件还可以分发动作. 通常流行的写法是你可以定义一个 mapDispatchToProps()
函数.他可以接收一个dispatch()
方法,然后返回一个回调参数, 这个回调参数就是你想要 hook到视图组件中的. 例如, 我们想要把 VisibleTodoList
注入一个 onTodoClick
的属性到 TodoList
的组件.而且这个 onTodoClick
还可以方法 TOGGLE_TODO
的动作:
const mapDispatchToProps = (dispatch) => {
return {
onTodoClick: (id) => {
dispatch(toggleTodo(id))
}
}
}
最终,我们通过调用 connect()
传递2个参数, 创建了 VisibleTodoList
对象.
import { connect } from 'react-redux'
const VisibleTodoList = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)
export default VisibleTodoList
这些都是 React redux 基本的 API, 但是还有很多方便好用的方法所以建议你阅读它的文档,如果你 经常担心 mapStateToProps
创建新的对象,你可以学习computing derived data with reselect.
接下来是全部的容器组件的代码:
import { connect } from 'react-redux'
import { setVisibilityFilter } from '../actions'
import Link from '../components/Link'
const mapStateToProps = (state, ownProps) => {
return {
active: ownProps.filter === state.visibilityFilter
}
}
const mapDispatchToProps = (dispatch, ownProps) => {
return {
onClick: () => {
dispatch(setVisibilityFilter(ownProps.filter))
}
}
}
const FilterLink = connect(
mapStateToProps,
mapDispatchToProps
)(Link)
export default FilterLink
import { connect } from 'react-redux'
import { toggleTodo } from '../actions'
import TodoList from '../components/TodoList'
const getVisibleTodos = (todos, filter) => {
switch (filter) {
case 'SHOW_ALL':
return todos
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed)
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed)
}
}
const mapStateToProps = (state) => {
return {
todos: getVisibleTodos(state.todos, state.visibilityFilter)
}
}
const mapDispatchToProps = (dispatch) => {
return {
onTodoClick: (id) => {
dispatch(toggleTodo(id))
}
}
}
const VisibleTodoList = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)
export default VisibleTodoList
其他的组件
import React from 'react'
import { connect } from 'react-redux'
import { addTodo } from '../actions'
let AddTodo = ({ dispatch }) => {
let input
return (
<div>
<form onSubmit={e => {
e.preventDefault()
if (!input.value.trim()) {
return
}
dispatch(addTodo(input.value))
input.value = ''
}}>
<input ref={node => {
input = node
}} />
<button type="submit">
Add Todo
</button>
</form>
</div>
)
}
AddTodo = connect()(AddTodo)
export default AddTodo
传递 Store
所有的容器组件都能够访问 Redux 的 store, 所以他们可以订阅它. 有个方法就是可以把它当做一个参数 prop 传递给每一个容器组件. 但是这样很无聊,你需要把它在所有视图组件中传递?
就是因为他们在组件树里很深的位置哦.?不懂
我们推荐的一个方法就是使用一个特殊的 react redux 组件叫做 <Provider>
,它可以很魔性的让 store
变得在所有的容器组件中可见, 而不需要再去显示的传递它, 因为你只需要在根组件里引用一次.
import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import todoApp from './reducers'
import App from './components/App'
let store = createStore(todoApp)
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
到这里, 一个 todo
app 也就完事儿, 我们回过头来重新查看它的源码, 对比上面我们所讲解过的一切. 再去感受如何 build 一个redux-react app.
-THE END-