到目前为止,在本书中,我们已经在 React 组件中管理了状态。当状态需要在不同组件之间共享时,我们还使用了 React 上下文。这种方法适用于许多应用程序。React-Redux 帮助我们稳健地处理复杂的状态场景。当用户交互导致对状态的若干更改时(可能有些更改是有条件的),尤其是当交互导致 web 服务调用时,它会发光。当应用程序中有很多共享状态时,这也很好。
在本章中,我们将继续构建 React 商店,添加 React Redux 以帮助我们管理状态交互。我们最终会在商店的标题中添加一个购物篮摘要组件,它会通知用户购物篮中有多少商品。当项目添加到购物篮时,Redux 将帮助我们更新此组件。
在本章的最后一节中,我们将探索一种类似 Redux 的方法来管理组件中的复杂状态。这是在 Redux 存储中管理状态和使用setState
或useState
在组件中管理状态之间的中间地带。
在本章中,我们将学习以下主题:
- 原则和关键概念
- 安装 Redux
- 创建减速器
- 创建操作
- 创建商店
- 将 React 应用程序连接到商店
- 使用 useReducer 管理状态
在本章中,我们将使用以下技术:
-
Node.js 和
npm
:TypeScript 和 React 依赖于这些。我们可以从安装这些 https://nodejs.org/en/download/ 。如果我们已经安装了这些,请确保npm
至少是 5.2 版 -
Visual Studio 代码:我们需要一个编辑器来编写 React 和 TypeScript 代码,可以从安装 https://code.visualstudio.com/ 。我们还需要在 VisualStudio 代码中安装 TSLint(由 egamma 编写)和 Prettier(由 Estben Petersen 编写)扩展。
-
反应车间:我们将从上一章完成的反应车间项目开始。这可在 GitHub 的上获得 https://github.com/carlrip/LearnReact17WithTypeScript/tree/master/07-WorkingWithForms/04-FormSubmission 。
In order to restore code from a previous chapter, the LearnReact17WithTypeScript
repository at https://github.com/carlrip/LearnReact17WithTypeScript can be downloaded. The relevant folder can then be opened in Visual Studio Code and then npm install
can be entered in the terminal to do the restore. All the code snippets in this chapter can be found online at https://github.com/carlrip/LearnReact17WithTypeScript/tree/master/08-ReactRedux%EF%BB%BF.
在本节中,我们将首先介绍 Redux 中的三个原则,然后深入了解核心概念。
让我们来看看 ReDux 的三个原理:
- 单一真实来源:这意味着整个应用程序状态存储在单个对象中。在实际应用程序中,此对象可能包含嵌套对象的复杂树。
- 状态为只读:表示状态不能直接更改。这有点像说我们不能直接改变组件内的状态。在 Redux 中,更改状态的唯一方法是分派所谓的操作
- 使用纯函数进行更改:负责更改状态的函数称为减缩器
在接下来的部分中,我们将深入探讨操作和减缩器,以及管理它们的东西,即所谓的存储。
应用程序的整个状态都存在于所谓的存储中。状态存储在 JavaScript 对象中,如下所示:
{
products: [{ id: 1, name: "Table", ...}, {...}, ...],
productsLoading: false,
currentProduct: { id: 2, xname: "Chair", ... },
basket: [{ product: { id: 2, xname: "Chair" }, quantity: 1 }],
};
在本例中,单个对象包含以下内容:
- 一系列产品
- 是否从 web API 获取产品
- 用户正在查看的当前产品
- 用户篮子中的项目
该状态不包含任何函数、setter 或 getter。它是一个简单的 JavaScript 对象。商店还协调 Redux 中的所有移动部件。这包括通过还原程序将操作推送到更新状态。
因此,更新存储中的状态需要做的第一件事是发送一个操作。动作是另一个简单的 JavaScript 对象,如下所示:
{
type: "PRODUCTS/LOADING"
}
type
属性确定需要执行的操作类型。这是行动的重要和必要部分。如果 action 对象中没有type
,reducer 将不知道如何更改状态。在上一个示例中,该操作只包含type
属性。这是因为减速器不需要任何更多信息来更改此类操作的状态。
以下示例是另一个操作:
{
type: "PRODUCTS/GETSINGLE",
product: { id: 1, name: "Table", ...}
}
这一次,product
属性中的操作包含了额外的信息位。减速器需要此附加信息来更改此类操作的状态
因此,减缩器是使实际状态发生变化的纯函数。
A pure function always returns the same result for a given set of parameters. So, these functions don't depend on any state outside the scope of the function that isn't passed into the function. Pure functions also don't change any state outside the scope of the function.
以下是减速器的示例:
export const productsReducer = (state = initialProductState, action) => {
switch (action.type) {
case "PRODUCTS/LOADING": {
return {
...state,
productsLoading: true
};
}
case "PRODUCTS/GETSINGLE": {
return {
...state,
currentProduct: action.product,
productsLoading: false
};
}
default:
}
return state || initialProductState;
};
以下是有关减速器的一些信息:
- 减速器为当前状态和正在执行的操作提供两个参数
- state 参数默认为第一次调用 reducer 时的初始状态对象
- switch 语句用于操作类型,并为其每个分支中的每个操作类型创建适当的新状态对象
- 要创建新状态,我们将当前状态分散到一个新对象中,然后用已更改的属性覆盖它
- 从减速器返回新状态
您会注意到,我们刚才看到的 actions 和 reducer 没有 TypeScript 类型。显然,在下面的部分中实现这些时,我们将包括必要的类型。
因此,现在我们已经开始了解 Redux 是什么,是时候在我们的 React 商店中将其付诸实践了。
在使用 Redux 之前,我们需要将其与 TypeScript 类型一起安装。我们还将安装一个名为redux-thunk
的附加库,我们需要它来实现异步操作:
- 如果我们还没有,那么让我们从上一章结束的地方开始,在 VisualStudio 代码中打开 React shop 项目。那么,让我们通过终端中的
npm
安装 core Redux 库:
npm install redux
请注意,核心 Redux 库中包含 TypeScript 类型。因此,不需要对这些进行额外安装。
- 现在让我们为 Redux 安装 React 特定位。这些位允许我们将 React 组件连接到 Redux 存储。让我们通过
npm
安装这些:
npm install react-redux
- 让我们为
react-redux
安装 TypeScript 类型:
npm install --save-dev @types/react-redux
- 我们也来安装
redux-thunk
:
npm install redux-thunk
- 最后,我们可以为
redux-thunk
安装 TypeScript 类型:
npm install --save-dev @types/redux-thunk
现在安装了所有的 Redux 位,我们可以将 Redux 添加到我们在下一节中所研究的 React 车间。
我们将扩展在前几章中构建的 React shop,并添加 Redux 以管理产品页面上的状态。在本节中,我们将创建操作以开始将产品放入页面的过程。将有一个行动来获得产品。将有另一个操作来更改一些新的加载状态,我们最终将把它与我们在项目中已有的withLoading
HOC 绑定。
在开始 Redux 操作之前,让我们在ProductsData.ts
中创建一个用于获取产品的假 API:
export const getProducts = async (): Promise<IProduct[]> => {
await wait(1000);
return products;
};
因此,函数在返回产品之前会异步等待一秒钟。
我们需要通过创建一些类型来开始操作的实现。我们下一步做这个。
是时候开始用 Redux 增强我们的 React 商店了。首先,我们将为 Redux 存储创建一些状态类型和操作:
- 让我们在
src
文件夹中创建一个名为ProductsTypes.ts
的新文件,顶部有以下导入语句:
import { IProduct } from "./ProductsData";
- 让我们为将要实现的两种不同操作类型创建一个枚举:
export enum ProductsActionTypes {
GETALL = "PRODUCTS/GETALL",
LOADING = "PRODUCTS/LOADING"
}
Redux 不指定操作类型字符串的格式。因此,动作类型字符串的格式是我们的选择。我们需要确保字符串在商店中的操作类型中是唯一的。因此,我们在字符串中包含了两位信息:
- 操作涉及的存储区域。在我们的例子中,这是
PRODUCTS
。 - 该区域内的具体操作。在我们的例子中,我们有
GETALL
用于获取所有产品,而LOADING
表示正在获取产品
我们可以选择PRODUCTS
-GETALL
或Get All Products
。我们只需要确保字符串是唯一的。当我们在实现 action 和 reducer 时使用这些函数时,我们使用了一个枚举来提供良好的智能感知。
- 现在,我们可以为两个操作创建接口:
export interface IProductsGetAllAction {
type: ProductsActionTypes.GETALL,
products: IProduct[]
}
export interface IProductsLoadingAction {
type: ProductsActionTypes.LOADING
}
IProductsGetAllAction
接口用于在需要取货时发出的动作。IProductsLoadingAction
接口用于使减速器改变加载状态的动作。
- 让我们将动作类型与联合类型组合在一起:
export type ProductsActions =
| IProductsGetAllAction
| IProductsLoadingAction
这将是传递到减速器中的动作参数的类型。
- 最后,让我们为存储中的状态区域创建一个接口:
export interface IProductsState {
readonly products: IProduct[];
readonly productsLoading: boolean;
}
因此,我们的州将包含一系列产品,以及是否正在加载产品。
请注意,属性的前缀是readonly
关键字。这将帮助我们避免直接更改状态。
现在我们已经为动作和状态准备好了类型,我们可以在下一节中创建一些动作。
在本节中,我们将创建两个操作来获取产品和指示正在加载的产品。
- 让我们首先创建一个带有以下 import 语句的
ProductsActions.ts
文件:
import { ActionCreator, AnyAction, Dispatch } from "redux";
这些是 Redux 中的一些类型,我们将在执行操作时使用它们。
- 我们的一个操作将是异步的。那么,让我们从
redux-thunk
导入一个类型,准备在我们执行此操作时使用:
import { ThunkAction } from "redux-thunk";
- 让我们添加另一条导入语句,以便使用我们的伪 API:
import { getProducts as getProductsFromAPI } from "./ProductsData";
为了避免名称冲突,我们将 API 函数重命名为getProductsFromAPI
,因为稍后我们将创建一个名为getProducts
的操作。
- 我们还将导入上一节中创建的类型:
import { IProductsGetAllAction, IProductsLoadingAction, IProductsState, ProductsActionTypes } from "./ProductsTypes";
- 我们现在要创建一个称为动作创建者的东西。动作创建者按照 tin 上的说明执行:这是一个创建并返回动作的函数!让我们创建一个用于创建产品加载操作的操作创建者:
const loading: ActionCreator<IProductsLoadingAction> = () => {
return {
type: ProductsActionTypes.LOADING
}
};
- 我们使用泛型
ActionCreator
类型,该类型包含函数签名的适当操作接口 - 该函数只返回所需的操作对象
我们可以使用隐式返回语句更简洁地编写此函数,如下所示:
const loading: ActionCreator<IProductsLoadingAction> = () => ({
type: ProductsActionTypes.LOADING
});
从现在起,在实现动作创建者时,我们将使用这个较短的语法。
- 让我们继续实施 action creator 以获取产品。这更复杂,所以让我们从函数签名开始:
export const getProducts: ActionCreator<ThunkAction<Promise<AnyAction>, IProductsState, null, IProductsGetAllAction>> = () => {};
我们再次使用泛型ActionCreator
类型,但这次它包含的不仅仅是最终将返回的操作接口。这是因为此特定操作是异步的。
我们在ActionCreator
中使用ThunkAction
进行异步操作,而异步操作又是一个具有四个参数的泛型类型:
- 第一个参数是返回类型,理想情况下应为
Promise<IProductsGetAllAction>
。然而,TypeScript 编译器很难解决这个问题,因此,我们选择了稍微松散的Promise<AnyAction>
类型。 - 第二个参数是与操作相关的状态接口。
- 第三个参数是传递给 action creator 的参数类型,在本例中为
null
,因为没有参数。 - 最后一个参数是操作的类型。
我们导出此 action creator,因为它最终将从ProductsPage
组件调用。
- 异步操作需要返回一个异步函数,该函数最终将分派我们的操作:
export const getProducts: ActionCreator<ThunkAction<Promise<AnyAction>, IProductsState, null, IProductsGetAllAction>> = () => {
return async (dispatch: Dispatch) => {
};
};
所以,函数要做的第一件事就是返回另一个函数,使用async
关键字标记它是异步的。内部函数将存储区中的调度程序作为参数。
- 让我们实现内部功能,然后:
return async (dispatch: Dispatch) => {
dispatch(loading());
const products = await getProductsFromAPI();
return dispatch({
products,
type: ProductsActionTypes.GETALL
});
};
- 我们要做的第一件事是分派另一个动作,以便减速器最终相应地改变加载状态
- 下一步是从伪 API 异步获取产品
- 最后一步是分派所需的操作
现在我们已经创建了两个动作,我们将在下一节中创建一个减速器。
减速器是负责为给定操作创建新状态的函数。因此,函数使用当前状态执行操作并返回新状态。在本节中,我们将为我们在产品上创建的两个操作创建一个缩减器
- 让我们首先创建一个名为
ProductsReducer.ts
的文件,其中包含以下导入语句:
import { Reducer } from "redux";
import { IProductsState, ProductsActions, ProductsActionTypes } from "./ProductsTypes";
我们正在从 Redux 导入Reducer
类型,以及前面创建的操作和状态的类型。
- 接下来,我们需要定义初始状态:
const initialProductState: IProductsState = {
products: [],
productsLoading: false
};
因此,我们将产品设置为空数组,产品加载状态设置为false
。
- 我们现在可以开始创建 reducer 函数:
export const productsReducer: Reducer<IProductsState, ProductsActions> = (
state = initialProductState,
action
) => {
switch (action.type) {
// TODO - change the state
}
return state;
};
-
我们已经使用 Redux 中的
Reducer
泛型类型键入了函数,并传入了状态和动作类型。这给了我们一个很好的类型安全级别。 -
该函数根据 Redux 的要求接收状态和操作的参数。
-
状态默认为我们在上一步中刚刚设置的初始状态对象。
-
在函数末尾,如果 switch 语句无法识别操作类型,则返回默认状态。
- 让我们继续实施我们的减速器:
switch (action.type) {
case ProductsActionTypes.LOADING: {
return {
...state,
productsLoading: true
};
}
case ProductsActionTypes.GETALL: {
return {
...state,
products: action.products,
productsLoading: false
};
}
}
我们为每个操作实现了一个交换机分支。两个分支都遵循相同的模式,返回一个新的状态对象,该对象将旧状态分散到其中,并在顶部合并相应的属性
这是我们完成的第一个减速器。在下一节中,我们将创建我们的商店。
在本节中,我们将创建一个存储区,用于保存我们的状态并管理操作和还原:
- 让我们首先创建一个名为
Store.tsx
的新文件,并使用以下 import 语句从 Redux 中获取所需的位和块:
import { applyMiddleware, combineReducers, createStore, Store } from "redux";
-
createStore
是我们最终将用于创建商店的功能 -
我们需要
applyMiddleware
函数,因为我们需要使用 Redux Thunk 中间件来管理异步操作 -
combineReducers
函数是我们可以用来合并减速器的函数 -
Store
是我们可以用于商店的类型脚本
- 我们来导入
redux-thunk
:
import thunk from "redux-thunk";
- 最后,让我们导入减速器和状态类型:
import { productsReducer } from "./ProductsReducer";
import { IProductsState } from "./ProductsTypes";
- 商店的一个关键部分是状态。那么,让我们为这个定义一个接口:
export interface IApplicationState {
products: IProductsState;
}
此时,接口仅包含我们的产品状态。
- 现在让我们将减速机置于 Redux
combineReducer
功能中:
const rootReducer = combineReducers<IApplicationState>({
products: productsReducer
});
- 通过定义 state 和 root reducer,我们可以创建我们的存储。我们实际上要创建一个函数来创建存储:
export default function configureStore(): Store<IApplicationState> {
const store = createStore(rootReducer, undefined, applyMiddleware(thunk));
return store;
}
- 创建我们的存储的函数被称为
configureStore
,并返回传递给它的特定存储状态的泛型Store
类型。 - 该函数使用 Redux
createStore
函数创建并返回存储。我们传入了我们的 reducer 和 Redux-Thunk 中间件。我们通过undefined
作为初始状态,因为我们的减速器负责初始状态。
我们的商店开了个好头。在下一节中,我们将开始将 React 商店连接到我们的商店。
在本节中,我们将把Products
页面连接到我们的商店。第一项工作是添加 React-ReduxProvider
组件,我们将在下一节中进行。
Provider
组件可以在任何级别将存储传递给它下面的组件。因此,在本节中,我们将在组件层次结构的顶部添加Provider
,以便所有组件都可以访问它:
- 让我们打开现有的
index.tsx
并从 React Redux 导入Provider
组件:
import { Provider} from "react-redux";
- 我们也从 React Redux 导入
Store
类型:
import { Store } from "redux";
- 最后,我们需要从我们的商店导入以下内容:
import configureStore from "./Store";
import { IApplicationState } from "./Store";
- 然后,我们将在 import 语句之后创建一个小函数组件:
interface IProps {
store: Store<IApplicationState>;
}
const Root: React.SFC<IProps> = props => {
return ();
};
这个Root
组件将成为我们的新根元素。它把我们的商店当作道具。
- 因此,我们需要在新的根组件中包含旧的根元素
Routes
:
const Root: React.SFC<IProps> = props => {
return (
<Routes />
);
};
- 该组件还需要添加一个组件,即 React Redux 的
Provider
组件:
return (
<Provider store={props.store}>
<Routes />
</Provider>
);
我们将Provider
放置在组件树的顶部,并将我们的存储传递给它。
- 完成新的根组件后,让我们更改根渲染函数:
const store = configureStore();
ReactDOM.render(<Root store={store} />, document.getElementById(
"root"
) as HTMLElement);
我们首先使用configureStore
函数创建存储,然后将其传递到Root
组件中。
因此,这是将我们的组件连接到商店的第一步。在下一节中,我们将为ProductPage
组件完成此连接。
我们即将看到我们的强化商店投入使用。在本节中,我们将把我们的商店连接到几个组件。
我们要连接到商店的第一个组件是ProductsPage
组件。
让我们打开ProductsPage.tsx
并开始重构它:
- 首先,我们从 React Redux 导入
connect
函数:
import { connect } from "react-redux";
我们将使用本节末尾的connect
函数将ProductsPage
组件连接到存储。
- 让我们从我们的商店导入商店状态类型和
getProducts
操作创建者:
import { IApplicationState } from "./Store";
import { getProducts } from "./ProductsActions";
ProductPage
组件现在将不包含任何状态,因为它将保存在 Redux 存储中。因此,让我们从移除状态接口、静态getDerivedStateFromProps
方法以及构造函数开始。ProductsPage
组件现在应具有以下形状:
class ProductsPage extends React.Component<RouteComponentProps> {
public async componentDidMount() { ... }
public render() { ... }
}
- 现在,数据将通过道具从商店中获取。那么,让我们重构我们的道具界面:
interface IProps extends RouteComponentProps {
getProducts: typeof getProducts;
loading: boolean;
products: IProduct[];
}
class ProductsPage extends React.Component<IProps> { ... }
因此,我们将获得从存储区传递到组件的以下数据:
getProducts
动作创造者- 一个名为
loading
的标志,指示是否正在提取产品 - 产品系列
- 因此,我们调整
componentDidMount
生命周期方法,调用getProducts
动作创建者,开始产品取数流程:
public componentDidMount() {
this.props.getProducts();
}
- 我们不再直接从
ProductsData.ts
引用products
数组。因此,让我们从 input 语句中删除它,使其如下所示:
import { IProduct } from "./ProductsData";
- 现在还没有迹象表明我们过去的状态。我们现在只在
render
方法开始时获取它,而不是将其存储在状态:
public render() {
const searchParams = new URLSearchParams(this.props.location.search);
const search = searchParams.get("search") || "";
return ( ... );
}
- 让我们继续使用
render
方法,替换旧的state
引用:
<ul className="product-list">
{this.props.products.map(product => {
if (!search || (search && product.name.toLowerCase().indexOf(search.toLowerCase()) > -1)
) { ... }
})}
</ul>
- 在类下,但在 export 语句之前,让我们创建一个函数,将来自存储的状态映射到组件道具:
const mapStateToProps = (store: IApplicationState) => {
return {
loading: store.products.productsLoading,
products: store.products.products
};
};
因此,我们需要了解产品是否与商店中的产品一样被装载,并将其传递给我们的道具。
- 我们还需要映射到另一个道具,那就是
getProducts
函数道具。让我们创建另一个函数,将此操作从存储映射到组件中的此函数道具:
const mapDispatchToProps = (dispatch: any) => {
return {
getProducts: () => dispatch(getProducts())
};
};
- 文件底部还有一项工作要做。这是为了在导出之前将 React-Redux
connect
HOC 包裹在我们的ProductsPage
组件上:
export default connect(
mapStateToProps,
mapDispatchToProps
)(ProductsPage);
connect
HOC 将组件连接到我们的存储,存储由组件树中较高的Provider
组件提供给我们。connect
HOC 还调用映射器函数,将状态和动作创建者从存储映射到组件道具。
- 终于到了尝试增强页面的时候了。让我们通过终端启动 dev 服务器和应用程序:
npm start
我们应该发现页面的行为与以前完全相同。唯一的区别是现在状态在我们的 Redux 商店中进行管理。
在下一节中,我们将通过添加项目中已有的加载微调器来增强产品页面。
在本节中,我们将向产品页面添加一个加载微调器。在此之前,我们将把产品列表提取到它自己的组件中。然后,我们可以将withLoader
HOC 添加到提取的组件中:
- 让我们使用以下导入为提取的组件创建一个名为
ProductsList.tsx
的新文件:
import * as React from "react";
import { Link } from "react-router-dom";
import { IProduct } from "./ProductsData";
import withLoader from "./withLoader";
- 该组件将为 products 数组和搜索字符串提供道具:
interface IProps {
products?: IProduct[];
search: string;
}
- 我们将该组件称为
ProductList
,它将是一个 SFC。让我们开始创建组件:
const ProductsList: React.SFC<IProps> = props => {
const search = props.search;
return ();
};
- 现在我们可以将
ul
标记从ProductsPage
组件 JSX 移动到新ProductList
组件中的返回语句中:
return (
<ul className="product-list">
{props.products &&
props.products.map(product => {
if (
!search ||
(search &&
product.name.toLowerCase().indexOf(search.toLowerCase())
> -1)
) {
return (
<li key={product.id} className="product-list-item">
<Link to={`/products/${product.id}`}>{product.name}
</Link>
</li>
);
} else {
return null;
}
})}
</ul>
);
注意,在移动 JSX 之后,我们删除了对this
的引用。
- 为了完成
ProductList
组件,让我们使用withLoader
HOC 包装将其导出:
export default withLoader(ProductsList);
- 我们将
ProductPage.tsx
中的 return 语句更改为引用提取的组件:
return (
<div className="page-container">
<p>
Welcome to React Shop where you can get all your tools for ReactJS!
</p>
<ProductsList
search={search}
products={this.props.products}
loading={this.props.loading}
/>
</div>
);
- 我们不能忘记导入引用了它的
ProductsList
组件:
import ProductsList from "./ProductsList";
- 最后,我们可以删除
ProductsPage.tsx
中导入的Link
组件,因为它不再被引用。
如果我们转到 running 应用程序并浏览到 Products 页面,我们现在应该会在产品加载时看到加载微调器:
因此,我们的产品页面已经很好地连接到了我们的 Redux 商店。在下一节中,我们将把产品页面连接到商店。
将ProductPage
组件连接到我们的商店首先需要在我们的商店做一些工作。我们需要当前产品的附加状态,以及它是否已添加到篮子中。我们还需要额外的操作和代码来获取产品并将其添加到篮子中:
- 首先,让我们在
ProductsTypes.ts
中为当前产品添加附加状态:
export interface IProductsState {
readonly currentProduct: IProduct | null;
...
}
- 在
ProductTypes.ts
中,我们添加获取产品的动作类型:
export enum ProductsActionTypes {
GETALL = "PRODUCTS/GETALL",
GETSINGLE = "PRODUCTS/GETSINGLE",
LOADING = "PRODUCTS/LOADING"
}
- 我们还将添加获取产品的操作类型:
export interface IProductsGetSingleAction {
type: ProductsActionTypes.GETSINGLE;
product: IProduct;
}
- 然后,我们可以将此操作类型添加到联合操作类型:
export type ProductsActions = IProductsGetAllAction| IProductsGetSingleAction | IProductsLoadingAction;
- 让我们继续在
ProductsActions.ts
中创建新的动作创建者。首先,让我们导入我们的假 API 以获得产品:
import { getProduct as getProductFromAPI, getProducts as getProductsFromAPI} from "./ProductsData";
- 然后,我们可以导入需要实现的操作创建者的类型:
import { IProductsGetAllAction, IProductsGetSingleAction, IProductsLoadingAction, IProductsState, ProductsActionTypes } from "./productsTypes";
- 让我们实施 action creator 以获取产品:
export const getProduct: ActionCreator<ThunkAction<Promise<any>, IProductsState, null, IProductsGetSingleAction>> = (id: number) => {
return async (dispatch: Dispatch) => {
dispatch(loading());
const product = await getProductFromAPI(id);
dispatch({
product,
type: ProductsActionTypes.GETSINGLE
});
};
};
这与getProducts
动作创造者非常相似。结构上唯一的区别是 action creator 为产品 ID 引入了一个参数。
- 现在转到
ProductsReducer.ts
中的减速器。让我们首先在初始状态中将当前产品设置为 null:
const initialProductState: IProductsState = {
currentProduct: null,
...
};
- 在
productReducer
函数中,我们在 switch 语句中为新的动作类型添加一个分支:
switch (action.type) {
...
case ProductsActionTypes.GETSINGLE: {
return {
...state,
currentProduct: action.product,
productsLoading: false
};
}
}
我们将旧状态扩展到新对象中,覆盖当前项目,并将加载状态设置为false
。
这就是 Redux 商店中产品页面需要的一些状态管理。然而,我们还没有在我们的商店管理购物篮。我们将在下一节中进行此操作。
在本节中,我们将为篮子添加状态管理。我们将为此在商店中创建一个新分区。
- 首先,让我们为名为
BasketTypes.ts
的类型创建一个新文件,内容如下:
import { IProduct } from "./ProductsData";
export enum BasketActionTypes {
ADD = "BASKET/ADD"
}
export interface IBasketState {
readonly products: IProduct[];
}
export interface IBasketAdd {
type: BasketActionTypes.ADD;
product: IProduct;
}
export type BasketActions = IBasketAdd;
- 我们的篮子里只有一种状态,那就是篮子里的一系列产品。
- 也只有一个行动。这是将产品添加到篮子中。
- 让我们创建一个名为
BasketActions.ts
的文件,其内容如下:
import { BasketActionTypes, IBasketAdd } from "./BasketTypes";
import { IProduct } from "./ProductsData";
export const addToBasket = (product: IProduct): IBasketAdd => ({
product,
type: BasketActionTypes.ADD
});
这是添加到篮中的动作创建者。该函数接收一个产品,并以适当的操作类型在操作中返回该产品。
- 现在就去减速器。让我们创建一个名为
BasketReducer.ts
的文件,其中包含以下导入语句:
import { Reducer } from "redux";
import { BasketActions, BasketActionTypes, IBasketState } from "./BasketTypes";
- 让我们为初始篮子状态创建一个对象:
const initialBasketState: IBasketState = {
products: []
};
- 现在让我们创建减速器:
export const basketReducer: Reducer<IBasketState, BasketActions> = (state = initialBasketState, action) => {
switch (action.type) {
case BasketActionTypes.ADD: {
return {
...state,
products: state.products.concat(action.product)
};
}
}
return state || initialBasketState;
};
这遵循与productsReducer
相同的模式。
值得注意的一点是,我们如何优雅地将product
添加到products
数组中,而不改变原始数组。我们使用 JavaScriptconcat
函数,它通过将原始数组与传入的参数合并来创建一个新数组。这是一个很好的函数,可用于状态更改涉及向数组添加项的还原器中。
- 现在我们打开
Store.ts
导入新的减速器,并说明篮子:
import { basketReducer } from "./BasketReducer";
import { IBasketState } from "./BasketTypes";
- 让我们将篮子状态添加到存储:
export interface IApplicationState {
basket: IBasketState;
products: IProductsState;
}
- 我们现在有两个减速机。那么,让我们将篮子减速器添加到
combineReducers
函数调用中:
export const rootReducer = combineReducers<IApplicationState>({
basket: basketReducer,
products: productsReducer
});
现在我们已经调整了存储,我们可以将ProductPage
组件连接到它。
在本节中,我们将把ProductPage
组件连接到我们的商店:
- 首先将以下内容导入
ProductPage.tsx
:
import { connect } from "react-redux";
import { addToBasket } from "./BasketActions";
import { getProduct } from "./ProductsActions";
import { IApplicationState } from "./Store";
- 我们现在要引用商店的
getProduct
,而不是ProductsData.ts
中的那个。因此,让我们将其从导入中删除,使其看起来如下所示:
import { IProduct } from "./ProductsData";
- 接下来,让我们将状态移动到道具中:
interface IProps extends RouteComponentProps<{ id: string }> {
addToBasket: typeof addToBasket;
getProduct: typeof getProduct;
loading: boolean;
product?: IProduct;
added: boolean;
}
class ProductPage extends React.Component<IProps> { ... }
因此,本次移动后应移除IState
接口和Props
类型。
- 我们可以删除构造函数,因为现在不需要初始化任何状态。这些都是在商店里做的。
- 我们将
componentDidMount
生命周期方法更改为调用 action creator 获取产品:
public componentDidMount() {
if (this.props.match.params.id) {
const id: number = parseInt(this.props.match.params.id, 10);
this.props.getProduct(id);
}
}
注意,我们还删除了async
关键字,因为该方法不再是异步的。
- 转到
render
函数,让我们将对 state 的引用替换为对 props 的引用:
public render() {
const product = this.props.product;
return (
<div className="page-container">
<Prompt when={!this.props.added} message={this.navAwayMessage}
/>
{product || this.props.loading ? (
<Product
loading={this.props.loading}
product={product}
inBasket={this.props.added}
onAddToBasket={this.handleAddClick}
/>
) : (
<p>Product not found!</p>
)}
</div>
);
}
- 现在让我们看看单击处理程序,并对其进行重构,以调用 action creator 将其添加到篮子中:
private handleAddClick = () => {
if (this.props.product) {
this.props.addToBasket(this.props.product);
}
};
- 接下来是连接过程中的最后几个步骤。让我们实现将动作创建者从商店映射到组件道具的功能:
const mapDispatchToProps = (dispatch: any) => {
return {
addToBasket: (product: IProduct) => dispatch(addToBasket(product)),
getProduct: (id: number) => dispatch(getProduct(id))
};
};
- 将状态映射到组件道具稍微复杂一些。让我们从简单的映射开始:
const mapStateToProps = (store: IApplicationState) => {
return {
basketProducts: store.basket.products,
loading: store.products.productsLoading,
product: store.products.currentProduct || undefined
};
};
请注意,我们将空currentProduct
映射到undefined
。
- 我们需要映射到的其他道具是
added
。我们需要检查商店中的当前产品是否处于篮子状态才能设置此boolean
值。我们可以使用 products 数组中的some
函数来实现:
const mapStateToProps = (store: IApplicationState) => {
return {
added: store.basket.products.some(p => store.products.currentProduct ? p.id === store.products.currentProduct.id : false),
...
};
};
- 最后一步是使用 React Redux 的
connect
HOC 将ProductPage
组件连接到存储:
export default connect(
mapStateToProps,
mapDispatchToProps
)(ProductPage);
我们现在可以转到 running 应用程序,访问产品页面,并将其添加到购物篮中。单击“添加到篮子”按钮后,该按钮应消失。如果我们浏览到另一个产品,然后返回到我们已经添加到购物篮中的产品,则“添加到购物篮”按钮不应出现。
因此,我们现在已经将产品和产品页面连接到我们的 Redux 商店。在下一节中,我们将创建一个篮子摘要组件,并将其连接到商店。
在本节中,我们将创建一个名为BasketSummary
的新组件。这将显示篮子中的物品数量,并将位于我们商店的右上角。以下屏幕截图显示了屏幕右上角篮子摘要的外观:
- 让我们创建一个名为
BasketSummary.tsx
的文件,其内容如下:
import * as React from "react";
interface IProps {
count: number;
}
const BasketSummary: React.SFC<IProps> = props => {
return <div className="basket-summary">{props.count}</div>;
};
export default BasketSummary;
这是一个简单的组件,它将篮子中的产品数量作为一个道具,并在一个带有basket-summary
CSS 类的div
样式中显示该值。
- 让我们添加刚才在
index.css
中引用的 CSS 类:
.basket-summary {
display: inline-block;
margin-left: 10px;
padding: 5px 10px;
border: white solid 2px;
}
- 我们将把篮子摘要添加到标题部分。那么,我们将其导入
Header.tsx
:
import BasketSummary from "./BasketSummary";
- 我们也从 React Redux 导入
connect
函数:
import { connect } from "react-redux";
- 让我们也为我们的商店导入状态类型:
import { IApplicationState } from "./Store";
- 为
Header
组件中的产品数量添加一个道具:
interface IProps extends RouteComponentProps {
basketCount: number;
}
class Header extends React.Component<IProps, IState> {
public constructor(props: IProps) { ... }
...
}
我们将在此组件中保持搜索状态为本地。
- 现在我们将
BasketSummary
组件添加到Header
组件 JSX 中:
<header className="header">
<div className="search-container">
<input ... />
<BasketSummary count={this.props.basketCount} />
</div>
...
</header>
- 下一步是将购物篮中的产品数量映射到
basketCount
道具:
const mapStateToProps = (store: IApplicationState) => {
return {
basketCount: store.basket.products.length
};
};
- 最后,我们可以将
Header
组件连接到存储:
export default connect(mapStateToProps)(withRouter(Header));
既然Header
组件正在使用BasketSummary
组件,并且已经连接到商店,我们应该能够在运行的应用程序中将产品添加到购物篮中,并看到购物篮摘要增加。
至此,关于将组件连接到存储的这一部分就结束了。我们已经将一些不同的组件连接到了商店,所以希望这个过程现在是有意义的
在下一节中,我们将探索一种类似 Redux 的方法来管理组件内的状态。
Redux 非常适合在我们的应用程序中管理复杂状态。但是,如果我们正在管理的状态只存在于单个组件中,则会有点沉重。显然,我们可以使用setState
(对于类组件)或useState
(对于功能组件)来管理这些案例。然而,如果国家是复杂的呢?可能有很多状态,状态交互可能涉及很多步骤,其中一些是异步的。在本节中,我们将探索一种使用 React 中的useReducer
函数管理这些案例的方法。我们的例子将是人为的和简单的,但它会让我们了解这种方法。
我们将在 React 商店的产品页面中添加一个 Like 按钮。用户可以多次喜欢某个产品。Product
组件将跟踪 like 的数量以及处于其状态的最后一个 like 的日期和时间:
- 我们将首先打开
Product.tsx
并在Product
组件之前为我们的状态创建一个接口,其中包含喜欢的数量和最后一个喜欢的日期:
interface ILikeState {
likes: number;
lastLike: Date | null;
}
- 我们将创建一个变量来保存初始状态,也在
Product
之外:
const initialLikeState: ILikeState = {
likes: 0,
lastLike: null
};
- 现在,让我们为操作创建一个类型:
enum LikeActionTypes {
LIKE = "LIKE"
}
interface ILikeAction {
type: LikeActionTypes.LIKE;
now: Date;
}
- 我们还将创建一个包含所有操作类型的联合类型。在我们的示例中,我们只有一种动作类型,但让我们这样做是为了了解一种可扩展的方法:
type LikeActions = ILikeAction;
- 在
Product
组件内部,让我们调用 React 中的useReducer
函数来获取我们的状态和dispatch
函数:
const [state, dispatch]: [
ILikeState,
(action: ILikeAction) => void
] = React.useReducer(reducer, initialLikeState);
让我们来分析一下:
- 我们将传递给
useReducer
一个名为reducer
的函数(我们尚未创建)。 - 我们也进入
useReducer
我们的初始状态。 useReducer
返回包含两个元素的数组。第一个元素是当前状态,第二个是调用动作的dispatch
函数。
- 让我们重构这一行并分解状态,以便可以直接引用状态的各个部分:
const [{ likes, lastLike }, dispatch]: [
ILikeState,
(action: ILikeAction) => void
] = React.useReducer(reducer, initialLikeState);
- 在
Product
组件中 JSX 的底部,让我们添加 JSX 来呈现我们有多少喜欢,并添加一个按钮来添加喜欢:
{!props.inBasket && (
<button onClick={handleAddClick}>Add to basket</button>
)}
<div className="like-container">
{likes > 0 && (
<div>{`I like this x ${likes}, last at ${lastLike}`}</div>
)}
<button onClick={handleLikeClick}>
{likes > 0 ? "Like again" : "Like"}
</button>
</div>
- 让我们添加刚才引用到
index.css
中的like-container
CSS 类:
.like-container {
margin-top: 20px;
}
.like-container button {
margin-top: 5px;
}
- 让我们在 Like 按钮上实现 click 处理程序:
const handleLikeClick = () => {
dispatch({ type: LikeActionTypes.LIKE, now: new Date() });
};
- 我们的最后一项任务是在
Product
组件之外,在LikeActions
类型下实现减速器功能:
const reducer = (state: ILikeState = initialLikeState, action: LikeActions) => {
switch (action.type) {
case LikeActionTypes.LIKE:
return { ...state, likes: state.likes + 1, lastLike: action.now };
}
return state;
};
如果我们尝试一下,在导航到产品页面后,我们最初会看到一个 Like 按钮。如果我们单击它,按钮文本将再次变为“喜欢”,并在其上方显示一段文本,指示有多少喜欢以及上次喜欢它的时间。
这种实现感觉非常类似于在 Redux 存储中实现操作和缩减器,但这都是在一个组件中实现的。对于我们刚刚经历的例子来说,这是一种过分的做法,但在我们需要管理更多的状态时,它可能会被证明是有用的。
我们从介绍 Redux 开始,学习了原则和关键概念。我们了解到,状态存储在单个对象中,并在调度操作时由称为 reducer 的纯函数更改。
我们在 React 商店中创建了自己的商店,以将理论付诸实践。以下是我们在实施过程中学到的一些要点:
- 操作类型的枚举在引用它们时为我们提供了良好的智能感知。
- 使用接口定义动作可以提供很好的类型安全性,并允许我们创建一个联合类型,reducer 可以使用它来处理动作。
- 在状态接口中使用只读属性可以帮助我们避免直接改变状态。
- 同步动作创建者只需返回所需的动作对象。
- 异步动作创建者返回最终返回动作对象的函数。
- reducer 为它处理的每个操作类型都包含一个逻辑分支,通过将旧状态扩展到新对象中,然后用更改的属性覆盖它来创建新状态。
- Redux 中名为
createStore
的函数创建实际的存储。我们将所有合并在一起的简化程序与 Redux Thunk 中间件一起传递,以管理异步操作。
然后我们将一些组件连接到商店。以下是该过程中的关键点:
- React Redux 中的
Provider
组件需要位于要使用存储的所有组件之上。这将接收包含商店的道具。 - React Redux 的
connect
HOC 然后将各个组件连接到存储。这需要两个参数,用于将状态和动作创建者映射到组件道具。
在我们的 React 应用程序中实现 Redux 时,我们需要考虑很多细节。它确实在状态管理复杂的场景中大放异彩,因为 Redux 迫使我们将逻辑分解为易于理解和维护的独立部分
我们了解到,通过利用 React 的useReducer
功能,我们可以在单个组件中使用类似 Redux 的方法。当状态复杂且仅存在于单个组件中时,可以使用此选项
Redux 操作经常执行的一项任务是与 RESTAPI 交互。在下一章中,我们将学习如何在基于类和基于函数的组件中与 RESTAPI 交互。我们还将了解一个用于调用 RESTAPI 的本机函数以及一个流行的开源库。
在结束本章之前,让我们用一些问题来测试我们的知识:
- 是否需要 action 对象中的
type
属性,是否需要将该属性称为 type?我们可以叫它别的吗? - action 对象可以包含多少属性?
- 什么是动作创造者?
- 为什么我们在 React shop 应用程序的 Redux 商店中需要 Redux Thunk?
- 我们能不能用些别的东西,而不是 Redux Thunk?
- 在我们刚刚实现的
basketReducer
中,为什么不使用push
函数将项目添加到篮子状态?也就是说,突出显示的线条有什么问题?
export const basketReducer: Reducer<IBasketState, BasketActions> = (
state = initialBasketState,
action
) => {
switch (action.type) {
case BasketActionTypes.ADD: {
state.products.push(action.product);
}
}
return state || initialBasketState;
};
以下链接是有关 React Redux 的更多信息的良好资源:
- Redux 在线文档非常值得在阅读 https://redux.js.org 。
- 除了这些核心 Redux 文档之外,React Redux 文档也值得一看。这些位于https://react-redux.js.org/ 。
- Redux Thunk GitHub 位于https://github.com/reduxjs/redux-thunk 。主页包含一些有用的信息和示例。