redux 通过在全局级别设置状态来帮助您管理状态。在之前的教程中,我们很好地了解了 Redux 架构和 Redux 的组成部分,例如 action、action creators、store 和 reducer。
在本系列的第二篇文章中,我们将加深对 Redux 的理解,并在我们已经知道的基础上进行构建。我们将从创建一个实际的 Redux 应用程序开始——一个联系人列表——它比基本计数器更复杂。这将帮助您加强对我在上一篇教程中介绍的单存储和多减速器概念的理解。稍后我们将讨论如何将 Redux 状态与 react 应用程序绑定,以及在从头开始创建项目时应考虑的最佳实践。
但是,如果您还没有阅读第一篇文章也没关系——只要您了解 Redux 基础知识,您应该仍然可以继续阅读。本教程的代码可在 repo 中找到,您可以以此为起点。
使用 Redux 创建联系人列表
我们将构建一个具有以下功能的基本联系人列表:
显示所有联系人
搜索联系人
从服务器获取所有联系人
添加新联系人
将新的联系人数据推送到服务器
这是我们的应用程序的外观:
最终产品——联系人列表查看
最终产品 - 添加联系人视图
一口气涵盖所有内容是困难的。因此,在这篇文章中,我们将只关注 Redux 部分,即添加新联系人并显示新添加的联系人。从 Redux 的角度来看,我们将初始化 state,创建 store,添加 reducer 和 action,等等。
在下一个教程中,我们将学习如何连接 React 和 Redux 并从 React 前端调度 Redux 操作。在最后一部分,我们将把重点转移到使用 Redux进行api调用。这包括从服务器获取联系人并在添加新联系人时发出服务器请求。除此之外,我们还将创建一个搜索栏功能,让您搜索所有现有的联系人。
创建状态树的草图
您可以 从我的 GitHub 存储库下载react-redux 演示应用程序。克隆 repo 并使用 v1 分支作为起点。v1分支与 create-react-app 模板非常相似。唯一的区别是我添加了一些空目录来组织 Redux。这是目录结构。
. ├── package.json ├── public ├── README.md ├── src │ ├── actions │ ├── App.js │ ├── components │ ├── containers │ ├── index.js │ ├── reducers │ └── store └── yarn.lock
或者,您可以从头开始创建一个新项目。无论哪种方式,您都需要在开始之前安装基本的反应样板和 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 中没有硬性规定,pr事件会阻止您将 ui
对象作为contacts
. 随意以对您的应用程序有意义的方式组织您的状态。
联系人属性有两个嵌套在其中的属性contactlist
- 和 newContact
. 这contactlist
是一个联系人数组,而newContact
在填写联系人表单时临时存储联系人详细信息。我将以此为起点来构建我们很棒的联系人列表应用程序。
如何组织 Redux
Redux 对如何构建应用程序没有意见。有一些流行的模式,在本教程中,我将简要介绍其中的一些。但是你应该选择一种模式并坚持下去,直到你完全理解所有部分是如何连接在一起的。
您会发现最常见的模式是 Rails 样式的文件和文件夹结构。您将拥有几个顶级目录,如下所示:
components:存储哑 React 组件的地方。这些组件不关心你是否使用 Redux。
容器:智能 React 组件的目录,用于将操作分派到 Redux 存储。redux 和 react 之间的绑定将在这里发生。
动作:动作创建者将进入此目录。
reducers:每个reducer 都有一个单独的文件,您将把所有reducer 逻辑放在这个目录中。
store:初始化 state 和配置 store 的逻辑会到这里。
下图展示了如果我们遵循这种模式,我们的应用程序会是什么样子:
Rails 风格应该适用于中小型应用程序。但是,当您的应用程序增长时,您可以考虑转向dom ain-style 方法或其他与 domain-style 密切相关的流行替代方案。在这里,每个功能都有自己的目录,与该功能(域)相关的所有内容都将在其中。下图比较了两种方法,左边是 Rails 风格,右边是领域风格。
现在,继续为components、container、store、reducers和action创建目录。让我们从商店开始。
单个存储,多个减速器
让我们先为store和reducer创建一个原型。在我们之前的示例中,这就是我们的商店的外观:
const store = createStore( 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。虽然现在单个 reducer 听起来不错,但想象一下将所有业务逻辑放在一个非常大的 reducer 下。
上面的代码不遵循我们在上一节中讨论的 Redux 结构。
为了解决单个 reducer 的问题,Redux 有一个名为 combineReducers的方法 ,可以让您创建多个 reducer,然后将它们组合成一个 reducer 函数。combineReducers 函数增强了可读性。所以我要把reducer分成两个——acontactsReducer
和a uiReducer
。
在上面的示例中,createStore
接受可选的第二个参数,即初始状态。但是,如果我们要拆分 reducers,我们可以将整个文件移动initialState
到一个新的文件位置,比如reducers/initialState.js。然后我们将一个子集导入 initialState
到每个 reducer 文件中。
拆分减速器
让我们休息一下我们的代码来解决这两个问题。首先,创建一个名为 store/createStore.js的新文件并添加以下代码:
import {createStore} from 'redux'; import rootReducer from '../reducers/'; /*Create a function called configurestore */ export default function configureStore() { return createStore(rootReducer); }
接下来,在reducers/index.js中创建一个根 reducer,如下所示:
import { combineReducers } from 'redux' import contactsReducer from './contactsReducer'; import uiReducer from './uiReducer'; const rootReducer =combineReducers({ contacts: contactsReducer, ui: uiReducer, }) export default rootReducer;
最后,我们需要为contactsReducer
and创建代码uiReducer
。
减速器/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; } }
减速器/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 需要为其状态设置一个默认值,并且它总是需要返回一些东西。如果reducer 没有遵循这个规范,你会得到错误。
由于我们已经涵盖了很多代码,让我们来看看我们对我们的方法所做的更改:
该
combineReducers
调用已被引入以将拆分减速器捆绑在一起。对象的状态
ui
将由 处理,uiReducer
联系人的状态由 处理contactsReducer
。为了保持 reducer 的纯净,使用了扩展运算符。三点语法是扩展运算符的一部分。如果您对扩展语法不满意,则应考虑使用 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" }
reducer 使用此信息来更新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 = configureStore(); //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;
如果一切正常,您应该在开发者控制台中看到这一点。
而已!在开发者控制台中,您可以看到正在记录的 Redux 存储,因此您可以看到它在每次操作后是如何变化的。
概括
我们为我们很棒的联系人列表应用程序创建了一个简单的 Redux 应用程序。我们学习了 reducer,拆分 reducer 以使我们的应用程序结构更清晰,以及编写改变 store 的操作。
- 拆分减速器
- 减速器/contactsReducer.js
- 减速器/uiReducer.js
- 状态初始化