第八章 React Redux

  • Principles and key concepts
  • Installing Redux
  • Creating reducers
  • Creating actions
  • Creating a store
  • Connecting our React App to the store
  • Managing state with use Reducer

Principles and key concepts

Principles

Redux的三大原则:

  • Single source of truth:意味着整个项目的状态被存储在一个单一对象。在一个真实应用系统中,这个对象很可能包含了一个复杂的内嵌对象树。
  • State is read-only:意味着状态不能被直接改变。就是说不能通过组件来改变状态。在Redux中,唯一能改变状态的方法是通过action进行传递(dispatch)。
  • Changes are made with pure functions:那些能够改变状态的函数被称为“reducer”。

接下来的环节,会深入介绍action和reducer以及消息的store内容。

Key concepts

Redux内存活的整个应用的状态被称为一个store。状态被存储在一个JavaScript对象中,形式如下:

1
2
3
4
5
6
{
products: [{ id: 1, name: "Table", ...}, {...}, ...],
productsLoading: false,
currentProduct: { id: 2, xname: "Chair", ... },
basket: [{ product: { id: 2, xname: "Chair" }, quantity: 1 }],
};

状态不会包含有任何函数、setter或者getter。它就是一个简单的JavaScript对象。

要更新一个store中的state,就是派遣一个action。其中action又是另外一个简单的JavaScript对象,格式如下:

1
2
3
{
type: "PRODUCTS/LOADING"
}

type属性决定了哪种action需要被处理。type是必须的,否则reducer不知道如何改变状态。

1
2
3
4
{
type: "PRODUCTS/GETSINGLE",
product: { id: 1, name: "Table", ...}
}

这个带有一个额外的属性值,因为reducer除了要知道action的类型外,还要获取更新的内容。

因此,reducer是纯函数。

纯函数就是不依赖于外部自由变量的函数,对于给定的输入,总能得到相同的结果。

下面是reducer的一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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;
};

关于reducer有几点:

  • reducer接收两个参数,current state 和要处理的action
  • reducer第一次被调用时,state参数默认为一个初始化状态对象
  • 这里需要使用switch语句处理不同的action类型
  • 返回语句表示创建一个新的状态覆盖原来已有的状态属性
  • reducer返回新的(更新的)状态

你会注意action和reducer都不是TypeScript类型的。下面开始实战。

Installing Redux

在使用Redux之前,需要安装依赖项。另外还需要安装一个库redux-thunk,以实现异步的ation:

  1. 安装redux,
1
yarn add redux
  1. 安装具体的redux的关联框架,
1
yarn add rect-redux
  1. 对应TS,
1
yarn add -D @types/react-redux
  1. 安装redux-thunk
1
2
yarn add redux-thunk
yarn add -D @types/redux-thunk

Creating actions

这里将沿用前面章节使用的代码,将Redux集成到产品页面上。本小节,我们会创建action获取产品内容。以及使用另外一个action更改新的状态。

在此之前,首先在ProductsData.ts创建一个假的API,

1
2
3
4
export const getProducts = async (): Promise<IProduct[]> => {
await wait(1000);
return products;
}

该函数异步等待返回的产品信息。

Creating state and action types

下面使用Redux来增强React shop。首先,创建一些state类型,以及action类型。

  1. src文件夹创建一个新的文件ProductsTypes.ts
1
import { IProduct } from "./ProductsData";
  1. 添加两种不同类型的action枚举,
1
2
3
4
export enum ProductsActionTypes {
GETALL = "PRODUCTS/GETALL",
LOADING = "PRODUCTS/LOADING"
}

Redux并没有要求action类型为字符串形式。这里我们选择用字符串来表述。同时要确保字符串是全局唯一的。我们定义的字符串中包含了两点重要信息:

  • 存储的action被关联。这里它是PRODUCTS
  • 特定的具体操作被指示。这里,GETALL表示获取所有商品,LOADING表示商品正在获取中。

当然我们也可以写成类似PRODUCTS-GETALL或者Get All Products。我们只需要确保字符串是唯一的。

  1. 下面为上述两种Action定义接口:
1
2
3
4
5
6
7
8
export interface IProductsGetAllAction {
type: ProductsActionTypes.GETALL,
products: IProduct[]
}

export interface IProductsLoadingAction {
type: ProductsActionTypes.LOADING
}

IProductsGetAllAction用作获取商品时的派遣动作。IProductsLoadingAction用作加载状态。

  1. 组合为一个新的union type:
1
2
3
export type ProductsActions = 
| IProductsGetAllAction
| IProductsLoadingAction

该类型将被传递在reducer的参数上。

  1. 最后,为这种存储状态创建一个接口:
1
2
3
4
export interface IProductsState {
readonly products: IProduct[];
readonly productsLoading: boolean;
}

这里的state将包含了一组商品信息,以及商品是否正在加载。

注意到属性前面引入了readonly关键字。它帮助我们避免对状态的直接修改。

有了state和action的类型后,接下来创建一些具体的action。

Creating actions

本小节,将创建两个action。获取商品的action,商品处于加载的action。

  1. 创建一个ProductsActions.ts文件,
1
import { ActionCreator, AnyAction, Dispatch } from "redux";

这里用到几个action类型需要实现。

  1. 其中一个action用作异步操作。需要导入redux-thunk
1
import { ThunkAction } from "redux-thunk";
  1. 另外导入之前的模拟API。
1
import { getProducts as getProductsFromAPI } from "./ProductsData";

这里需要重命名getProductsFromAPI,避免和getProducts的action冲突。

  1. 将先前定义的action类型导入。
1
import { IProductsGetAllAction, IProductsLoadingAction, IProductsState, ProductsActionTypes } from "./ProductsTypes";
  1. 接下来,创建一个action creator,顾名思义:它是一个函数,创建并返回一个action!
1
2
3
4
5
const loading: ActionCreator<IProductsLoadingAction> = () => {
return {
type: ProductsActionTypes.LOADING
}
};
  • 我们使用了泛型ActionCreator作为函数签名
  • 函数仅仅返回要求的action对象

还可以使用隐式返回语句另函数更为简洁

1
2
3
const loading: ActionCreator<IProductsLoadingAction> = () => ({
type: ProductsActionTypes.LOADING,
});

我们将用到这个简短语法用于action creator的实现。

  1. 添加另一个action creator的实现,这稍微更复杂一些:
1
2
3
4
5
6
export const getProducts: ActionCreator<ThunkAction<
Promise<AnyAction>,
IProductsState,
null,
IProductsGetAllAction
>> = () => {};

因为这里的action是异步的,需要进行一层包装。这里使用了ThunkAction泛型类型来包装同步action,它包含4个参数:

  • 第一个参数是返回类型,理想上应该是Promise<IProductsGetAllAction>。然而,TypeScript编译器无法处理,因此折中放宽为Promise<AnyAction>类型。
  • 第二个参数为关联的state接口。
  • 第三个参数是传递给到action creator的函数参数的类型,因为我们的action creator没有定义参数,这里传递null。
  • 最后一个参数是action的类型。

我们对这些action creator进行暴露,因为最终会被ProductsPage组件调用。

  1. 异步action需要返回一个异步函数,最终派遣我们的action:
1
2
3
4
5
6
7
8
9
export const getProducts: ActionCreator<ThunkAction<
Promise<AnyAction>,
IProductsState,
null,
IProductsGetAllAction
>> = () => {
return async (dispatch: Dispatch) => {
};
};

因此,函数第一件事是返回另一个函数,使用async关键字标记为异步的。内部函数以Dispatcher作为回调参数。

  1. 下面实现内部函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export const getProducts: ActionCreator<ThunkAction<
Promise<AnyAction>,
IProductsState,
null,
IProductsGetAllAction
>> = () => {
return async (dispatch: Dispatch) => {
dispatch(loading());
const products = await getProductsFromAPI();
return dispatch({
products,
type: ProductsActionTypes.GETALL,
});
};
};
  • 首先做的,dispatch 启动action,以得到最终的loading state。
  • 然后异步的方式,从模拟API获取商品信息
  • 最后一步dispatch 要求的action。

目前创建了好几个action了,接下来创建相应的reducer。

Creating reducers

一个reducer,就是一个传入给定的action,产生新的state的一个函数。因此,这个函数在当前state,接收了一个action,返回新的state。

  1. src目录新建文件ProductsReducer.ts
1
2
import { Reducer } from "redux";
import { IProductsState, ProductsActions, ProductsActionTypes } from "./ProductsTypes";

这里导入了Reducer依赖,以及前面定义的action和state。

  1. 定义初始state:
1
2
3
4
const initialProductState: IProductsState = {
products: [],
productsLoading: false
};

初始状态下,商品信息为空数组,处于非加载状态。

  1. 接下来创建reducer函数:
1
2
3
4
5
6
7
8
9
export const productsReducer: Reducer<IProductsState, ProductsActions> = (
state = initialProductState,
action
) => {
switch (action.type) {
// TODO - change the state
}
return state;
};
  • 该函数返回Reducer,包含有state和action。
  • 函数接收的参数由Redux提供。
  • 状态默认为初始化时的状态。
  • 对于不能识别的switch语句,返回默认的state。
  1. 实现我们商品的reducer:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
switch (action.type) {
case ProductsActionTypes.LOADING: {
return {
...state,
productsLoading: true
}
}
case ProductsActionTypes.GETALL: {
return {
...state,
products: action.products,
productsLoading: false
}
}
}

我们为每个action实现了对应的reducer,合并旧的state,并返回一个新的state。

这样,我们的第一个reducer完成了。接下来创建我们的store。

Creating a store

本小节,将创建一个store,用于存放我们的state并管理这些action和reducer:

  1. 新建文件Store.tsx文件,导入相应的需要的组件。
1
import { applyMiddleware, combineReducers, createStore, Store } from "redux";
  • createStore 创建store的函数
  • 我们需要applyMiddleware函数,因为我们使用了redux thunk中间件来管理异步action。
  • combineReducers函数用于合并reducer
  • Store是一个TypeScript版的store对象
  1. 导入redux-thunk
1
import thunk from "redux-thunk";
  1. 最后,导入reducer和state,
1
2
import { productsReducer } from "./ProductsReducer";
import { IProductsState } from "./ProductsTypes";
  1. store的关键部分是state。因此,定义一个接口:
1
2
3
export interface IApplicationState {
products: IProductsState;
}

这个接口仅仅包含了商品的state。

  1. 将reducer添加到Redux的combineReducer函数,
1
2
3
const rootReducer = combineReducers<IApplicationState>({
products: productsReducer
});
  1. 定义好state和reducer后,我们可以创建我们的store了。实际上我们是创建一个函数:
1
2
3
4
export default function configureStore(): Store<IApplicationState> {
const store = createStore(rootReducer, undefined, applyMiddleware(thunk));
return store;
}
  • 函数configureStore返回泛型Store
  • Redux中的函数createStore,我们传入定义的reducer以及Redux Thunk中间件,传递undefined作为初始化状态。

接下来,如何连接到我们创建的store?

Connecting our React app to the store

在本小节,我们将Products页面连接到store。第一件要做的工作室添加React Redux的Provider组件。

Adding the store Provider component

Provider组件可以在它任意下层的组件传递store。因此,本小节,需要将Provider添加到组件的最高层级,这样其它组件都可以访问。

  1. 打开原先的index.tsx,导入Provider
1
import { Provider } from "react-redux";
  1. 另外把Store也导入进来。
1
import { Store } from "redux";
  1. 其它用到的store和state也导入进来。
1
2
import configureStore from "./Store";
import { IApplicationState } from "./Store";
  1. 创建一些功能组件,
1
2
3
4
5
6
7
interface IProps {
store: Store<IApplicationState>;
}

const Root: React.SFC<IProps> = props => {
return();
};

这里的Root组件将会成为我们新的root element。它将store作为一个prop。

  1. 这样一来,我们需要导入旧的根元素,Routes,放置在新的root组件中:
1
2
3
4
5
const Root: React.SFC<IProps> = props => {
return (
<Routes />
);
};

另外还要把Provider组件加进来:

1
2
3
4
5
return (
<Provider store={props.store}>
<Routes />
</Provider>
);

现在已经将Provider组件的最上层,以及将store传递进去。

  1. 另外还要更改根部渲染函数,
1
2
const store = configureStore();
ReactDOM.render(<Root store={store} />, document.getElementById("root") as HTMLElement);

首先通过configureStore函数创建了全局的store,并将它传递给Root组件。

这样一来,所有组件都已经连接到了这个store。接下来,需要需要在其它子层组件中对其进行连接。

Connecting components to the store

Connecting ProductsPage to the store

首先连接的组件是ProductsPage,

下面开始对其进行重构,

  1. 导入connect函数,
1
import { connect } from "react-redux";

我们将使用connectProductsPage连接到store。

  1. 导入store的state,以及getProductsaction creator。
1
2
import { IApplicationState } from "./Store";
import { getProducts } from "./ProductsActions";
  1. 组件ProductPage不再包含任何state,因为将由Redux store装载。因此,需要将组件原有的state接口、静态方法getDerivedStateFromProps、以及构造器进行整改。ProductsPage原来的外形是:
1
2
3
4
class ProductsPage extends React.Component<RouteComponentProps> {
public async componentDidMount() { ... }
public render() { ... }
}
  1. 组件的数据将通过props从store获得。一次,重构props接口:
1
2
3
4
5
6
interface IProps extends RouteComponentProps {
getProducts: typeof getProducts;
loading: boolean;
products: IProduct[];
}
class ProductsPage extends React.Component<IProps> {...}

因此,我们会将下面信息经由store传递给该组件:

  • getProducts action creator

  • loading标志,表示当前是否在获取商品信息

  • 商品列表数组

  1. 接下来调整组件的生命周期方法componentDidMount,通过调用getProducts 这个action creator来获取商品信息:
1
2
3
public componentDidMount() {
this.props.getProducts();
}
  1. 现在不再直接从ProductsData.ts中获取products商品列表了。将导入语句移除掉:
1
import { IProduct } from "./ProductsData";
  1. 还有一个未使用的searchstate也不需要了。如下,我们原来仅仅是将它放置在render方法,
1
2
3
4
5
public render() {
const searchParams = new URLSearchParams(this.props.location.search);
const search = searchParams.get("search") || "";
return ( ... );
}
  1. 现在需要替换掉原来对state的引用:
1
2
3
4
5
<ul className="product-list">
{this.props.products.map(product => {
if (!search || (search && product.name.toLowerCase().indexOf(search.toLowerCase()) > -1)) { ... }
})}
</ul>
  1. 在export语句之前,class语句之后,创建一个函数来映射store和组件props的state:
1
2
3
4
5
6
const mapStateToProps = (store: IApplicationState) => {
return {
loading: store.products.productsLoading,
products: store.products.products
}
}

这样一来,我们可以得知商品是否在loading,以及从store存储的商品信息传递到我们的props中。

  1. 由前面得知,还有一个prop属性需要进行映射,即getProducts。因此创建另外一个函数来关联这种关系:
1
2
3
4
5
const mapDispatchToProps = (dispath: any) => {
return {
getProducts: () => dispatch(getProducts())
};
};
  1. 剩下最后一件事,需要在该文件最后。包装React Redux的connect HOC(钩子)到ProductsPage中:
1
2
3
4
export default connect (
mapStateToProps,
mapDispatchToProps
)(ProductsPage);

connect钩子将组件和Redux存储连接起来,由最高层的Provider提供。connect钩子会调用映射函数进行两者的state状态传递。

  1. 最后验证我们的结果:
1
npm start

我们会发现页面的行为并没有跟原来的有差异。唯一不同的是state现在由Redux store进行管理。

接下来的小节,我们将商品页面添加加载进度条。

Connecting ProductsPage to the loading store state

本小节将添加一个加载进度条。在此之前,需要将商品信息进行萃取。然后添加withLoaderHOC到组件中:

  1. 首先为抽取的组件创建一个新文件ProductsList.tsx
1
2
3
4
import * as React from 'react';
import {Link} from 'react-router-dom';
import {IProduct} from './ProductsData';
import withLoader from './withLoader';
  1. 该组件props作为商品数组信息和查询字符串的入参:
1
2
3
4
interface IProps {
products?: IProduct[];
search: string;
}
  1. 我们将调用ProductList组件作为一个SFC。
1
2
3
4
const ProductsList: React.SFC<IProps> = props => {
const search = props.search;
return ();
}
  1. 现在将来自组件ProductsPage组件的 ul标签语句迁移到ProductList组件的return语句中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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>
);

要注意的是,我们移除了原来this`的引用。

  1. 另外,还需要将组件暴露给钩子withLoader
1
export default withLoader(ProductsList);
  1. 修改原来ProductsPage.tsx组件的返回语句,用新提取的组件替代:
1
2
3
4
5
6
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>
);
  1. 以及在组件中引入引用:
1
import ProductsList from "./ProductsList";
  1. 最后,移除在ProductsPage.tsx组件中的Link引用。

    运行应用,进入Products页面,可以看到一个加载spinner:

    现在,我们的Products页面已经接入到Redux store。下个小节会将`Product也接入到store中。

Adding product stte and actions to the store

要将ProductPage组件连接到Redux存储中,需要创建额外的state,表示商品是否被添加到购物车。另外,需要额外的action和reducer,来表述获取商品、添加到购物篮的行为。

  1. 首先,在ProductsTypes.ts添加相应的state,表述“当前”商品:
1
2
3
4
export interface IProductsState {
readonly currentProduct: IProduct || null;
...
}
  1. 为获取商品的行为添加相应action:
1
2
3
4
5
export enum ProductsActionTypes {
GETALL = "PRODUCTS/GETALL",
GETSINGLE = "PRODUCTS/GETSINGLE",
LOADING = "PRODUCTS/LOADING"
}
  1. 为获取商品的行为添加action type:
1
2
3
4
export interface IProductsGetSingleAction {
type: ProductsActionTypes.GETSINGLE;
product: IProduct;
}
  1. 添加到联合类型(union actions type):
1
export type ProductsActions = IProductsGetAllAction | IProductsGetSingleAction | IProductsLoadingAction;
  1. 接着创建新的action creator。首先,导入假的api,用于表示获取商品:
1
import { getProduct as getProductFromAPI, getProducts as getProductsFromAPI } from "./ProductsData";
  1. 接着为action creator导入需要用到的类型:
1
import { IProductsGetAllAction, IPrudctsGetSingleAction, IProductsLoadingAction, IProductsState, ProductsActionType } from "./productsTypes";
  1. 实现action creator:
1
2
3
4
5
6
7
8
9
10
11
12
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非常相似。不同的是这里的入参是商品id。

  1. ProductsReducer.ts中,首先设置初始状态为null:
1
2
3
4
const initialProductState: IProductsState = {
currentProduct: null,
...
}
  1. productReducer函数中,添加相应新的分支语句:
1
2
3
4
5
6
7
8
9
10
switch (action.type) {
...
case ProductsActionTypes.GETSINGLE: {
return {
...state,
currentProduct: action.product,
productsLoading: false
}
}
}

Adding basket state and actions to the store

这里需要为购物篮添加状态管理。

  1. 首先,创建一个新文件BasketTypes.ts,内容如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
  • 这里仅一个state,代表购物篮中的商品数组信息。
  • 这里仅一个action,表示向购物篮添加商品。
  1. 新建一个文件BasketActions.ts,包含下面内容:
1
2
3
4
5
6
7
import {BasketActionTypes, IBasketAdd} from './BasketTypes';
import {IProduct} from './ProductsData';

export const addToBasket = (product: IProduct): IBasketAdd => ({
product,
type: BasketActionTypes.ADD,
});

这是商品添加到购物篮的action creator。该函数接收一个商品入参,返回相应的action。

  1. 在reducer中,创建文件BasketReducer.ts,内容如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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;
};

这里有趣的地方是,如何优雅地向products数组添加product,而不改变原来的数组变量信息。我们使用了JavaScript的concat函数,它会创建一个新的arrary,并将原来的数组以参数形式合并。

  1. 现在编译Store.ts,导入新的reducer和state:
1
2
import { basketReducer } from "./BasketReducer";
import { IBasketState } from "./BasketTypes";
  1. 将购物篮的state添加到store中:
1
2
3
4
export interface IApplicationState {
basket: IBasketState;
products: IProductsState;
}
  1. 现在有两个reducer。因此,添加购物篮reducer到combineReducers的函数调用中:
1
2
3
4
export const rootReducer = combineReducers<IApplicationState>({
basket: basketReducer,
products: productsReducer
})

现在可以从Store连接到ProductPage组件了。

Connecting ProductPage to the store

  1. 首先在ProductPage.tsx导入相应的组件:
1
2
3
4
import {connect} from 'react-redux';
import {addToBasket} from './BasketActions';
import {getProduct} from './ProductsActions';
import {IApplicationState} from './Store';
  1. 因为是通过getProduct从获取商品信息,ProductsData.ts不再需要用到。移除该导入:
1
import { IProduct } from "./ProductsData";
  1. 接着,将state挪到props属性:
1
2
3
4
5
6
7
8
interface IProps extends RouteComponentProps<{id: string}> {
addToBasket: typeof addToBasket;
getProduct: typeof getProduct;
loading: boolean;
product?: IProduct;
added: boolean;
}
class ProductPage extends React.Component<IProps> { ... }
  1. 我们可以移除掉构造函数,因为它不再需要初始化任何状态。
  2. 以及,需要在componentDidMount生命周期函数中调用相应的action creator获取商品信息:
1
2
3
4
5
6
public componentDidMount() {
if (this.props.match.params.id) {
const id: number = parseInt(this.props.match.params.id, 10);
this.props.getProduct(id);
}
}

注意我们也移除了async关键字,因为该方法不再是异步的。

  1. 移步到render函数,修改为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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>
);
}
  1. 在点击句柄中,对其重构为通过调用action creator来表示添加商品。
1
2
3
4
5
private handleAddClick = () => {
if (this.props.product) {
this.props.addToBasket(this.props.product);
}
};
  1. 最后一步。需要实现对应的mapDispatchToProps,映射关联store和组件的props。
1
2
3
4
5
6
const mapDispatchToProps = (dispath: any) => {
return {
addToBasket: (product: IProduct) => dispatch(addToBasket(product)),
getProduct: (id: number) => dispatch(getProduct(id))
}
}
  1. 以及添加state的映射。
1
2
3
4
5
6
7
const mapStateToProps = (store: IApplicationState) => {
return {
basketProducts: store.basket.products,
loading: store.products.productsLoading,
product: store.proucts.currentProduct || undefined
};
};

注意,我们将null的currentProduct映射为undefined

  1. 剩余的需要映射的属性是added。我们需要检测当前商品对应的store中,是否为“已添加至购物篮”,这里用到some函数对数组进行处理:
1
2
3
4
5
6
const mapStateToProps = (store: IApplicationState) => {
return {
added: store.basket.products.some(p => store.products.currentProduct ? p.id === store.products.currentProduct.id : false),
...
}
}
  1. 剩余步骤是,使用connect钩子连接ProductPage组件到store中:
1
2
3
4
export default connect (
mapStateToProps,
mapDispatchToProps
)(ProductPage);

现在回到页面App,尝试点击按钮,添加商品到购物篮看看效果,已添加的商品,再次进入商品的页面时,添加按钮会消失。

Creating and connecting BasketSummary to the store

在该小节,我们将创建新的组件BasketSummary。它会显示购物篮中商品的个数,并显示在右上角。

  1. 首先创建一个新的文件BasketSummary.tsx,内容如下:
1
2
3
4
5
6
7
8
9
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;

这是一个简单组件,props入参为商品个数,并显示值。

  1. index.class添加对应的CSS类,
1
2
3
4
5
6
.basket-summary {
display: inline-block;
margin-left: 10px;
padding: 5px 10px;
border: white solid 2px;
}
  1. 我们需要将该组件添加到header component中。因此,在Header.tsx添加:
1
2
3
import BasketSummary from "./BasketSummary";
import { connect } from "react-redux";
import { IApplicationState } from "./Store";
  1. IProps添加一个number属性:
1
2
3
4
5
6
7
interface IProps extends RouteComponentProps {
basketCount: number;
}
class Header extends React.Component<IProps, IState> {
public constructor(props: IProps) { ... }
...
}
  1. 添加BasketSummary组件到Header组件中:
1
2
3
4
5
6
7
<header className="header">
<div className="search-container">
<input ... />
<BasketSummary count={this.props.basketCount} />
</div>
...
</header>
  1. 最后一步就是添加映射:
1
2
3
4
5
const mapStateToProps = (store: IApplicationState) => {
return {
basketCount: store.basket.products.length
};
};
  1. 以及通过钩子暴露连接
1
export default connect(mapStateToProps)(withRouter(Header));

现在Header组件消费BasketSummary组件信息,并连接到store中。尝试在页面中添加商品信息,可以看到数字增加了。

Managing state with useReducer

Redux对于状态管理带来很大的帮助。但如果仅仅为了管理存在的单一组件的状态,则显得有点笨重。显然,我们,单一的组件直接使用setState(for class compoents)useState(for function compoents)就可以了。然而,对于复杂的组件状态会怎样?有一大堆的state信息,并且这些state的交互可能涉及很多操作,某些甚至可能是异步的。

在本小节,我们将探索使用useReduder函数来管理这些状态的方法。我们的例子将尽量人性化和简单,以理解这种管理方法。

我们希望添加一个_Like_ 按钮到*Product* 页面。用户可以多次点击这个like按钮。Product组件会跟踪点击这个按钮的次数、最后一次like的时间,并显示。

  1. 首先打开Product.tsx,创建一个接口,它包含了“like”的次数、最后“like”的时间:
1
2
3
4
interface ILikeState {
likes: number;
lastLike: Date | null;
}
  1. 创建一个变量表述初始状态:
1
2
3
4
const initialLikeState: ILikeState = {
likes: 0,
lastLike: null
}
  1. 创建action类型:
1
2
3
4
5
6
7
enum LikeActionTypes {
LIKE = "LIKE"
}
interface ILikeAction {
type: LikeActionTypes.LIKE;
now: Date;
}
  1. 创建一个联合类型包含所有这些action。在我们的例子中,虽然仅只有一个action type,先理解这种方式带来的扩展性的好处:
1
type LikeAction = ILikeAction;
  1. 在组件Product内部,让我们调用useReducer函数获取state和dispatch
1
const [state, dispatch]: [ILikeState, (action: ILikeAction) => void] = React.useReducer(reducer, initialLikeState);

让我们分解下:

  • 我们向useReducer传入一个函数reducer函数参数。
  • 另外也传入了useReducer初始状态。
  • useReducer返回一个数组包含两个元素。第一个元素为当前state,另一个是dispatch用于调用一个action。
  1. 让我们重构该行,对state进行解构,这样我们就可以直接引用一系列state:
1
2
3
4
const [{ likes, lastLike }, dispatch]: [
ILikeState,
(action: ILikeAction) => void
] = React.useReducer(reducer, initialLikeState);
  1. Product组件的底部,添加相应的“like”信息和按钮:
1
2
3
4
5
{!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>
  1. 添加CSS样式:
1
2
3
4
5
6
.like-container {
margin-top: 20px;
}
.like-container button {
margin-top: 5px;
}
  1. Like按钮实现点击事件处理:
1
2
3
const handleLikeClick = () => {
dispatch({ type: LikeActionTypes.LIKE, now: new Date() });
}
  1. 最后一步,在Product组件外部实现reducer函数,即在LikeActions类型下:
1
2
3
4
5
6
7
8
9
10
11
const reducer = (state: ILikeState = initialLikeState, action: LikeAction) => {
switch (action.type) {
case LikeAtionTypes.LIKE:
return {
...state,
likes: state.likes + 1,
lastLike: action.now
};
return state;
};
}

该实现方式和实现action、reducer相似,不同的是所有操作都在一个组件内实现。

Summary

下面是几个关键点:

  • 枚举类型的action type,在引用时能给我们更好的提示。
  • 使用接口来定义action带来更好的类型安全等级,这样允许我们创建联合类型(union type),提供给reducer使用。
  • 使用readonly定义的state属性,帮助我们避免对state的直接更改。
  • synchronous action creator返回action 对象。
  • Asynchronous action creator返回一个function,该function最终返回action对象。
  • reducer包含一系列的action type逻辑。
  • Redux提供的createStore创建实际的store。

要将组件衔接到store中,下面是几个关键点:

  • Provider组件需要放置在所有消费组件的顶部。它拥有一个prop定义了store。
  • connect钩子可以将独立的组件衔接到store中。它接收两个参数,用于将state和action creator映射到组件的props中。