React系列教程(4)React Redux快速入门
豆约翰习惯将掌握某一技术分为5个层次:初窥门径,小试牛刀,渐入佳境,得心应手,玩转自如
本篇属于React框架中的第1层次即初窥门径
- 原文地址:A Complete React Redux Tutorial for 2019
- 原文作者:Dave Ceddia
- 译文出自:掘金翻译计划
- 本文永久链接:github.com/xitu/gold-m…
- 译者:xilihuasi
- 校对者:xionglong58、Fengziyin1234
注:对原文有增补(补充了请求api接口的完整实例以及配套源码)
想要理解 Redux 完整的工作机制真的让人头疼。特别是作为初学者。
术语太多了!Actions、reducers、action creators、middleware、pure functions、immutability、thunks 等等。
怎么把这些全都与 React 结合起来构建一个可运行的应用?
你可以花几个小时阅读博客以及尝试从复杂的“真实世界”应用中研习以将它拼凑起来。
在本篇 Redux 教程中,我会渐进地解释如何将 Redux 与 React 搭配使用 —— 从简单的 React 开始 —— 以及一个非常简单的 React + Redux 案例。我会解释为什么每个功能都很有用(以及什么情况下做取舍)。
接着我们会看到更加进阶的内容,手把手,直到你全部都理解为止。我们开始吧 :)
请注意:本教程相当齐全。也就意味篇幅着比较长。
Redux 的好处
如果你稍微使用过一段时间的 React,你可能就了解了 props 和单向数据流。数据通过 props 在组件树间向下传递。就像这个组件一样:
image.pngcount
存在 App
的 state 里,会以 prop 的形式向下传递:
要想数据向上传递,需要通过回调函数实现,因此必须首先将回调函数向下传递到任何想通过调用它来传递数据的组件中。
image.png你可以把数据想象成电流,通过彩色电线连接需要它的组件。数据通过线路上下流动,但是线路不能在空气中贯穿 —— 它们必须从一个组件连接到另一个组件。
多级传递数据是一种痛苦
迟早你会陷入这类场景,顶级容器组件有一些数据,有一个 4 级以上的子组件需要这些数据。这有一个 Twitter 的例子,所有头像都圈出来了:
image.png我们假设根组件 App
的 state 有 user
对象。该对象包含当前用户头像、昵称和其他资料信息。
为了把 user
数据传递给全部 3 个 Avatar
组件,必须要经过一堆并不需要该数据的中间组件。
获取数据就像用针在采矿探险一样。等等,那根本没有意义。无论如何,这很痛苦。也被称为 “prop-drilling”。
更重要的是,这不是好的软件设计。中间组件被迫接受和传递他们并不关心的 props。也就意味着重构和重用这些组件会变得比原本更难。
如果不需要这些数据的组件根本不用看到它们的话不是很棒吗?
Redux 就是解决这个问题的一种方法。
相邻组件间的数据传递
如果你有些兄弟组件需要共享数据,React 的方式是把数据向上传到父组件中,然后再通过 props 向下传递。
但这可能很麻烦。Redux 会为你提供一个可以存储数据的全局 "parent",然后你可以通过 React-Redux 把兄弟组件和数据 connect
起来。
使用 React-Redux 将数据连接到任何组件
使用 react-redux
的 connect
函数,你可以将任何组件插入 Redux 的 store 以及取出需要的数据。
学习 Redux,从简单 React 开始
我们将采用增量的方法,从带有组件 state 的简单 React 应用开始,一点点添加 Redux,以及解决过程中遇到的错误。我们称之为“错误驱动型开发” :)
这是一个计数器:
image.png这本例中,Counter 组件有 state,包裹着它的 App 是一个简单包装器。
Counter.js
import React from 'react';
class Counter extends React.Component {
state = { count: 0 }
increment = () => {
this.setState({
count: this.state.count + 1
});
}
decrement = () => {
this.setState({
count: this.state.count - 1
});
}
render() {
return (
<div>
<h2>Counter</h2>
<div>
<button onClick={this.decrement}>-</button>
<span>{this.state.count}</span>
<button onClick={this.increment}>+</button>
</div>
</div>
)
}
}
export default Counter;
快速回顾一下,它是如何运行的:
-
count
state 存储在Counter
组件 - 当用户点击 "+" 时,会调用按钮的
onClick
处理器执行increment
函数。 -
increment
函数会更新 state 的 count 值。 - 因为 state 改变了,React 会重新渲染
Counter
组件(以及它的子元素),这样就会显示新计数值。
在 React 应用中添加 Redux
yarn add \ # or npm i --save
redux \
react-redux \
redux-thunk \
redux-devtools-extension \
react-router-dom
redux vs react-redux
redux
给你一个 store,让你可以在里面保存 state,取出 state,以及当 state 发生改变时做出响应。但那就是它所有能做的事。
实际上是 react-redux
把各个 state 和 React 组件连接起来。
没错:redux
对 React 根本不了解。
redux
库可以脱离 React 应用使用。它可以和 Vue、Angular 甚至后端的 Node/Express 应用一起使用。
Redux 有全局唯一 Store
我们将首先从 Redux 中的一小部分入手:store。
我们已经讨论过 Redux 怎样在一个独立 store 里保存你应用的 state。以及怎样提取 state 的一部分把它作为 props 嵌入你的组件。
你会经常看到 "state" 和 "store" 这两个词互换使用。技术上来讲,state 是数据,store 是保存数据的地方。
因此:作为我们从简单的 React 到 Redux 重构的第一步,我们要创建一个 store 来保持 state。
创建 Redux Store
Redux 有一个很方便的函数用来创建 stores,叫做 createStore
。很合逻辑,嗯?
我们在 index.js
中创建一个 store。引入 createStore
然后像这样调用:
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux'
import { createStore } from 'redux';
import Counter from './Counter'
const store = createStore();
const App = () => (
<Provider store={store}>
<Counter/>
</Provider>
);
ReactDOM.render(<App />, document.getElementById('root'));
这样会遇到 "Expected the reducer to be a function." 错误。
image.pngStore 需要一个 Reducer
因此,有件关于 Redux 的事:它并不是非常智能。
你可能期待通过创建一个 store,它会给你的 state 一个合适的默认值。或许是一个空对象?
但是并非如此。这里没有约定优于配置。
Redux 不会对你的 state 做任何假设。它可能是一个 object、number、string,或者任何你需要的。这取决于你。
我们必须提供一个返回 state 的函数。这个函数被称为 reducer(我们马上就知道为什么了)。那么我们创建一个非常简单的 reducer,把它传给 createStore
,然后看会发生什么:
index.js
function reducer(state, action) {
console.log('reducer', state, action);
return state;
}
const store = createStore(reducer);
修改完后,打开控制台
你应该可以看到类似这样的日志信息:
image.png(INIT 后面的字母和数字是 Redux 随机生成的)
注意在你创建 store 的同时 Redux 如何调用你的 reducer。(为了证实这点:调用 createStore
之后立即输出 console.log
,看看 reducer 后面会打印什么)
同样注意 Redux 如何传递了一个 undefined
的 state
,同时 action 是一个有 type
属性的对象。
我们稍后会更多地讨论 actions。现在,我们先看看 reducer。
Redux Reducer 是什么?
"reducer" 术语看起来可能有点陌生和害怕,但是本节过后,我认为你会同意如下观点,正如俗话所说的那样,“只是一个函数”。
你用过数组的 reduce
函数吗?
它是这样用的:你传入一个函数,遍历数组的每一个元素时都会调用你传入的函数,类似 map
的作用 —— 你可能在 React 里面渲染列表而对 map
很熟悉。
你的函数调用时会接收两个参数:上一次迭代的结果,和当前数组元素。它结合当前元素和之前的 "total" 结果然后返回新的 total 值。
结合下面例子看会更加清晰明了:
var letters = ['r', 'e', 'd', 'u', 'x'];
// `reduce` 接收两个参数:
// - 一个用来 reduce 的函数 (也称为 "reducer")
// - 一个计算结果的初始值
var word = letters.reduce(
function(accumulatedResult, arrayItem) {
return accumulatedResult + arrayItem;
},
''); // <-- 注意这个空字符串:它是初始值
console.log(word) // => "redux"
你给 reduce
传入的函数理所应当被叫做 "reducer",因为它将整个数组的元素 reduces 成一个结果。
Redux 基本上是数组 reduce
的豪华版。前面,你看到 Redux reducers 如何拥有这个显著特征:
(state, action) => newState
含义:它接收当前 state
和一个 action
,然后返回 newState
。看起来很像 Array.reduce
里 reducer 的特点!
(accumulatedValue, nextItem) => nextAccumulatedValue
Redux reducers 就像你传给 Array.reduce 的函数作用一样!:) 它们 reduce 的是 actions。它们把一组 actions(随着时间)reduce 成一个单独的 state。不同之处在于 Array 的 reduce 立即发生,而 Redux 则随着正运行应用的生命周期一直发生。
给 Reducer 一个初始状态
记住 reducer 的职责是接收当前 state
和一个 action
然后返回新的 state。
它还有另一个职责:在首次调用的时候应该返回初始 state。它有点像应用的“引导页”。它必须从某处开始,对吧?
惯用的方式是定义一个 initialState
变量然后使用 ES6 默认参数给 state
赋初始值。
既然要把 Counter
state 迁移到 Redux,我们先立马创建它的初始 state。在 Counter
组件里,我们的 state 是一个有 count
属性的对象,所以我们在这创建一个一样的 initialState。
index.js
const initialState = {
count: 0
};
function reducer(state = initialState, action) {
console.log('reducer', state, action);
return state;
}
如果你再看下控制台,你会看到 state
打印的值为 {count: 0}
。那就是我们想要的。
所以这告诉我们一条关于 reducers 的重要规则。
Reducers 重要规则一:reducer 绝不能返回 undefined。
通常 state 应该总是已定义的。已定义的 state 是良好的 state。而undefined不那么好(并且会破坏你的应用)。
Dispatch Actions 来改变 State
是的,一下来了两个名字:我们将 "dispatch" 一些 "actions"。
什么是 Redux Action?
在 Redux 中,具有 type
属性的普通对象就被称为 action。就是这样,只要遵循这两个规则,它就是一个 action:
{
type: "add an item",
item: "Apple"
}
This is also an action:
{
type: 7008
}
Here's another one:
{
type: "INCREMENT"
}
Actions 的格式非常自由。只要它是个带有 type
属性的对象就可以了。
为了保证事务的合理性和可维护性,我们 Redux 用户通常给 actions 的 type 属性赋简单字符串,并且通常是大写的,来表明它们是常量。
Action 对象描述你想做出的改变(如“增加 counter”)或者将触发的事件(如“请求服务失败并显示错误信息”)。
尽管 Actions 名声响亮,但它是无趣的,呆板的对象。它们事实上不做任何事情。反正它们自己不做。
为了让 action 做点事情,你需要 dispatch。
Redux Dispatch 工作机制
我们刚才创建的 store 有一个内置函数 dispatch
。调用的时候携带 action, reducer 会被redux调用,,并收到action参数(然后 reducer 的返回值会更新 state)。
我们在 store 上试试看。
index.js
const store = createStore(reducer);
store.dispatch({ type: "INCREMENT" });
store.dispatch({ type: "INCREMENT" });
store.dispatch({ type: "DECREMENT" });
store.dispatch({ type: "RESET" });
在你的 CodeSandbox 中添加这些 dispatch 调用然后检查控制台
image.png每一次调用 dispatch
最终都会调用 reducer!
同样注意到 state 每次都一样?{count: 0}
一直没变。
这是因为我们的 reducer 没有作用于那些 actions。不过很容易解决。现在就开始吧。
在 Redux Reducer 中处理 Actions
为了让 actions 做点事情,我们需要在 reducer 里面写几行代码来根据每个 action 的 type
值来对应得更新 state。
有几种方式实现。
你可以创建一个对象来通过 action 的 type 来查找对应的处理函数。
或者你可以写一大堆 if/else 语句
if(action.type === "INCREMENT") {
...
} else if(action.type === "RESET") {
...
}
或者你可以用一个简单的 switch
语句,也是我下面采用的方式,因为它很直观,也是这种场景的常用方法。
尽管有些人讨厌 switch
,如果你也是 —— 随意用你喜欢的方式写 reducers 就好 :)
下面是我们处理 actions 的逻辑:
index.js
function reducer(state = initialState, action) {
console.log('reducer', state, action);
switch(action.type) {
case 'INCREMENT':
return {
count: state.count + 1
};
case 'DECREMENT':
return {
count: state.count - 1
};
case 'RESET':
return {
count: 0
};
default:
return state;
}
}
试一下然后在控制台看看会输出什么。
image.png快看!count
变了!
我们准备好把它连接到 React 了,在此之前让我们先谈谈这段 reducer 代码。
如何保持纯 Reducers
另一个关于 reducers 的规则是它们必须是纯函数。也就是说不能修改它们的参数,也不能有副作用(side effect)。
Reducer 规则二:Reducers 必须是纯函数。
“副作用(side effect)”是指对函数作用域之外的任何更改。不要改变函数作用域以外的变量,不要调用其他会改变的函数(比如 fetch
,跟网络和其他系统有关),也不要 dispatch actions 等。
技术角度来看 console.log
是副作用(side effect),但是我们忽略它。
最重要的事情是:不要修改 state
参数。
这意味着你不能执行 state.count = 0
、state.items.push(newItem)
、state.count++
及其他类型的变动 —— 不要改变 state
本身,及其任何子属性。
你可以把它想成一个游戏,你唯一能做的事就是 return { ... }
。这是个有趣的游戏。开始会有点恼人。但是通过练习你会变得更好。
全部规则
必须返回一个 state,不要改变 state,不要 connect 每一个组件,要吃西兰花,11 点后不要外出…这简直没完没了。就像一个规则工厂,我甚至不知道那是什么。
是的,Redux 就像一个霸道的父母。但它是出于爱。函数式编程的爱。
Redux 建立在不变性的基础上,因为变化的全局 state 是一条通往废墟之路。
你试过在全局对象里面保存你的 state 吗?起初它还很好。美妙并且简单。任何东西都能接触到 state 因为它一直是可用的并且很容易更改。
然后 state 开始以不可预测的方式发生改变,想要找到改变它的代码变得几乎不可能。
为了避免这些问题,Redux 提出了以下规则。
- State 是只读的,唯一修改它的方式是 actions。
- 更新的唯一方式:dispatch(action) -> reducer -> new state。
- Reducer 函数必须是“纯”的 —— 不能修改它的参数,也不能有副作用(side effect)。
如何在 React 中使用 Redux
此时我们有个很小的带有 reducer
的 store
,当接收到 action
时它知道如何更新 state
。
现在是时候将 Redux 连接到 React 了。
要做到这一点,要用到 react-redux
库的两样东西:一个名为 Provider
的组件和一个 connect
函数。
通过用 Provider
组件包装整个应用,如果它想的话,应用树里的每一个组件都可以访问 Redux store。
在 index.js
里,引入 Provider
然后用它把 App
的内容包装起来。store
会以 prop 形式传递。
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux'
import { createStore } from 'redux';
import Counter from './Counter'
const initialState = {
count: 0
};
function reducer(state = initialState, action) {
console.log('reducer', state, action);
switch(action.type) {
case 'INCREMENT':
return {
count: state.count + 1
};
case 'DECREMENT':
return {
count: state.count - 1
};
case 'RESET':
return {
count: 0
};
default:
return state;
}
}
const store = createStore(reducer);
store.dispatch({ type: "INCREMENT" });
store.dispatch({ type: "INCREMENT" });
store.dispatch({ type: "DECREMENT" });
store.dispatch({ type: "RESET" });
const App = () => (
<Provider store={store}>
<Counter/>
</Provider>
);
ReactDOM.render(<App />, document.getElementById('root'));
这样之后,Counter
, Counter
的子元素,以及子元素的子元素等等——所有这些现在都可以访问 Redux stroe。
但不是自动的。我们需要在我们的组件使用 connect
函数来访问 store。
React-Redux Provider 工作机制
Provider
可能看起来有一点点像魔法。它在底层实际是用了 React 的 Context 特性。
Context 就像是连接每个组件的秘密通道,使用 connect
就可打开秘密通道的大门。
为 Redux 准备 Counter 组件
现在 Counter 有了内部 state。我们打算把它干掉,为从 Redux 以 prop 方式获取 count
做准备。
移除顶部的 state 初始化,以及 increment
和 decrement
内部调用的 setState
。然后,把 this.state.count
替换成 this.props.count
。
Counter.js
import React from 'react';
class Counter extends React.Component {
// state = { count: 0 }; // 删除
increment = () => {
/*
// 删除
this.setState({
count: this.state.count + 1
});
*/
};
decrement = () => {
/*
// 同样删除
this.setState({
count: this.state.count - 1
});
*/
};
render() {
return (
<div className="counter">
<h2>Counter</h2>
<div>
<button onClick={this.decrement}>-</button>
<span className="count">{
// 把 state:
//// this.state.count
// 替换成:
this.props.count
}</span>
<button onClick={this.increment}>+</button>
</div>
</div>
);
}
}
export default Counter;
现在 increment
和 decrement
是空的。我们会很快再次填充它们。
你会注意到 count 消失了 —— 它确实应该这样,因为目前还没有给 Counter
传递 count
prop。
连接组件和 Redux
要从 Redux 获取 count
,我们首先需要在 Counter.js 顶部引入 connect
函数。
Counter.js
import { connect } from 'react-redux';
然后我们需要在底部把 Counter 组件和 Redux 连接起来:
Counter.js
// 添加这个函数:
function mapStateToProps(state) {
return {
count: state.count
};
}
// 然后把:
// export default Counter;
// 替换成:
export default connect(mapStateToProps)(Counter);
之前我们只导出了组件本身。现在我们用 connect
函数调用把它包装起来,这样我们就可以导出已连接的 Counter。至于应用的其余部分,看起来就像一个常规组件。
然后 count 应该就重新出现了!直到我们重新实现 increment/decrement,它是不会变化的。
如何使用 React Redux connect
你可能注意到这个调用看起来有点……奇怪。为什么是 connect(mapStateToProps)(Counter)
而不是 connect(mapStateToProps, Counter)
或者 connect(Counter, mapStateToProps)
?它做了什么?
这样写是因为 connect
是一个高阶函数,它简单说就是当你调用它时会返回一个函数。然后调用返回的函数传入一个组件时,它会返回一个新(包装的)组件。
它的另一个名称是 高阶组件 (简称 "HOC")。HOCs 过去曾有过一些糟糕的新闻,但它仍然是一个相当有用的模式,connect
就是一个很好的例子。
Connect
做的是在 Redux 内部 hook,取出整个 state,然后把它传进你提供的 mapStateToProps
函数。它是个自定义函数,因为只有你知道你存在 Redux 里面的 state 的“结构”。
mapStateToProps 工作机制
connect
把整个 state 传给了你的 mapStateToProps
函数,就好像在说,“嘿,告诉我你想从这堆东西里面要什么。”
mapStateToProps
返回的对象以 props 形式传给了你的组件。以上面为例就是把 state.count
的值用 count
prop 传递:对象的属性变成了 prop 名称,它们对应的值会变成 props 的值。你看,这个函数就像字面含义一样定义从 state 到 props 的映射。
顺便说说 —— mapStateToProps
的名称是使用惯例,但并不是特定的。你可以简写成 mapState
或者用任何你想的方式调用。只要你接收 state
对象然后返回全是 props 的对象,那就没问题。
为什么不传整个 state?
在上面的例子中,我们的 state 结构已经是对的了,看起来 mapDispatchToProps
可能是不必要的。如果你实质上复制参数(state)给一个跟 state 相同的对象,这有什么意义呢?
在很小的例子中,可能会传全部 state,但通常你只会从更大的 state 集合中选择部分组件需要的数据。
并且,没有 mapStateToProps
函数,connect
不会传递任何 state。
你可以传整个 state,然后让组件梳理。但那不是一个很好的习惯,因为组件需要知道 Redux state 的结构然后从中挑选它需要的数据,后面如果你想更改结构会变得更难。
从 React 组件 Dispatch Redux Actions
现在我们的 Counter 已经被 connect
了,我们也获取到了 count
值。现在我们如何 dispatch actions 来改变 count?
好吧,connect
为你提供支持:除了传递(mapped)state,它还从 store 传递了 dispatch
函数!
要在 Counter 内部 dispatch action,我们可以调用 this.props.dispatch
携带一个 action。
我们的 reducer 已经准备好处理 INCREMENT
和 DECREMENT
actions 了,那么接下来从 increment/decrement 中 dispatch:
Counter.js
increment = () => {
this.props.dispatch({ type: "INCREMENT" });
};
decrement = () => {
this.props.dispatch({ type: "DECREMENT" });
};
现在我们完成了。按钮应该又重新生效了。
Action 常量
在大部分 Redux 应用中,你可以看到 action 常量都是一些简单字符串。这是一个额外的抽象级别,从长远来看可以为你节省不少时间。
Action 常量帮你避免错别字,action 命名的错别字会是一个巨大的痛苦:没有报错,没有哪里坏掉的明显标志,并且你的 action 没有做任何事情?那就可能是个错别字。
Action 常量很容易编写:用变量保存你的 action 字符串。
把这些变量放在一个 actions.js
文件里是个好办法(当你的应用很小时)。
actions.js
export const INCREMENT = "INCREMENT";
export const DECREMENT = "DECREMENT";
然后你就可以引入这些 action 名称,用它们来代替手写字符串:
Counter.js
import React from "react";
import { INCREMENT, DECREMENT } from './actions';
class Counter extends React.Component {
state = { count: 0 };
increment = () => {
this.props.dispatch({ type: INCREMENT });
};
decrement = () => {
this.props.dispatch({ type: DECREMENT });
};
render() {
...
}
}
Redux Action 生成器是什么?
现在我们已经手写 action 对象。像个异教徒。
如果你有一个函数会为你编写它会怎么样?不要再误写 actinos 了!
我可以告诉你,这很疯狂。手写 { type: INCREMENT }
并保证没有弄乱有多困难?
当你的应用变得越来越大,actions 越来越多,并且这些 actions 开始变得更复杂 —— 要传更多数据而不仅是一个 type
—— action 生成器会帮上大忙。
就像 action 常量一样,但它们不是必须品。这是另一层的抽象,如果你不想在你的应用里面使用,那也没关系。
不过我还是会解释下它们是什么。然后你可以决定你是否有时/总是/绝不想使用它们。
Actions 生成器在 Redex 术语中是一个简单的函数术语,它返回一个 action 对象。就这些 :)
这是其中两个,返回熟悉的 actions。顺便说一句,它们在 action 常量的 "actions.js" 中完美契合。
actions.js
export const INCREMENT = "INCREMENT";
export const DECREMENT = "DECREMENT";
export function increment() {
return { type: INCREMENT };
}
export const decrement = () => ({ type: DECREMENT });
我用了两种不同方式——一个 function
和一个箭头函数——来表明你用哪种方式写并不重要。挑选你喜欢的方式就好。
你可能注意到函数命名是小写的(好吧,如果较长的话会是驼峰命名),而 action 常量会是 UPPER_CASE_WITH_UNDERSCORES
。同样,这也只是惯例。这会让你一眼区分 action 生成器和 action 常量。但你也可以按你喜欢的方式命名。Redux 并不关心。
现在,如何使用 action 生成器呢?引入然后 dispatch 就好了,当然!
Counter.js
import React from "react";
import { increment, decrement } from './actions';
class Counter extends React.Component {
state = { count: 0 };
increment = () => {
this.props.dispatch(increment()); // << 在这使用
};
decrement = () => {
this.props.dispatch(decrement());
};
render() {
...
}
}
关键是要记得调用 action creator()!
不要 dispatch(increment)
🚫
应该 dispatch(increment())
✅
牢记 action 生成器是一个平凡无奇的函数。Dispatch 需要 action 是一个对象,而不是函数。
而且:你肯定会在这里出错并且非常困惑。至少一次,或许很多次。那很正常。我有时也依旧会忘记。
如何使用 React Redux mapDispatchToProps
现在你知道 action 生成器是什么,我们可以讨论又一个级别的抽象。(我知道,我知道。这是可选的。)
你知道 connect
如何传递 dispatch
函数吗?你知道你是如何厌倦一直敲 this.props.dispatch
并且它看起来多么混乱?(跟我来)
写一个 mapDispatchToProps
对象(或者函数!但通常是对象)然后传给你要包装组件的 connect
函数,你将收到这些 action 生成器作为可调用 props。看代码:
Counter.js
import React from 'react';
import { connect } from 'react-redux';
import { increment, decrement } from './actions';
class Counter extends React.Component {
increment = () => {
// 我们可以调用 `increment` prop,
// 它会 dispatch action:
this.props.increment();
}
decrement = () => {
this.props.decrement();
}
render() {
// ...
}
}
function mapStateToProps(state) {
return {
count: state.count
};
}
// 在这个对象中, 属性名会成为 prop 的 names,
// 属性值应该是 action 生成器函数.
// 它们跟 `dispatch` 绑定起来.
const mapDispatchToProps = {
increment,
decrement
};
export default connect(mapStateToProps, mapDispatchToProps)(Counter);
这很棒,因为它把你从手动调用 dispatch
中解放出来。
如何使用 Redux Thunk 获取数据
既然 reducers 应该是“纯”的,我们不能做在 reducer 里面做任何 API 调用或者 dispatch actions。
我们也不能在 action 生成器里面做这些事!
但是如果我们把 action 生成器返回一个可以处理我们工作的函数会怎样呢?就像这样:
function getUser() {
return function() {
return fetch('/current_user');
};
}
越界了,Redux 不支持这种 actions。固执的 Redux 只接受简单对象作为 actions。
这时就需要 redux-thunk 了。它是个中间件,基本是 Redux 的一个插件,它可以使 Redux 处理像上面 getUser()
那样的 actions。
你可以像其他 action 生成器一样 dispatch 这些 "thunk actions":dispatch(getUser())
。
"thunk" 是什么?
thunk是一类函数的别名,主要特征是对另外一个函数添加了一些额外的操作,类似装饰器。其主要用途为延迟函数执行(惰性求值)或者给一个函数执行前后添加一些额外的操作。
"thunk" 是指被其它函数作为返回值的函数。
在 Redux 术语中,它是一个返回值为函数而非简单 action 对象的 action 生成器,就像这样:
function doStuff() {
return function(dispatch, getState) {
// 在这里 dispatch actions
// 或者获取数据
// 或者该干啥干啥
}
}
从技术角度讲,被返回的函数就是 "thunk",把它作为返回值的就是“action 生成器”。通常我把它们一起称为 "thunk action"。
Action 生成器返回的函数接收两个参数:dispatch
函数和 getState
。
大多数场景你只需要 dispatch
,但有时你想根据 Redux state 里面的值额外做些事情。这种情况下,调用 getState()
你就会获得整个 state 的值然后按需所取。
如何安装 Redux Thunk
使用 NPM 或者 Yarn 安装 redux-thunk,运行 npm install --save redux-thunk
。
然后,在 index.js(或者其他你创建 store 的地方),引入 redux-thunk
然后通过 Redux 的 applyMiddleware
函数把它应用到 store 中。
import thunk from 'redux-thunk';
import { createStore, applyMiddleware } from 'redux';
function reducer(state, action) {
// ...
}
const store = createStore(
reducer,
applyMiddleware(thunk)
);
必须确保 thunk
包装在 applyMiddleware
调用里面,否则不会生效。不要直接传 thunk
。
结合 Redux 请求数据的例子
我们从免费开放接口https://jsonplaceholder.typicode.com/posts获取全部posts数据
actions/postsActions.js
export const GET_POSTS_BEGIN = 'GET POSTS'
export const GET_POSTS_SUCCESS = 'GET_POSTS_SUCCESS'
export const GET_POSTS_FAILURE = 'GET_POSTS_FAILURE'
export const getPosts = () => ({ type: GET_POSTS_BEGIN })
export const getPostsSuccess = posts => ({
type: GET_POSTS_SUCCESS,
payload: posts,
})
export const getPostsFailure = () => ({ type: GET_POSTS_FAILURE })
export function fetchPosts() {
return async dispatch => {
dispatch(getPosts())
try {
const response = await fetch('https://jsonplaceholder.typicode.com/posts')
const data = await response.json()
dispatch(getPostsSuccess(data))
} catch (error) {
dispatch(getPostsFailure())
}
}
}
fetch('https://jsonplaceholder.typicode.com/posts')
是实际上请求数据的部分。然后我们在它前后分别做了一些 dispatch
调用。
Dispatch Action 来获取数据
要开始调用并且实际获取数据,我们需要 dispatch fetchPosts
action。
在哪里调用呢?
如果某一特定的组件需要数据,最好的调用地方通常是在组件刚刚加载之后,也就是它的 componentDidMount
生命周期函数。
如何给 Redux Actions 命名
获取数据的 Redux actions 通常使用标准三连:BEGIN、SUCCESS、FAILURE。这不是硬性要求,只是惯例。
BEGIN/SUCCESS/FAILURE 模式很棒,因为它给你提供钩子来跟踪发生了什么 —— 比如,设置 "loading" 标志为 "true" 以响应 BEGIN 操作,在 SUCCESS 或 FAILURE 之后设为 false
。
而且,与 Redux 中的其他所有内容一样,这个也是一个惯例,如果你不需要的话可以忽略掉。
在你调用 API 之前,dispatch BEGIN action。
调用成功之后,你可以 dispatch SUCCESS 数据。如果请求失败,你可以 dispatch 错误信息。
有时最后一个调用 ERROR。其实调用什么一点也不重要,只要你保持一致就好。
接收到 GET_POSTS_SUCCESS
action 返回的post数据后,我们写一个 reducer 把它存进 Redux store 中。开始请求时把 loading
标志设为 true,失败或者完成时设为 false。
reducers/postsReducer.js
import * as actions from '../actions/postsActions'
export const initialState = {
loading: false,
hasErrors: false,
posts: [],
}
export default function postsReducer(state = initialState, action) {
switch (action.type) {
case actions.GET_POSTS_BEGIN:
return { ...state, loading: true }
case actions.GET_POSTS_SUCCESS:
return { posts: action.payload, loading: false, hasErrors: false }
case actions.GET_POSTS_FAILURE:
return { ...state, loading: false, hasErrors: true }
default:
return state
}
}
最后,我们需要把post数据传给展示它们并且也负责请求数据的 PostsPage
组件。
pages/PostsPages.js
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { fetchPosts } from '../actions/postsActions'
import { Post } from '../components/Post'
class PostsPage extends Component{
componentDidMount() {
this.props.fetchPosts()
}
render(){
const {loading, posts, hasErrors} = this.props;
const renderPosts = () => {
if (loading) return <p>Loading posts...</p>
if (hasErrors) return <p>Unable to display posts.</p>
return posts.map(post => <Post key={post.id} post={post} excerpt />)
}
return (
<section>
<h1>Posts</h1>
{renderPosts()}
</section>
)
}
}
const mapStateToProps = state => ({
loading: state.posts.loading,
posts: state.posts.posts,
hasErrors: state.posts.hasErrors,
})
export default connect(mapStateToProps,{fetchPosts})(PostsPage)
components/Post.js
import React from 'react'
export const Post = ({ post, excerpt }) => (
<article>
<h2>{post.title}</h2>
<p>{post.body}</p>
</article>
)
我指的是带有 state.posts.<whatever>
的数据而不仅仅是 state.<whatever>
,因为我假设你可能会有不止一个 reducer,每一个都处理各自的 state。为了确保这样,我们可以写一个 reducers/index.js
文件把它们放在一起:
reducers/index.js
import { combineReducers } from 'redux'
import postsReducer from './postsReducer'
const rootReducer = combineReducers({
posts: postsReducer,
})
export default rootReducer
然后,当我们创建 store 我们可以传递这个“根” reducer:
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux'
import { createStore, applyMiddleware } from 'redux';
import Counter from './Counter'
import thunk from 'redux-thunk';
import PostsPage from './pages/PostsPage'
import rootReducer from './reducers'
const store = createStore(
rootReducer,
applyMiddleware(thunk)
);
store.dispatch({ type: "INCREMENT" });
store.dispatch({ type: "INCREMENT" });
store.dispatch({ type: "DECREMENT" });
store.dispatch({ type: "RESET" });
const App = () => (
<Provider store={store}>
<Counter/>
<PostsPage />
</Provider>
);
ReactDOM.render(<App />, document.getElementById('root'));
最终结果:
image.png
Redux 中错误处理
这里的错误处理比较轻量,但是对大部分调用 API 的 actions 来说基本结构是一样的。基本观点是:
- 当调用失败时,dispatch 一个 FAILURE action
- 通过设置一些标志变量和/或保存错误信息来处理 reducer 中的 FAILURE action。
- 把错误标志和信息(如果有的话)传给需要处理错误的组件,然后根据任何你觉得合适的方式渲染错误信息。
能避免重复渲染吗?
这确实个常见问题。是的,它会不止一次触发渲染。
它首先会渲染空 state,然后再渲染 loading state,接着会再次渲染展示posts。可怕!三次渲染!(如果你直接跳过 "loading" state 就可以把渲染次数将为两次)
你可能会担心不必要的渲染影响性能,但是不会:单次渲染非常快。如果你在开发的应用肉眼可见的慢的话,分析一下找出慢的原因。