在这个系列的第二篇文章中,我们将加强对Redux的理解,并在我们已经知道的基础上进行。我们将首先创建一个现实的Redux应用--联系人列表,它比基本的计数器更复杂。这将帮助你加强对我在上一个教程中介绍的单存储和多还原器概念的理解。稍后我们将讨论将Redux状态与React应用程序绑定,以及从头开始创建项目时应考虑的最佳做法。
不过,如果你没有读过第一篇文章也没关系--只要你知道Redux的基础知识,应该还是可以跟上的。教程的代码可以在repo中找到,你可以把它作为一个起点。
使用Redux创建一个联系人列表
我们将建立一个具有以下功能的基本联系人列表。
- 显示所有联系人
- 搜索联系人
- 从服务器上获取所有联系人
- 添加一个新的联系人
- 将新的联系人数据推送到服务器
下面是我们的应用程序的样子。

最终产品--联系人列表视图

最终产品--添加联系人视图
在一次拉伸中涵盖所有内容是很难的。所以在这篇文章中,我们将只关注添加新联系人和显示新添加的联系人的Redux部分。从Redux的角度来看,我们将初始化状态,创建存储,添加还原器和动作等。
在下一个教程中,我们将学习如何连接React和Redux,并从React前端调度Redux动作。在最后一部分,我们将把重点转向使用Redux进行API调用。这包括从服务器上获取联系人,并在添加新的联系人时进行服务器请求。除此之外,我们还将创建一个搜索栏功能,让你搜索所有现有的联系人。
创建一个状态树的草图
你可以从我的GitHub仓库下载react-redux演示程序。克隆该仓库并使用v1分支作为起点。v1分支与create-react-app模板非常相似。唯一的区别是,我添加了一些空目录来组织Redux。这里是目录结构。
.
├── package.json
├── public
├── README.md
├── src
│ ├── actions
│ ├── App.js
│ ├── components
│ ├── containers
│ ├── index.js
│ ├── reducers
│ └── store
└── yarn.lock
另外,你也可以从头开始创建一个新项目。无论哪种方式,你都需要在开始之前安装一个基本的 react boilerplate 和 redux。
最好是先有一个状态树的粗略草图。在我看来,从长远来看,这将为你节省很多时间。下面是一个可能的状态树的粗略草图。
const initialState = {
contacts: {
contactList: [],
newContact: {
name: '',
surname: '',
email: '',
address: '',
phone: ''
},
ui: {
//All the UI related state here. eg: hide/show modals,
//toggle checkbox etc.
}
}
}
我们的商店需要有两个属性--contacts 和ui 。联系人属性负责所有与联系人相关的状态,而ui ,处理特定的UI状态。Redux中没有硬性规定阻止你将ui 对象作为contacts 的子状态。请自由地以对你的应用程序有意义的方式组织你的状态。
联系人属性有两个属性嵌套在它里面--contactlist 和newContact 。contactlist 是一个联系人数组,而newContact 在填写联系人表格时临时存储联系人的详细信息。我将以这个为起点,建立我们超棒的联系人列表应用程序。
如何组织Redux
Redux对你如何构造你的应用程序没有意见。有一些流行的模式,在本教程中,我将简要地谈一谈其中的一些。但你应该选择一种模式并坚持下去,直到你完全理解所有的部分是如何连接在一起的。
你会发现,最常见的模式是Rails式的文件和文件夹结构。你会有几个像下面这样的顶层目录。
- 组件:一个存储哑巴React组件的地方。这些组件并不关心你是否使用Redux。
- 容器:一个存放智能React组件的目录,这些组件将动作分派到Redux商店。redux 和 react 之间的绑定将在这里进行。
- 行动:动作创建者将在这个目录中。
- reducer:每个还原器都有一个单独的文件,你将把所有的还原器逻辑放在这个目录中。
- 存储:用于初始化状态和配置存储的逻辑将放在这里。
下面的图片展示了如果我们遵循这种模式,我们的应用程序可能会是什么样子。

Rails的风格应该适用于小型和中型的应用程序。然而,当你的应用程序增长时,你可以考虑转向域风格的方法或其他与域风格密切相关的流行替代方案。在这里,每个功能都会有一个自己的目录,与该功能(域)相关的所有东西都会在里面。下面的图片比较了这两种方法,左边是Rails-style,右边是domain-style。

现在,继续为组件、容器、存储、还原器和行动创建目录。让我们从存储开始。
单个存储,多个还原器
让我们先创建一个原型,用于 存储器 和 还原器。从我们之前的例子来看,我们的存储将是这样的。
const store = configureStore( reducer, {
contacts: {
contactlist: [],
newContact: { }
},
ui: {
isContactFormHidden: true
}
})
const reducer = (state, action) => {
switch(action.type) {
case "HANDLE_INPUT_CHANGE":
break;
case "ADD_NEW_CONTACT":
break;
case "TOGGLE_CONTACT_FORM":
break;
}
return state;
}
switch语句有三个案例,对应于我们将要创建的三个动作。下面是对这些动作的简单解释。
HANDLE_INPUT_CHANGE:当用户在联系表格中输入新值时,这个动作会被触发。ADD_NEW_CONTACT::当用户提交表单时,这个动作被派发。TOGGLE_CONTACT_FORM:这是一个UI动作,负责显示/隐藏联系人表单。
虽然这种天真的方法可行,但随着应用程序的增长,使用这种技术会有一些不足之处。
- 我们使用的是一个单一的reducer。虽然单个减速器目前听起来还不错,但想象一下,所有的业务逻辑都在一个非常大的减速器之下。
- 上面的代码并没有遵循我们在上一节中讨论的Redux结构。
为了解决单一还原器的问题,Redux有一个方法叫做 [combineReducers](https://redux.js.org/docs/api/combineReducers.html)的方法,可以让你创建多个还原器,然后将它们合并为一个还原函数。combineReducers函数增强了可读性。因此,我打算将还原器分成两个--一个contactsReducer ,一个uiReducer 。
在上面的例子中,configureStore 接受了一个可选的第二个参数,即初始状态。然而,如果我们要拆分减速器,我们可以将整个initialState ,到一个新的文件位置,例如reducers/initialState.js。然后,我们将把initialState 的一个子集导入到每个还原器文件中。
分割减速器
让我们重组我们的代码来解决这两个问题。首先,创建一个名为store/createStore.js的新文件并添加以下代码。
import {configureStore} from 'redux';
import rootReducer from '../reducers/';
/*Create a function called makeStore */
export default function makeStore() {
return configureStore(rootReducer);
}
接下来,在reducers/index.js中创建一个根减速器,如下所示。
import { combineReducers } from 'redux'
import contactsReducer from './contactsReducer';
import uiReducer from './uiReducer';
const rootReducer =combineReducers({
contacts: contactsReducer,
ui: uiReducer,
})
export default rootReducer;
最后,我们需要创建contactsReducer 和uiReducer 的代码。
reducers/contactsReducer.js
import initialState from './initialState';
export default function contactReducer(state = initialState.contacts, action) {
switch(action.type) {
/* Add contacts to the state array */
case "ADD_CONTACT": {
return {
...state,
contactList: [...state.contactList, state.newContact]
}
}
/* Handle input for the contact form.
The payload (input changes) gets merged with the newContact object
*/
case "HANDLE_INPUT_CHANGE": {
return {
...state, newContact: {
...state.newContact, ...action.payload }
}
}
default: return state;
}
}
reducers/uiReducer.js
import initialState from './initialState';
export default function uiReducer(state = initialState.ui, action) {
switch(action.type) {
/* Show/hide the form */
case "TOGGLE_CONTACT_FORM": {
return {
...state, isContactFormHidden: !state.isContactFormHidden
}
}
default: return state;
}
}
当你创建reducer时,始终要记住以下几点:一个reducer需要有一个默认的状态值,而且它总是需要返回一些东西。如果减速器没有遵循这个规范,你会得到错误。
既然我们已经涵盖了大量的代码,让我们来看看我们的方法所做的改变。
- 引入了
combineReducers调用,将分裂的还原器联系在一起。 ui对象的状态将由uiReducer处理,联系人的状态由contactsReducer处理。- 为了保持还原器的纯洁性,我们使用了传播操作符。三个点的语法是传播操作符的一部分。如果你对传播语法不适应,你应该考虑使用Immutability.js这样的库。
- 初始值不再被指定为
createStore的可选参数。相反,我们已经为它创建了一个单独的文件,叫做initialState.js。我们正在导入initialState,然后通过做state = initialState.ui,设置默认状态。
状态初始化
下面是reducers/initialState.js文件的代码。
const initialState = {
contacts: {
contactList: [],
newContact: {
name: '',
surname: '',
email: '',
address: '',
phone: ''
},
},
ui: {
isContactFormHidden: true
}
}
export default initialState;
动作和动作创建器
让我们添加几个动作和动作创建者,用于添加处理表单的变化,添加一个新的联系人,以及切换UI状态。如果你还记得,动作创建者只是返回一个动作的函数。在actions/index.js中添加以下代码。
export const addContact =() => {
return {
type: "ADD_CONTACT",
}
}
export const handleInputChange = (name, value) => {
return {
type: "HANDLE_INPUT_CHANGE",
payload: { [name]: value}
}
}
export const toggleContactForm = () => {
return {
type: "TOGGLE_CONTACT_FORM",
}
}
每个动作都需要返回一个类型属性。类型就像一个钥匙,决定了哪个减速器被调用,以及状态如何被更新以响应该动作。有效载荷是可选的,实际上你可以调用任何你想要的东西。
在我们的例子中,我们已经创建了三个动作。
TOGGLE_CONTACT_FORM 不需要有效载荷,因为每次动作被触发时,ui.isContactFormHidden 的值都会被切换。布尔值的动作不需要有效载荷。
HANDLE_INPUT_CHANGE 动作在表单值改变时被触发。因此,举例来说,想象一下,用户正在填写电子邮件字段。然后该动作接收"email" 和"bob@example.com" 作为输入,交给还原器的有效载荷是一个看起来像这样的对象。
{
email: "bob@example.com"
}
还原器使用这些信息来更新newContact 状态的相关属性。
派遣行动和订阅商店
下一个合乎逻辑的步骤是调度动作。一旦动作被派发,状态就会随之改变。为了调度动作并获得更新的状态树,Redux提供了某些存储动作。它们是
dispatch(action):派遣一个有可能触发状态变化的动作。getState():返回你的应用程序的当前状态树。subscriber(listener):一个变化监听器,当一个动作被派发并且状态树的某些部分被改变时,它就会被调用。
前往index.js文件,导入configureStore 函数和我们之前创建的三个动作。
import React from 'react';
import {render}from 'react-dom';
import App from './App';
/* Import Redux store and the actions */
import configureStore from './store/configureStore';
import {toggleContactForm,
handleInputChange} from './actions';
接下来,创建一个store 对象,并添加一个监听器,在每次派发动作时记录状态树的信息。
const store = getStore();
//Note that subscribe() returns a function for unregistering the listener
const unsubscribe = store.subscribe(() =>
console.log(store.getState())
)
最后,派发一些动作。
/* returns isContactFormHidden returns false */
store.dispatch(toggleContactForm());
/* returns isContactFormHidden returns false */
store.dispatch(toggleContactForm());
/* updates the state of contacts.newContact object */
store.dispatch(handleInputChange('email', 'manjunath@example.com'))
unsubscribe();
使用钩子来调度和订阅商店
如果你在React中使用过基于函数的组件,你很可能已经熟悉了钩子的概念。事实上,你可能已经使用useState 钩子来管理React应用程序中的组件级状态。
本着同样的精神,Redux引入了一些不同的钩子,使我们能够在功能组件内执行通常的任务(调度动作、获取状态等),同时编写最少的代码。这些钩子是在React Redux 7.1中首次加入的。例如,为了调度动作和获取状态树,Redux提供了以下钩子。
[useDispatch](https://react-redux.js.org/api/hooks#usedispatch): 派遣一个有可能触发状态变化的动作[useSelector](https://react-redux.js.org/api/hooks#useselector-examples):获取状态树甚至是状态的一个分支
现在,有了这些钩子,我们可以把上面的代码重构成这样。
// Other imports here
// Import the redux hooks
import { useDispatch, useSelector } from 'react-redux'
// Return the dispatch function from hook
const dispatch = useDispatch()
// Call getStore() to create store object
const store = getStore();
// Get state tree using useSelector
const state = useSelector(state => state)
// Gets the UI branch of the state
const ui = useSelector(state => state.UI)
/* returns isContactFormHidden returns false */
dispatch(toggleContactForm());
/* returns isContactFormHidden returns false */
dispatch(toggleContactForm());
/* updates the state of contacts.newContact object */
dispatch(handleInputChange('email', 'manjunath@example.com'))
unsubscribe();
如果一切工作正常,你应该在开发者控制台看到这个。

这就是了!在开发者控制台,你可以看到Redux存储被记录下来,所以你可以看到它在每个动作后的变化。
总结
我们已经为我们很棒的联系人列表应用程序创建了一个原始的Redux应用程序。我们学习了还原器、拆分还原器以使我们的应用结构更简洁,以及编写用于突变存储的动作。
在文章的最后,我们使用store.subscribe() 方法订阅了商店。从技术上讲,如果你要使用React和Redux,这并不是最好的方法。有更多优化的方法来连接react前端和Redux。我们将在下一个教程中介绍这些。