提醒: 本节中,所有赋值语句均可以使用解构赋值
action
Action 是把数据从应用(译者注:这里之所以不叫 view 是因为这些数据有可能是服务器响应,用户输入或其它非 view 的数据 )传到 store 的有效载荷。它是 store 数据的唯一来源。一般来说你会通过 store.dispatch()
将 action 传到 store。
在dispatch一个action时候,action中包含action类型和载荷:
dispatch({
type: 'login'
payload: {},
});
action的本质是普通的js对象。且有以下约定:
- action中必须有一个
type
字段来表示action的类型 - action中的其他字段参照Flux 标准 Action
在这个todo app中,可以定义下面的action
{
type: ''
}
action创建函数
action创建函数本质是一个返回action对象的js函数。
在redux中,创建函数只返回一个简单的action:
export function addTodo(todoObj) {
return {
type: 'addTodo',
text: todoObj.text,
status: '-1',
index: todoObj.index,
}
}
这样做使创建函数更容易移植和测试。
传统给flus实现中,调用创建函数时一般还会发送一个 dispatch
:
function addTodoWithDispatch(text) {
const action = {
type: ADD_TODO,
...text
}
dispatch(action)
}
redux中发送dispatch只需要将创建函数的结果传给 dispath()
dispatch(addTodo(text))
或者创建一个 被绑定的 action 创建函数 来自动 dispatch:
const boundAddTodo = text => dispatch(addTodo(text))
const boundCompleteTodo = index => dispatch(completeTodo(index))
然后直接调用
boundAddTodo(text);
boundCompleteTodo(index);
store中可以直接通过 store.dispatch
来发起dispatch,但通常做法是使用redux-react中的 connect()
帮助器来调用。bindActionCreators()
可以自动把多个 action 创建函数 绑定到 dispatch()
方法上。
reducers
Reducers 指定了应用状态的变化如何响应 actions 并发送到 store 的,记住 actions 只是描述了有事情发生了这一事实,并没有描述应用如何更新 state。
设计state结构
在 Redux 应用中,所有的 state 都被保存在一个单一对象中。建议在写代码前先想一下这个对象的结构。如何才能以最简的形式把应用的 state 用对象描述出来?
以 todo 应用为例,需要保存两种不同的数据:
- 当前选中的任务过滤条件;
- 完整的任务列表。
通常,这个 state 树还需要存放其它一些数据,以及一些 UI 相关的 state。这样做没问题,但尽量把这些数据与 UI 相关的 state 分开。
const state = {
visible: "showAll",
todos: [
{
text: "上班打卡",
status: 1,
},
{
text: "下班打卡",
status: -1,
}
],
}
处理 Reducer 关系时的注意事项
开发复杂的应用时,不可避免会有一些数据相互引用。建议你尽可能地把 state 范式化,不存在嵌套。把所有数据放到一个对象里,每个数据以 ID 为主键,不同实体或列表间通过 ID 相互引用数据。把应用的 state 想像成数据库。这种方法在 normalizr 文档里有详细阐述。例如,实际开发中,在 state 里同时存放
todosById: { id -> todo }
和todos: array<id>
是比较好的方式,本文中为了保持示例简单没有这样处理。对上面注意事项的附加内容 实际开发中,数据关联交给后端,前端只负责获取数据以及展示数据。后端在查库的时候通过id关联来返回数据,前端获取到的数据,如果有id需求,那么得到的数据就是有id字段。比如从后端拿去系统里所有用户信息,每个用户都需要有唯一id。如果获取的数据不需要有id,比如一些状态字段,这时候得到的数据就不会有id字段。
通常情况下,每个表都会有一个id字段。action处理
确定action结构后,就可以开发reducer。reducer是一个纯函数,接收action和旧的state,返回新的state。reducer中 禁止 进行以下动作:
- 修改传入参数
- 执行有副作用的函数
- 调用非纯函数
```javascript
const initState = {
visible: “showAll”,
todos: [
{
}, {text: "上班打卡",
status: 1,
} ], }text: "下班打卡",
status: -1,
function todoApp(state, action) { if(typeof action === “undefined”) { return initState; } switch(action.type) { case “addTodo”: { return { …state, todos: [ …state.todos, { text: action.text, status: action.status, } ] } } case “setVisible”: { return { …state, visible: action.visible, } } case “toggleStatus”: { const newTodos = state.todos.map(current => { if(action.index == current.index) { return { …current, status: current.index + 1, } } return current; }) return { …state, …newTodos, } } default: { return initState; } } }
export default todoApp;
注意到上面的reducer中, `setVisible` 只设置过滤状态,并不参与待办的增加和状态转换,因此可以将其拆分:
```javascript
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)
}
}
redux提供了 combineReducers
来做上面 todoApp
做的事情:
import { combineReducers } from 'redux'
const todoApp = combineReducers({
visibilityFilter,
todos
})
export default todoApp
注意上面的写法和下面完全等价:
import { combineReducers } from 'redux'
const todoApp = combineReducers({
visibilityFilter,
todos
})
export default todoApp
你也可以给它们设置不同的 key,或者调用不同的函数。下面两种合成 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,每个 reducer 根据它们的 key 来筛选出 state 中的一部分数据并处理,然后这个生成的函数再将所有 reducer 的结果合并成一个大的对象。没有任何魔法。正如其他 reducers,如果 combineReducers() 中包含的所有 reducers 都没有更改 state,那么也就不会创建一个新的对象。
在es6中, combineReducers
接收一个对象,可以把所有顶级的 reducer 放到一个独立的文件中,通过 export
暴露出每个 reducer 函数,然后使用 import * as reducers
得到一个以它们名字作为 key 的 object:
import { combineReducers } from 'redux'
import * as reducers from './reducers'
const todoApp = combineReducers(reducers)
提示 reducers中的switch条件不一定必须是
action.type
,也可以是约定好的其余字段。但是action中type字段是必须有,否则应用会运行异常。
store
store负责将action和reducer联系到一起,有以下作用:
- 维持应用的 state;
- 提供
getState()
方法获取 state; - 提供
dispatch(action)
方法更新 state; - 通过
subscribe(listener)
注册监听器; - 通过
subscribe(listener)
返回的函数注销监听器。
每个redux应用有且只能有一个store 。如果需要拆分逻辑,需要组合reducers而不是再开一个store。
// 创建store
import { createStore } from 'redux'
import todoApp from './reducers'
let store = createStore(todoApp)
createStore()
的第二个参数是可选的, 用于设置 state 初始状态。这对开发同构应用时非常有用,服务器端 redux 应用的 state 结构可以与客户端保持一致, 那么客户端可以将从网络接收到的服务端 state 直接用于本地数据初始化。
let store = createStore(todoApp, window.STATE_FROM_SERVER)
发起actions
import * as actions from './actions/action';
import store from './store';
// 打印初始状态
console.log(store.getState())
// 每次 state 更新时,打印日志
// 注意 subscribe() 返回一个函数用来注销监听器
const unsubscribe = store.subscribe(() => console.log(store.getState()))
// 发起一系列 action
store.dispatch(actions.addTodo({
text: '周末去哪玩',
status: -1,
}))
// 停止监听 state 更新
unsubscribe()
官网示例:
import {
addTodo,
toggleTodo,
setVisibilityFilter,
VisibilityFilters
} from './actions'
// 打印初始状态
console.log(store.getState())
// 每次 state 更新时,打印日志
// 注意 subscribe() 返回一个函数用来注销监听器
const unsubscribe = store.subscribe(() => console.log(store.getState()))
// 发起一系列 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))
// 停止监听 state 更新
unsubscribe()
搭配react
在搭配react时,要考虑以下要点:
- 事件处理:切换代办状态时要触发action,让reducer更改state
- 获取state:state需要手动放到组件中,如果没有任何操作,state不会自己跑到组件state或者props中
- 传入store:redux从stroe中获取state和actions,和state一样,store也不会自己跑到组件中
获取state
获取state通过容器组件来实现。容器组件将组件和redux关联起来。
技术上讲,容器组件就是通过 store.subscribe()
从state树中读取数据,并将其通过props提供给要渲染的组件。你可以手工来开发容器组件,但建议使用 React Redux 库的 connect()
方法来生成,这个方法做了性能优化来避免很多不必要的重复渲染。(这样你就不必为了性能而手动实现 React 性能优化建议 中的 shouldComponentUpdate
方法。)
const mapStateToProps = (state) => {
return {
todos: state.todos,
visible: state.visible,
}
}
const ConnectedTodoApp = connect(mapStateToProps)(TodoApp);
除了读取state,容器组件还可以分发action。和state类似,可以定义 mapDispatchToProps
方法接收 dispatch
中的方法并返回想要注入要渲染的组件的props中的回调方法。
const mapDispatchToProps = dispatch => {
return {
onTodoClick: id => {
dispatch(toggleTodo(id))
}
}
}
传入store
所有容器组件都可以访问 Redux store,所以可以手动监听它。一种方式是把它以 props 的形式传入到所有容器组件中。但这太麻烦了,因为必须要用 store
把展示组件包裹一层,仅仅是因为恰好在组件树中渲染了一个容器组件。
建议的方式是使用指定的 React Redux 组件 <Provider>
来 魔法般的 让所有容器组件都可以访问 store,而不必显式地传递它。只需要在渲染根组件时使用即可。
export default function() {
return(
<Provider store={store}>
<ConnectedTodoApp/>
</Provider>
);
}
效果
源码
import { Button, Input, message, Table } from 'antd';
import React, { useState } from 'react';
import { connect, Provider } from 'react-redux';
import store from './store';
import * as actions from './actions/action';
/* eslint-disable eqeqeq */
function TodoApp(props) {
const [value, setValue] = useState("");
function onChange(e) {
if(e) {
e.preventDefault();
}
setValue(e.target.value);
}
function renderOptions(text, index) {
const { dispatch } = props;
if(text.status === "-1") {
return(
<div>
<Button onClick={() => dispatch(actions.toggleStatus({
index: index + 1,
status: "0",
}))}>开始</Button>
<Button onClick={() => dispatch(actions.toggleStatus({
index: index + 1,
status: "1",
}))}>完成</Button>
<Button onClick={() => dispatch(actions.toggleStatus({
index: index + 1,
status: "-1",
}))}>重置</Button>
</div>
);
}
if(text.status == 0) {
return(
<div>
<Button onClick={() => dispatch(actions.toggleStatus({
index: index + 1,
status: "1",
}))}>完成</Button>
<Button onClick={() => dispatch(actions.toggleStatus({
index: index + 1,
status: "-1",
}))}>重置</Button>
</div>
);
}
if(text.status == 1) {
return(
<Button onClick={() => dispatch(actions.toggleStatus({
index: index + 1,
status: "-1",
}))}>重置</Button>
);
}
}
const addTodo = () => {
const { dispatch } = props;
if(value.length === 0 || !value) {
message.warn("不允许添加空代办");
return;
}
dispatch(actions.addTodo({
text: value,
index: props.todos.length + 1,
status: "-1",
}))
}
const columns = [
{
title: "序号",
dataIndex: "index",
width: "10%",
},
{
title: "代办",
dataIndex: "text",
width: "60%",
},
{
title: "状态",
dataIndex: "status",
width: "10%",
filters: [
{
text: "未开始",
value: "-1",
},
{
text: "进行中",
value: "0",
},
{
text: "已完成",
value: "1",
},
],
onFilter: (value, record) => value === record.status,
render: (record, text) => {
if(text.status === "-1") {
return "未开始";
}
if(text.status === "1") {
return "已完成";
}
return "进行中";
}
},
{
title: "操作",
render: (text, record, index) => renderOptions(text, index)
},
];
return(
<div>
<Button onClick={addTodo}>新增代办</Button>
<Input style={{width: 600}} onChange={onChange} value={value}/>
<Table
columns={columns}
dataSource={props.todos}
/>
</div>
);
}
const mapStateToProps = (state) => {
return {
todos: state.todos,
visible: state.visible,
}
}
const ConnectedTodoApp = connect(mapStateToProps)(TodoApp);
export default function() {
return(
<Provider store={store}>
<ConnectedTodoApp/>
</Provider>
);
}
注意事项
connect后的组件必须包裹在Provider中
如果 connect 返回的组件的父组件中没有包含 store 的 Provider 则会报错找不到 stroe.