我希望这是学习 React Redux
最简单的入门教程。
我一开始学习 Redux
的时候我想找到最浅显易懂的入门教程。
尽管有大量的资源,但是我对于 Redux
的一些概念依旧搞不清楚。
我知道什么是 state
,但是 actions
, action creators
和 reducers
又是什么鬼?我被搞晕掉了。
然后我也搞不明白 React
和 Redux
是怎样结合起来的。
前端时间开始着手写 React Redux
教程的过程中我理解了许多。
我通过写这篇教程自学 Redux
的基本概念。我也希望这可以帮助所有正在学习 React
和 Redux
的人。
适用人群
如果满足下列条件,那么本教程正是你要找的:
- 良好的
Javascript, ES6, React
基础 - 你希望以最简单的方式学习
Redux
你可以学到什么
通过这篇教程你可以学到:
Redux
是什么- 怎样结合
React
使用Redux
搭建 React
开发环境
看这篇教程你需要有扎实的 Javascript, ES6, React
基础。
开始之前,让我们来简单搭建一个 React
开发环境。
你可以选择 webpack 3
或者 Parcel
。
如果选用 Parcel
你可以参考这个链接 Setting up React with Parcel ,或者从我的Github上克隆仓库:
|
|
如果你想用 webpack 3
可以参考这个教程 How to set up React, Webpack 3, and Babel ,或者从我的Github上克隆仓库:
|
|
什么是 state ?
理解 Redux
之前你首先需要理解什么是 state
。
如果你曾经写过 React
的 state
, 应该不足为奇。
我猜你应该写过如下的 React
状态组件:
|
|
上述组件是基于 Javascript ES6 class
的。
每个 React
状态组件持有自己的 state
。在 React
中 state
持有数据。组件渲染这组数据展示给用户。
React
提供了一个 setState
方法来更新组件内部的 state
。
我们通常这么用:
- 从组件内部持有的
state
中获取数据渲染 - 通过
React
的setState
方法更新state
Redux 解决了什么问题 ?
应用规模比较小的时候,在 React
组件内部维护 state
很方便。
但在复杂一点的业务场景下这么做就不太合适了。组件中会填满各种管理更新 state
的方法,臃肿不堪。前端不应该知道业务逻辑。
那么,有没有其它管理 state
的方式呢?Redux
就是其中的一个解决方案。
Redux
解决了一开始我们可能还不太清楚的问题:它为每一个 React
组件明确提供它所需要的 state
。
Redux
统一在一个地方维护 state
。
通过 Redux
,获取和管理 state
的逻辑也和 React
隔离开来。
这中方式的好处可能还不太明显。当你用上它后就会发现它的威力。
下一节内容我们聊一下为什么以及什么时候我们需要用 Redux
。
我要学 Redux 么 ?
想学习 Redux
但是没搞定?
Redux
的确吓坏了许多初学者。但是不必因此而感到担心。
Redux
没有那么难。关键是:不要随波逐流地去使用它。
你开始学习 Redux
应该是出于自发的动机和热情。
我学习 Redux
是因为:
- 我100%地渴望知道
Redux
是怎么工作的 - 我想要提升我的
React
技能 - 我想成为更牛X的
React
开发者 React/Redux
当下是黄金组合Redux
是一种思想,它和框架无关。学习了它可以在任何地方(比如Vue JS
,Angular
中)使用。
我要用 Redux 么 ?
没有 Redux
构建复杂的 React
应用也是可行的。只是有一些成本。
用 Redux
也是有一定成本的:它加入了另外一个抽象层。但是我我认为是利大于弊的。
另一个困惑初学者的问题是:我怎么判断什么时候我需要使用 Redux
呢?
如果你没有经验判定是否需要用 Redux
来管理 state
,我有如下建议使用的场景:
- 多个
React
组件需要访问同一个state
但是又没有父子关系 - 需要通过
props
向下向多个组件传递state
如果还是找不到感觉,别怕,我也一样。
Dan Abramov 说过『 Flux就像眼镜:你需要的时候你自然会知道 』。
事实上对于我而言也正是如此。
Dan Abramov 写了篇很棒的文章来帮助我们理解: Redux
的使用场景
dev.to 上也有一篇 Dan 关于这一问题的 讨论
看一下 Mark Erikson 整理的 Redux
学习资源
顺便说下,Mark 的博客被视为 Redux
的最佳实践
Dave Ceddia 也分享过关于 Redux
干了什么?以及它的使用场景 的话题。
在深入学习之前,花些时间去搞明白 Redux
解决了什么问题。然后再决定要不要学。
要清楚在小型的应用中使用 Redux
没什么好处。只有在大型应用下才能发挥它的威力。但不管如此,学习 Redux
解决问题的思路总不是什么坏事。
下一节,我们介绍一下内容:
Redux
的基本原则Redux
和React
怎么结合
了解 Redux store
Actions
, Reducers
我都知道。但有件事我搞不明白:这些动态的片段是怎么结合到一起的?
有小黄人相助不成?
是 store
把所有流动的片段协调地结合到 Redux
中。在 Redux
中 store
就像人类的大脑:这是种魔法。
Redux store
是一切的基础:整个应用的 state
都在 store
中维护。
如果我们把 Redux
的工作模式想象成人类的大脑。 state
就相当于存在于大脑(store
)中的一段回忆。
所以开始使用 Redux
的时候我们需要创建一个 store
来存放 state
。
切换到你的开发目录,安装 Redux
:
|
|
|
|
创建一个目录来存放 store
相关逻辑:
|
|
在 src/js/store
中创建 index.js
并初始化:
|
|
createStore
就是创建 Redux store
的方法。
createStore
接收一个 reducer
作为第一个参数,就是代码中的 rootReducer
。
你也可以传递一个初始 state
进 createStore
。 但是大多数时候你不必这么做。传递初始 state
在服务端渲染时比较有用。当然,state
来自 reducers
。
现在我们知道 reducer
做了啥了。reducer
生成了 state
。 state
并不是手动创建的。
带着这些概念我们继续我们第一个 Redux reducer
。
了解 Redux reducers
初始 state
在 服务端渲染 中很有用,它必须完全从 reducers
中产生。
reducer
是个什么鬼?
reducer
就是个 javascript 函数。它接收两个参数:当前的 state
和 action
。
Redux
的第三条原则中强调:state
必须是不可变的。这也是为什么 reducer
必须是一个纯函数。纯函数就是指给有明确的输入输出的函数。
在传统的 React
应用中我们通过 setState
方法来改变 state
。 在 Redux
中不可以这么做。
创建一个 reducer
不难,就是个包含两个参数的 javascript 普通函数。
在我们的例子中,我们将创建一个简单的 reducer
, 并向他传递初始 state
作为第一个参数。第二个参数我们将提供一个 action
。到目前为止 reducer
除了返回初始状态什么都不会做。
创建reducer 根目录
|
|
然后在 src/js/reducers
创建一个名为 index.js
的新文件:
|
|
我承诺这是篇尽可能简明的教程。所以我们仅有一个 reducer
:它除了返回初始 state
什么都不处理。
注意下初始 state
是怎样作为 默认参数 传递的。
下一节我们将结合一个 action
来让这个应用更加有趣。
了解 Redux actions
reducers
无疑是 Redux
中最重要的概念。reducers
用来生产应用的 state
。
但是 reducers
怎么知道什么时候去生产下一个 state
呢?
Redux
的第二条原则中说:改变 state
的唯一方式就是向 store
发送一个信号 。 这个 信号
指的就是 action
。
怎么改变这个不可更改的状态呢?你不能。返回的 state
是当前 state
的副本结合新数据后的一个全新的 state
。这点必须要明确。
比较欣慰的是 actions
就是简单的 javascript 对象。它长这样:
|
|
每一个 action
都要有一个 type
属性来描述要对 state
做怎样的改变。
你也可以指定一个叫 payload
的属性。在上面的例子中, payload
指代一篇新的文章。reducer
接下来将把这篇新文章加到 state
中去。
最佳实践是将每一个 action
都通过函数包裹起来。这个函数就是 action creator
。
让我们来把这些都串起来创建一个简单的 action
。
创建 actions 目录:
|
|
在 src/js/actions
目录中创建名为 index.js
的文件:
|
|
type 属性就是个简单的字符串。reducer
将根据这个字符串决定怎么处理生成下一个 state
。
由于字符串很容易重复产生冲突,所以最好在常量中统一定义 action types
。
这个方法可以避免一些很难排查的错误。
创建一个新目录:
|
|
然后在 src/js/constants
目录下创建名为 action-types.js
的文件:
然后打开 src/js/actions/index.js
用常量来替换字符串:
|
|
我们进一步完成了一个可运行的 Redux
应用。 接下来让我们重构下我们的 reducer
吧!
重构 reducer
进入下一步之前,让我们总结一下 Redux
的主要概念:
Redux store
就像大脑:它负责将所有流动的片段有机地整合进Redux
中- 应用中的
state
在store
中以唯一且不可变对象的形式存在 - 一旦
store
接收到一个action
他就会触发一个reducer
reducer
返回下一个state
reducer
是怎样构成的?
reducer
是一个 javascript 函数,它接收两个参数:state
和 action
。
reducer
一般会包含一个 switch 语句(傻一点的话也可以用:if / else)。
reducer
根据 action type
产生下一个 state
。此外,当没有匹配到 action type
时它至少也要返回初始 state
。
当 action type
匹配到一个 case 分支的时候, reducer
将计算下一个 state
并返回一个全新的对象。下面是个例子:
|
|
我们之前创建的 reducer
只返回了初始 state
什么都没做。让我们做点什么。
打开 src/js/reducers/index.js
按如下例子更新 reducer
:
|
|
发现了什么?
尽管代码逻辑没有错,但这违背了 Redux
的主要原则: 不可变 。
Array.prototype.push
不是一个纯函数,它改变了原来的数组。
解决的方式很简单。用 Array.prototype.concat
替换 Array.prototype.push
来保持原始数组是不可变的。
|
|
还不够!可以用 扩展运算符 优化一下:
|
|
上述例子中初始 state
完全没受干扰。
最初的文章数组并没有改变。
初始 state
对象也没有改变。返回的 state
是初始 state
的一个副本。
在 Redux
中有两个关键点来防止类似的改变。
- 针对数组可以用 concat(), slice(), 和 …操作符
- 针对对象可以用 Object.assign() and …操作符
扩展运算符 在 webpack 3 中还是个实验性功能。需要安装一个babel插件来防止语法错误:
|
|
打开 .babelrc
更改配置:
|
|
小贴士: reducer
随着应用的增长会变得臃肿。你可以拆分 reducer
进不同的函数,然后通过 combineReducers
将他们结合起来。
下一节我们将通过 console 控制台把玩一下 Redux
。搞起!
Redux store 中的方法
这节内容将会很快过完,我保证。
我想借助浏览器的控制台让你快速理解下 Redux
是怎样工作的。
Redux
本身是一个很小的类库 (2KB)。它暴露了 一些简单的API 让我们来管理 state
。其中最重要的方法就是:
我们将借助浏览器的管理控制台试验上述方法。
我们需要创建全局变量将我们先前创建的 store
和 action
暴露出来。
打开 src/js/index.js
按如下所示更新代码:
|
|
然后运行一下:
|
|
在浏览器中打开 http://localhost:8080/ 并按 F12
打开管理控制台。
由于我们全局暴露了 store
, 所以我们可以进入它的方法。试一下!
首先访问当前 state
:
|
|
输出:
|
|
没有文章。事实上我们还没有更新初始 state
。
让这变得更有趣些我们可以通过 subscribe 方法监听 state
的变化。
subscribe 方法接收一个回调函数,当 action
触发的时候该回调函数就会执行。触发 action
就意味着通知 store
我们想改变 state
。
通过如下操作注册回调函数:
|
|
要想改变 state
我们需要触发一个 action
。要触发一个 action
我们就需要调用 dispatch 方法 。
我们已经有了一个 action
:addArticle 用来向 state
中新增文章。
让我们触发一下这个 action
:
|
|
执行上述代码后你将看到:
|
|
验证一下 state
有没有变:
|
|
输出会是:
|
|
这就是个最简单的 Redux
原型。
难么?
稍微花点时间练习一下 Redux
的三个方法。在控制台中玩一把:
这就是开始入坑所需要知道的全部内容。
有信心进入下一步了么?让我们把 React
和 Redux
串起来吧。
把 React 和 Redux 结合起来
学习了 Redux
后我发现它并没有想象中那么复杂。
我知道我可以通过 getState
方法获取当前 state
;通过 dispatch
触发一个 action
; 通过 subscribe
监听 state
的变化。
目前我还不知道怎么将 React
和 Redux
组合起来使用。
我问自己:我需要在 React component
中调用 getState
方法么? 在 React component
我怎么去触发 action
? 等等一些列问题。
Redux
是框架无关的。你可以在纯 Javascript
中使用它。或者结合 Angular
,Redux
一起使用。 有许多第三方库可以实现把 Redux
和任何你喜欢的框架结合起来使用。
对于 React
而言, react-redux
就是这么一个第三方库。
首先让我们通过下面的命令安装一下:
|
|
为了演示 React
和 Redux
是如何协同工作的,我们将构建一个超级简单的应用。这个应用由如下组件组成:
- 一个 App 组件
- 一个展示文章的 List 组件
- 一个新增文章的 Form 组件
(这是个玩具应用,仅仅用来展示文章列表,新增文章条目。但是可以算一个学习 Redux
不错的开端。)
react-redux
react-redux
是一个 React
和 Redux
绑定库。这个类库将 React
和 Redux
高效地连接起来。
react-redux
的 connect
方法做了什么呢? 毫无疑问是将 React
的组件和 Redux
的 store
连接起来了。
你可以根据需要向 connect
方法传递两个或者三个参数。需要知道的最基本的东西就是:
mapStateToProps
函数mapDispatchToProps
函数
react-redux
中 mapStateToProps
做了什么呢?就像它的名字一样:它将部分 Redux state
和 React
组件中的 Props
连接了起来。通过这层连接,React
组件就可以从 store
中获取它所需要的 state
了。
react-redux
中 mapDispatchToProps
又做了什么呢?和 mapStateToProps
类似,只不过它是连接的 actions
。它将 部分 Redux action
和 React
组件中的 Props
连接了起来。通过这层连接,React
组件就可以从触发 actions
了。
都清楚了么?如果没有,停下来重读一遍。我知道许多内容需要时间消化。我要着急,早晚会搞明白的。
接下来的部分我们就要大显生手了!
App 组件 和 Redux store
我们知道 mapStateToProps
将部分 Redux state
和 React
组件中的 Props
连接了起来。你可能想问:连接 React
和 Redux
这么做就够了吗?不,还不够。
要连接 React
和 Redux
我们还要用到 Provider。
Provider
是 react-redux
提供的一个高阶组件。
直白地说,就是 Provider
将整个 React
应用封装起来,是它能够感知到整个 Redux store
的存在。
为什么这么做?我们知道在 Redux
中 store
管理着一切。React
必须通过 store
来访问 state
, 触发 actions
。
了解了以上这些,就打开 src/js/index.js
,修改如下:
|
|
看到没有?Provider
将整个应用包裹起来。并将 store
作为属性传入。
现在让我们创建一个 App 组件。没什么特别的:App 组件导入一个 List 组件并渲染。
创建一个目录来存放组件:
|
|
在 src/js/components
目录下创建名为 App.js
的文件。
|
|
花时间看下组件的构成:
|
|
然后继续创建 List 组件。
List 组件 和 Redux state
目前为止我们没做特殊的操作。
但是 List 组件需要和 Redux store
产生交互。
简单总结一下:连接 React 组件
和 Redux
的关键就是 connect
方法。
Connect 方法接收至少一个参数。
由于我们想让 List 获取文章列表,因此我们需要让 state.articles
和组件连接起来。怎么做呢?用 mapStateToProps
参数来实现。
在 src/js/components
目录下创建名为 List.js
的文件。写入如下内容:
|
|
List 组件接收一个名为 articles
的属性, 它是一个 articles
数组的副本。这个数组寄生于我们之前创建的 Redux state
内。它来自 reducer
:
|
|
然后我们就可以用这个属性在 JSX 中生成一个文章列表了:
|
|
小提示 可以用 React PropTypes
对属性进行校验。
最后组件被导出为 List。List 是无状态组件和 Redux store
连接之后的产物。
无状态组件没有内部 state 。数据是通过属性传入的。
依旧很困惑?我也是。理解 connect 的工作原理需要花点时间。别害怕,慢慢来。
我建议你停下来花点时间看下 connect 和 mapStateToProps 。
搞懂了我们就进入下一章的学习。
Form 组件 和 Redux actions
Form 组件比 List 组件稍微复杂一点。它是用于创建文章的表单。
另外它是一个状态组件。
状态组件是指在组件内部维护自身 state 的组件 。
状态组件?我们一直说要用 Redux
来管理 state
。 为什么现在又说要让 Form 自己去维护 state
?
即使在使用Redux的时候,使用有状态的组件也是完全可以的,两者并不冲突。
并不是所有的 state
都必须在 Redux
中管理。
在这个例子中我不想其他任何组件知道 Form 组件内部的 state
。
这么做很好。
这个组件做什么事呢?
该组件包含了一些在表单提交时更新本地 state
的逻辑。
另外它接收一个 Redux action
作为属性传入其中。用这种方式它可以通过触发 addArticle
action
来更新全局的 state
。
在 src/js/components
下建一个名为 Form.js
的文件。它看起来像下面这样:
|
|
怎么描述这个组件呢?除了 mapDispatchToProps 和 connect 它就是标准的 React
的那套东西。
mapDispatchToProps 将 Redux actions
和 React 属性
连接起来。这样组件就可以向外发送 action
了。
你可以看下 action
是怎样在 handleSubmit 方法中被触发的:
|
|
最后组件被导出为 Form 。 Form 就是组件和 Redux store
连接之后的产物。
注意:当mapStateToProps不存在的时候,connect 的第一个参数必须为null,就像例子中的 Form 一样。 否则,你会得到 TypeError: dispatch is not a function.
的警告。
我们的组件部分就都结束了。
更新下 App , 把 Form 组件加进来:
|
|
运行一下:
|
|
你将看到如下界面:
没什么特别的,但可以展示 React
和 Redux
是如何工作的。
左边的 List 组件和 Redux store 相连。它将在你新建文章的时候刷新渲染。
哈哈!
你可以在 这里 查看源码。
总结
我希望你可以从这篇教程中学到一些东西。我尽力把例子写的足够简单。我希望可以在下面的评论中倾听你的反馈。
Redux 有许多模板和移动部件。别灰心。拿起来把玩,花点时间去吸收它所有的概念。
我逐步从零开始慢慢地理解了 Redux 。相信你也可以做到。
当然,也请花点时间想清楚为什么需要在你的项目中引用 Redux。
无论怎样:学习 Redux 都是 100% 值得的。
Redux 并不是状态管理的唯一方式。Mobx是另一个有趣的可选方案。我也在关注 Apollo Client。谁将胜出?只有时间知道答案。
函数式编程的一些资源
Redux 使大多数初学者感到害怕,是因为它是围绕函数式编程和纯函数展开的。
我只能是推荐一些资源给大家,因为函数式编程超出了本指南的范围。 下面是一些函数式编程和纯函数相关的资源:
Redux 中的异步 actions
我不确定谈论异步 actions 是否合适。
大多数 Redux 初学者都很难仅学习纯粹的 Redux。 在 Redux 中处理复杂的异步 action 还是比较费劲的。
当你了解了 Redux 的核心概念后可以去读一下 Matt Stow 写的 A Dummy’s Guide to Redux and Thunk in React。这是篇关于 Redux 怎样用 redux-thunk 处理 API 请求的非常不错的介绍。
谢谢阅读,码得愉快 ~