现在我们已经讨论过了Redux的基础概念,我们会开始做一些更有趣的事情。
在本章中,我们会通过另一个项目引导您继续学习,同时详细解释每个过程。
那么,这次会用什么项目呢?
我已经有一个完美的项目。
请考虑如下的原型:
哦,它看起来就像前面的例子 - 但有一些变化。这次我们会考虑用户的操作。当我们点击任何按钮时,我们想要更新应用程序的状态,如下面的GIF所示:
这与以前的例子不同。在这种情况下,用户正在执行某些影响应用程序状态的操作。在前一个示例中,我们所做的只是显示应用程序的初始状态,而不考虑用户操作。
Redux Action是什么?
当你走进银行,柜员收到你的动作,即你进入银行的意图。在我们之前的例子中,它就是WITHDRAWAL_MONEY
。让钱离开银行金库的唯一办法是如果你让柜员知道你的动作或者意图。
现在,对于Redux Reducer也是一样。
与纯React中的setState()
不同,你更新一个Redux应用程序的状态的唯一方式是如果你让REDUCER知道你的意图。
不过怎么才能让REDUCER知道呢?
通过分发动作!
在真实世界中,你知道你想执行的实际动作。你可能将它写在一张单据上,把它交给银行柜员。
用Redux的工作方式也差不多。唯一的挑战是,在一个Redux应用中如何描述一个动作。很显然没法通过在柜台上讲或写在单据上。
好吧,有个好消息。
动作是用普通Java对象准确描述的。没有别的。
只有一件事情要知道。一个动作必须有一个type
字段。这个字段描述动作的意图。
在银行故事中,如果我们要描述对银行的动作,它看起来应该是这样:
{
type: "withdraw_money"
}
就是这样,真的。
一个Redux动作是被描述为一个普通对象。
请看看上面的动作。
你认为只用type
字段就能准确描述你想取钱的动作?
嗯,我不这样认为。你想取多少钱?
很多时候,你的动作需要一些额外的数据来完整描述。考虑下面的动作。我认为这是一个描述得更好的动作。
{
type: "withdraw_money",
amount: "$4000"
}
现在,有足够的信息来描述action。为了举例起见,请忽略该action可能包含的所有其他详细信息,例如您的银行帐号。
除了type
字段以外,Redux Action的结构实际上是取决于你自己。
不过,常见的方式是如下所示的有一个type
字段和一个payload
字段:
{
type: " ",
payload: {}
}
type
字段描述action,描述action的所有其它所需数据/信息被放在payload
对象中。
例如:
{
type: "withdraw_money",
payload: {
amount: "$4000"
}
}
嗯,是的! 这就是一个action。
在Reducer中处理对Action的响应
现在你已经成功地理解了Action是什么,重要的是要看到它们在实际意义上是如何变得有用的。
前面我说reducer带有两个参数,一个是state
,另一个是action
。
这里是一个简单的Reducer的样子:
function reducer(state, action) {
//返回新状态
}
action
作为第二个参数被传入Reducer。不过在函数本身内我们还没有用它做任何事情。
为处理传入reducer的action,你通常要在reducer内写一个switch
语句,像这样:
function reducer (state, action) {
switch (action.type) {
case "withdraw_money":
//do something
break;
case "deposit-money":
//do something
break;
default:
return state;
}
}
有些人似乎不喜欢switch
语句,但它基本上是一个if/else
,用于表示单个字段上的可能值。
上述代码会切换action type
,基于传入的action的类型做一些事情。从技术上讲,do something
这个位置需要返回一个新状态。
让我进一步解释。
假设你在某个网页上有两个假定的按钮,按钮#1和按钮#2,并且你的state对象看起来像这样的:
{
isOpen: true,
isClicked: false,
}
当按钮#1被点击时,你想开关isOpen
字段。在React应用程序上下文中,解决方案很简单。一旦该按钮被点击,你会这样做:
this.setState({isOpen: !this.state.isOpen})
同样,假设当按钮#2被点击时,你想更新isClicked
字段。解决方案一样简单:
this.setState({isClicked: !this.state.isClicked})
不错。
而用Redxu应用程序,你不能用setState()
来更新被Redux管理的state对象。
你必须先分发一个action。
我们假设action是如下这样的:
#1 :
{
type: "is_open"
}
#2 :
{
type: "is_clicked"
}
在一个Redux应用程序中,每个action都要流经reducer。
所有的action都是。所以,在本例中,action #1和action #2都会通过同一个reducer。
在这种情况下,reducer如何区分二者?
是的,你猜对了。
通过切换action.type
,我们可以处理毫无麻烦地处理两个action。
我的意思是:
function reducer (state, action) {
switch (action.type) {
case "is_open":
return; //返回新状态
case "is_clicked":
return; //返回新状态
default:
return state;
}
}
现在你看到为什么switch
语句是有好处的。所有的action都会流经reducer。因此,分开处理每个action类型很重要。
在下一节中,我们会继续创建如下的迷你应用程序任务:
检查应用程序中的action
正如我早前所解释的,只要有一个更新应用程序状态的意图,就必须分发一个action
。
不管这个意图是被用户点击、或者timeout事件,或者甚至AJAX请求发起的,规则是一样的。你必须分发一个action。
对于本应用程序也是一样的。
因为我们想更新应用程序的状态,所以只要有任意按钮被点击,我们就不惜分发一个action。
首先,我们来描述一下action。
尝试一下,看看你是否理解了它。
以下是我想出来的:
对于React按钮:
{
type: "SET_TECHNOLOGY",
text: "React"
}
对React-Redux按钮:
{
type: "SET_TECHNOLOGY",
text: "React-redux"
}
对Elm按钮:
{
type: "SET_TECHNOLOGY",
text: "Elm"
}
很简单,对吧?
注意,三个action的type
字段都是相同的。这是因为三个按钮做的是一样的事情。如果他们是银行的顾客,那么他们都会存钱,不过钱的数目不同。然后,action的type
将是DEPOSIT_MONEY
,不过amount
字段不同。
此外,你还会注意到,action type的取值都被写成大写字母。这是刻意的。它不是强制的,不过是Redux社区中相当流行的风格。
希望你现在明白我是如何提出这些action的。
介绍Action Creator
看看我们上面创建的action。你会注意到我们是在重复一些事情。
首先,它们都有同一个type
字段。如果我们必须在不同的地方分发这些action,就必须导出复制它们。这就不怎么好了。特别是因为保持代码DRY是一个好主意。
我们可以为此做些什么吗?
当然!
欢迎,Action Creator。
Redux都是些花哨的名字,对吧?Reducer、Action,现在又是Action Creator :)
让我来解释一下这是什么玩意。
Action Creator只是帮助你创建action的函数。仅此而已。它们是返回action对象的函数。
在我们的特定示例中,我们可以创建一个函数,它将接受一个text
参数并返回一个action,如下所示:
function setTechnology (text) {
return {
type: "SET_TECHNOLOGY",
text: text
}
}
现在我们不必为到处复制代码而烦恼。我们可以随时调用setTechnology
action creator,并且我们会得到一个返回的action!
多好用的函数啊。
用ES6,上面我们创建的action creator可以被简化为:
const setTechnology = text => ({ type: "SET_TECHNOLOGY", text });
现在,搞定了。
汇总
在前面的小节中,我已经分别讨论了构建更高级的Hello World应用程序所需的所有重要组件。
现在,我们来把这些东西放在一起,创建应用程序。激动吧?
首先,我们谈谈文件夹结构。
当你去一个银行时,柜员肯定坐在他们自己的小隔间或者办公室里。金库也在一个安全的房间保证安全。出于好的理由,事情会更有组织。每个人都在他们自己的空间里。
对Redux也是如此。
让redux应用的主要角色生活在他们自己的文件夹/目录中是一种常见的做法。
角色,我是指reducer
、action
和store
。
在您的应用程序目录中创建三个不同的文件夹是常见的,并根据这些角色给每个文件夹命名。
这并非必须必然的,你自己觉得如何组织你的项目。不过,对于大型应用程序来说,这确实是一个不错的实践。
我们现在会重构我们的当前应用程序目录。创建三个新文件夹:reducers
、store
、actions
。
现在你应该有个如下所示的组件结构:
在每个文件夹中,创建一个index.js
文件。这回称为每个Redux角色(reducers、store和actions)的入口。我称它们为角色,就像动画角色一样。它们是Redux系统的主要组件。
现在,我们会重构之前《第二章:第一个Redux应用程序》的应用程序,让它使用这种新目录结构。
store/index.js
import { createStore } from "redux";
import reducer from "../reducers";
const initialState = { tech: "React " };
export const store = createStore(reducer, initialState);
这跟我们之前的差不多。唯一的区别是,store现在是创建在它自己的index.js
文件中,就像不同的Redux角色有单独的隔间/办公室一样。
现在,如果我们在应用程序内的任何地方需要store,我们可以如同在import store from "./store";
中一样,安全地导入store。
尽管如此,这个特殊例子的App.js
与之前还是略有不同:
App.js
import React, { Component } from "react";
import HelloWorld from "./HelloWorld";
import ButtonGroup from "./ButtonGroup";
import { store } from "./store";
class App extends Component {
render() {
return [
<HelloWorld key={1} tech={store.getState().tech} />,
<ButtonGroup key={2} technologies={["React", "Elm", "React-redux"]} />
];
}
}
export default App;
不同之处是什么?
第四行中,store是从它自己的小隔间
中导入的。另外,现在有一个<ButtonGroup />
组件,该组件带有一个technologies
数组属性,并且生成三个按钮。ButtonGroup
组件处理Hello World
文件下三个按钮的渲染。
此外,你可能注意到,App
组件返回一个数组。这是React 16
的一个标志。使用React 16,你不必将相邻JSX
元素包在一个div
中。如果你想的话,你可以用数组,不过要传入一个key
属性给数组中的每个元素。
App.js
组件就是这样。
ButtonGroup
组件的实现很简单。代码如下:
ButtonGroup.js
import React from "react";
const ButtonGroup = ({ technologies }) => (
<div>
{technologies.map((tech, i) => (
<button
data-btn={tech}
key={`btn-${i}`}
className="hello-btn"
>
{tech}
</button>
))}
</div>
);
export default ButtonGroup;
ButtonGroup
是一个无状态组件,它带有一个technologies数组属性。
它用map
遍历这个数组,为数组中的每种技术渲染一个<button></button>
。
在本例中,传入的按钮数组是["React", "Elm", "React-redux"]
。
生成的按钮有几个属性。很明显className
是为样式用途。key
是为了防止
渲染没有key属性的多个条目时讨厌的React警告。天哪,那个错误每次都困扰我:(
最后,每个button
上也都有一个data-btn
属性。这称为数据属性。这是存储一些没有任何视觉表示的额外信息的一种方式。它使得从元素中获取某些值稍微容易一些。
完全渲染后的按钮将会看起来是这样的:
<button
data-tech="React"
key="btn-1"
className="hello-btn"> React </button>
现在,所有东西都可以正确渲染了,但是点击按钮,依然是什么都没有发生。
这是因为,我们还没有提供点击处理器。下面我们搞定它。
在render()
函数内,我们设置一个onClick
事件处理器:
<div>
{technologies.map((tech, i) => (
<button
data-tech={tech}
key={`btn-${i}`}
className="hello-btn"
onClick={dispatchBtnAction}
>
{tech}
</button>
))}
</div>
不错。下面我们编写dispatchBtnAction
。
不要忘记这个处理器的唯一目标是在点击发生时分发一个action。
比如,当你点击React按钮时,分发action:
{
type: "SET_TECHNOLOGY",
tech: "React"
}
如果你点击REact-Redux按钮,分发这个action:
{
type: "SET_TECHNOLOGY",
tech: "React-redux"
}
因此,dispatchBtnAction
函数是这样的:
function dispatchBtnAction(e) {
const tech = e.target.dataset.tech;
store.dispatch(setTechnology(tech));
}
嗯,看得懂上面的代码吗?
e.target.dataset.tech
会获取按钮上的数据属性集data-tech
。因此,tech
会保存该文本的值。
store.dispatch()
是在Redux分发action的方式,而setTechnology()
是我们之前写的action creator!
function setTechnology (text) {
return {
type: "SET_TECHNOLOGY",
text: text
}
}
我已经继续并在下面的插图中加了一些注释,以便您理解代码。
正如你已经知道的,store.dispatch
需要有一个action对象即可。不要忘记setTechnology
action creator。它接收传进来的按钮文本,并返回所需的action。
此外,按钮的tech
是从按钮的dataset中获取的。看到了吧,这就是为什么在每个按钮上有一个data-tech
属性的原因。这样我们就很容易从每个按钮中获取tech。
现在我们分发了正确的action。现在我们能否知道这样是不是就能按预期工作呢?
Action分发。这事是否能成?
首先,有一个简短的测验问题。在点击一个按钮并因此分发一个action后,Redux中接下来会发生什么?哪些Redux角色开始起作用?
简单。当你用WITHRAW_MONEY
动作击中银行时,你去见谁?银行柜员,是的。
这里也一样。当分发action时,要流经reducer。
为证明这一点,我会记录任何进入reducer的action。
reducers/index.js
export default (state, action) => {
console.log(action);
return state;
};
然后reducer返回应用程序的新状态。在我们的特定例子中,我们只返回相同的初始state
。
reducer中有了console.log()
后,下面我们看看在我们点击时,会发生什么。
哦耶!
当按钮被点击时,action就被记录了。这证明action实际通过了reducer。太棒了!
不过还有一件事。应用程序一启动,就有一个奇怪的action也被记录下来。它看起来像这样子:
{type: "@@redux/INITu.r.5.b.c"}
这是什么呢?
那么,不要太在乎你自己。这是Redux在设置您的应用程序时传递的一个action。它通常被称为Redux的init action
,当Redux用应用程序的初始状态初始化您的应用程序时,它会被传递给reducer。
现在,我们确认action实际上经过了Reducer。很棒!
虽然这很令人兴奋,但你带着借取款请求前往银行柜员的唯一原因是因为你需要钱。如果reducer没有采取我们传入的action,并用我们的action做一些事情,那么它的价值是什么?
让Reducer变得有价值
到目前为止,我们所从事的reducer并没有做任何特别聪明的事情。这就像银行柜员对我们的WITHDRAW_MONEY
意图不做任何事情一样。
我们是期望reducer做什么呢?
目前,如下是创建STORE
时我们传入createStore()
的initialState
。
const initialState = { tech: "React" };
export const store = createStore(reducer, initialState);
当用户点击任意按钮时,因而就传递一个action给reducer,我们希望reducer返回的新状态应该在这里就有了action文本。
这就是我所指的意思。
当前状态是{ tech: "React"}
。
假如有一个类型为SET_TECHNOLOGY
的新action,文本是React-Redux
:
{
type: "SET_TECHNOLOGY",
text: "React-Redux"
}
你希望新状态是什么?
是{tech: "React-Redux"}
。
我们分发一个action的唯一的理由是因为我们想要一个新的应用程序状态!
就像我之前提到过的,在一个reducer内,处理不同action行为的常见方式是使用JavaScript的switch
语句,如下所示:
export default (state, action) => {
switch (action.type) {
case "SET_TECHNOLOGY":
//do something.
default:
return state;
}
};
现在我们切换action type
。但是为什么呢?
是的,如果你想见一个银行柜员,你可能心里可能有很多种不同的action。
你可能想WITHDRAW_MONEY
,或者DEPOSIT_MONEY
,或者也许只是想SAY_HELLO
。
银行柜员是聪明的,所以他们领会你的action,并根据你的意图作出回应。
这正是我们用Reducer所做的事情。
switch
语句检测action的type
。
你想做什么?取钱、存钱,还是别的。。。
之后,我们就处理我们希望的已知的case。现在,只有一个case
,就是SET_TECHNOLOGY
。
并且对于default,确保只返回应用程序的state
。
目前为止还挺好。
银行柜员(reducer
)现在理解我们的action。不过,他们还没有给我们任何钱(state
)。
我们在case
内做点事情。
如下是更新版本的reducer,是个实际给我们钱的reducer :)
export default (state, action) => {
switch (action.type) {
case "SET_TECHNOLOGY":
return {
...state,
text: action.tech
};
default:
return state;
}
};
噢,是啊!
你明白我在这里做什么吗?
我会在下一节解释发生了什么。
永远不要在reducer内改变状态
当从reducer返回state
时,起初有些事情可能会让你失望。不过,如果那你已经写过好的React代码,那么你应该熟悉这一点。
你不应该在reducer中修改接收到的state
,而是应该总是返回state的一个新的副本。
从技术上讲,你永远不应该像这样做:
export default (state, action) => {
switch (action.type) {
case "SET_TECHNOLOGY":
state.tech = action.tech;
return state;
default:
return state;
}
};
这正是为什么我写的reducer返回这个的原因:
return {
...state,
tech: action.tech
};
我没有改变(或者修改)从reducer接收到的state,而是返回一个新对象。这个对象有之前state对象的所有属性。多亏了ES6扩展运算符...state
。不过,tech
字段被更新为action带来的action.text
。
此外,你写的每个Reducer应该是一个没有副作用的纯函数 - 没有API调用或者更新一个函数作用域外的值。
明白没有?
希望你明白了。
现在,银行柜员不再忽视我们的action。他们仙子啊实际上给我们钞票了!
做了这个之后,点击按钮。现在它起作用没有?
唉,它依然没有起作用。文本并没有更新。
这次到底是出了什么问题?
订阅Store更新
当你拜访银行时,让银行柜员知道你的意图WITHDRAWAL
action,并且成功收到你的钱了 - 那么下一步是什么?
很可能,您会收到通过电子邮件/短信或其他移动通知的警告,说您已经完成了交易,并且你的新帐户余额是如此如此。
如果你没有收到手机通知,肯定会收到某种个人收据,以表明你的帐户已成功进行交易。
OK,注意流程。一个action被发起,你收到钱,你得到一个成功交易的警告。
我们的Redux代码似乎有个问题。
一个action已经被成功发起了,我们已经收到钱(state),不过,状态成功更新的警告在哪里?
我们什么都没有得到。
好了,这里有一个解决方案。你可以订阅接收来自银行的交易通知,或者通过电子邮件/短信。
对于Redux也是一样。如果你想更新,就得订阅它们。
但是怎么订阅呢?
Redux store,不管你创建什么store,都有一个subscribe()
方法调用,像这样:store.subscribe()
。
这函数的名字不错啊!
传入store.subscribe()
的参数是一个函数,只要有状态更新,这个函数就会被调用。
请记住传给store.subscribe()
的参数应该是一个函数。
现在我们就来利用这一点。
想想看。状态更新后,我们希望或期望什么?我们期望重新渲染,对吧?
所以,状态已经更新了。Redux,请用新的状态值重新渲染应用程序。
下面我们来看看index.js
中应用程序被渲染的地方。
这是我们得到的。
ReactDOM.render(<App />, document.getElementById("root"))
这就是渲染整个应用程序的那一行。它采用<App />
,并在DOM中渲染它。root
ID是特定的。
首先,我们将这抽出到一个函数中。
看这段代码:
const render = function() {
ReactDOM.render(<App />, document.getElementById("root"))
}
因为现在这是在一个函数中,所以我们必须调用该函数来渲染应用程序。
const render = function() {
ReactDOM.render(<App />, document.getElementById("root"))
}
render()
现在,<App />
会像之前一样被渲染。
如果用ES6语法的话,函数可以变得更简单一些。
const render = () => ReactDOM.render(<App />, document.getElementById("root"));
render();
将<App/>
的渲染包在一个函数内,意味着我们现在可以像这样,订阅对store的更新:
store.subscribe(render);
这里render
是对<App />
的全部渲染逻辑,就是我们刚刚重构完的那一个。
你理解这里发生了什么,对吧?
任何时候,只要有一个对store的成功更新,<App/>
现在都会被用新的状态值重新渲染。
为清晰起见,如下是<App/>
组件:
class App extends Component {
render() {
return [
<HelloWorld key={1} tech={store.getState().tech} />,
<ButtonGroup key={2} technologies={["React", "Elm", "React-redux"]} />
];
}
}
只要重新渲染发生了,第四行上的store.getState()
现在就会获取到更新后的状态。
现在我们来看看应用程序是否按预料的那样工作。
耶!没问题了,我知道我们可以做到这一点!
我们成功地分发了一个action,从银行柜员那里收到钱,然后订阅接收通知。完美!
有关使用store.subscribe()的重要说明
既然已经到了这里,我得提醒一下使用store.subscribe()
的几个注意事项。这是一个低层的Redux API。
在生产中,主要是出于性能方面的原因,在处理大型应用程序时,您可能会使用诸如react-redux
之类的绑定。现在,作为我们的学习目的,继续使用store.subscribe()
是安全的。
在很久以前看过的一篇最漂亮的PR评论中,Dan Abramov在一个Redux应用程序示例中,说:
新的Counter Vanilla示例旨在消除Redux需要Webpack、React、热重载、saga、Action Creator、常量、Babel、npm、CSS模块、装饰器、流利的拉丁语、Egghead订阅、博士学位或超越期望的普通巫师等级。
我的观念也是一样。
在学习Redux时,尤其是如果你刚刚开始,你可以去掉尽可能多的额外部分。
先学会走,然后才能跑得更快。
好的,我们完成了吗?
是的,从技术上讲,我们是完成了。不过,还有一件事我很乐意告诉你。我将调出我的浏览器Devtools并启用paint-flashing。
现在,当我们点击并更新应用的状态时,请注意屏幕上出现的绿色闪烁。绿色闪烁代表被浏览器引擎重新绘制或重新渲染的应用程序的部分。
看一下:
正如你所看到的,即使每次进行状态更新时都会调用render()
函数,但并不是整个应用程序都会被重新渲染。只有具有新状态值的组件才会被重新渲染。本例中是<HelloWorld />
组件。
还有一件事。
如果应用的当前状态渲染Hello World React
,再次点击React
按钮不会重新渲染,因为状态值是一样的。
很好!
这是React虚拟DOM的Diff
算法在这里起作用了。如果你了解一些React,你以前肯定听说过这。
所以,是的。我们完成了本节!我很乐意解释这一点。我希望你也享受阅读。
总结
对于一个简单的应用程序来说,本章比你预期的要长。但没关系。你现在拥有更多关于Redux如何工作的知识。
以下是在本章学到的一些内容:
- 与纯React中的
setState()
不同,更新Redux应用程序状态的唯一方法是分发一个action。 - 一个action用普通JavaScript对象精确描述,但是它必须有一个
type
字段。 - 在Redux应用中,每个action都会流经reducer。
- 通过使用
switch
语句,你可以在Reducer内处理不同的action类型。 - Action Creator只是返回action对象的函数。
- redux应用程序的主要角色有在自己的文件夹/目录中是一种常见做法。
- 你不应该在Reducer中修改接收的
state
,而是应该总是返回该state的一个新副本。 - 要订阅store更新,请使用
store.subscribe()
方法。
练习
好了,现在该你来做一些很酷的事情了。
- 在练习文件中,我已经设置了一个简单的React应用程序模仿用户的银行应用程序。
好好看看上面的原型。除了用户能查看他们的总余额之外,他们还可以执行取钱动作。
用户的name
和balance
存在应用程序的state中。
{
name: "Ohans Emmanuel",
balance: 1559.30
}
你需要做两件事情:
- 将应用程序的状态重构为只被Redux所管理。
- 处理取钱action,从而实际消耗用户的余额(即,点击按钮,余额减少)。
你必须只通过Redux做这事。
- 如下图像是作为React应用程序创建的一个时间计数器。
state对象看起来像这样:
{
days: 11,
hours: 31,
minutes: 27,
seconds: 11,
activeSession: "minutes"
}
点击increase
或者decrease
按钮应该更新计数器中显示的值。
你需要做两件事情:
- 将应用程序的状态重构为只被Redux所管理。
- 处理
increase
和decrease
按钮,以实际影响计数器上显示的时间。