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:
安装redux,
安装具体的redux的关联框架,
对应TS,
1 yarn add -D @types/react-redux
安装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类型。
在src
文件夹创建一个新的文件ProductsTypes.ts
,
1 import { IProduct } from "./ProductsData" ;
添加两种不同类型的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
。我们只需要确保字符串是唯一的。
下面为上述两种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
用作加载状态。
组合为一个新的union type:
1 2 3 export type ProductsActions = | IProductsGetAllAction | IProductsLoadingAction
该类型将被传递在reducer的参数上。
最后,为这种存储状态创建一个接口:
1 2 3 4 export interface IProductsState { readonly products : IProduct []; readonly productsLoading : boolean ; }
这里的state将包含了一组商品信息,以及商品是否正在加载。
注意到属性前面引入了readonly
关键字。它帮助我们避免对状态的直接修改。
有了state和action的类型后,接下来创建一些具体的action。
Creating actions
本小节,将创建两个action。获取商品的action,商品处于加载的action。
创建一个ProductsActions.ts
文件,
1 import { ActionCreator , AnyAction , Dispatch } from "redux" ;
这里用到几个action类型需要实现。
其中一个action用作异步操作。需要导入redux-thunk
1 import { ThunkAction } from "redux-thunk" ;
另外导入之前的模拟API。
1 import { getProducts as getProductsFromAPI } from "./ProductsData" ;
这里需要重命名getProductsFromAPI
,避免和getProducts
的action冲突。
将先前定义的action类型导入。
1 import { IProductsGetAllAction , IProductsLoadingAction , IProductsState , ProductsActionTypes } from "./ProductsTypes" ;
接下来,创建一个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的实现。
添加另一个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
组件调用。
异步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 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。
在src
目录新建文件ProductsReducer.ts
:
1 2 import { Reducer } from "redux" ;import { IProductsState , ProductsActions , ProductsActionTypes } from "./ProductsTypes" ;
这里导入了Reducer
依赖,以及前面定义的action和state。
定义初始state:
1 2 3 4 const initialProductState : IProductsState = { products : [], productsLoading : false };
初始状态下,商品信息为空数组,处于非加载状态。
接下来创建reducer函数:
1 2 3 4 5 6 7 8 9 export const productsReducer : Reducer <IProductsState , ProductsActions > = ( state = initialProductState, action ) => { switch (action.type ) { } return state; };
该函数返回Reducer
,包含有state和action。
函数接收的参数由Redux提供。
状态默认为初始化时的状态。
对于不能识别的switch语句,返回默认的state。
实现我们商品的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:
新建文件Store.tsx
文件,导入相应的需要的组件。
1 import { applyMiddleware, combineReducers, createStore, Store } from "redux" ;
createStore
创建store的函数
我们需要applyMiddleware
函数,因为我们使用了redux thunk中间件来管理异步action。
combineReducers
函数用于合并reducer
Store
是一个TypeScript版的store对象
导入redux-thunk
:
1 import thunk from "redux-thunk" ;
最后,导入reducer和state,
1 2 import { productsReducer } from "./ProductsReducer" ;import { IProductsState } from "./ProductsTypes" ;
store的关键部分是state。因此,定义一个接口:
1 2 3 export interface IApplicationState { products : IProductsState ; }
这个接口仅仅包含了商品的state。
将reducer添加到Redux的combineReducer
函数,
1 2 3 const rootReducer = combineReducers<IApplicationState >({ products : productsReducer });
定义好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
添加到组件的最高层级,这样其它组件都可以访问。
打开原先的index.tsx
,导入Provider
。
1 import { Provider } from "react-redux" ;
另外把Store
也导入进来。
1 import { Store } from "redux" ;
其它用到的store和state也导入进来。
1 2 import configureStore from "./Store" ;import { IApplicationState } from "./Store" ;
创建一些功能组件,
1 2 3 4 5 6 7 interface IProps { store : Store <IApplicationState >; } const Root : React .SFC <IProps > = props => { return (); };
这里的Root
组件将会成为我们新的root element。它将store作为一个prop。
这样一来,我们需要导入旧的根元素,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 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
,
下面开始对其进行重构,
导入connect
函数,
1 import { connect } from "react-redux" ;
我们将使用connect
将ProductsPage
连接到store。
导入store的state,以及getProducts
action creator。
1 2 import { IApplicationState } from "./Store" ;import { getProducts } from "./ProductsActions" ;
组件ProductPage
不再包含任何state,因为将由Redux store装载。因此,需要将组件原有的state接口、静态方法getDerivedStateFromProps
、以及构造器进行整改。ProductsPage
原来的外形是:
1 2 3 4 class ProductsPage extends React.Component <RouteComponentProps > { public async componentDidMount ( ) { ... } public render ( ) { ... } }
组件的数据将通过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传递给该组件:
接下来调整组件的生命周期方法componentDidMount
,通过调用getProducts
这个action creator来获取商品信息:
1 2 3 public componentDidMount ( ) { this .props .getProducts (); }
现在不再直接从ProductsData.ts
中获取products
商品列表了。将导入语句移除掉:
1 import { IProduct } from "./ProductsData" ;
还有一个未使用的search
state也不需要了。如下,我们原来仅仅是将它放置在render
方法,
1 2 3 4 5 public render ( ) { const searchParams = new URLSearchParams (this .props .location .search ); const search = searchParams.get ("search" ) || "" ; return ( ... ); }
现在需要替换掉原来对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>
在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中。
由前面得知,还有一个prop属性需要进行映射,即getProducts
。因此创建另外一个函数来关联这种关系:
1 2 3 4 5 const mapDispatchToProps = (dispath: any ) => { return { getProducts : () => dispatch (getProducts ()) }; };
剩下最后一件事,需要在该文件最后。包装React Redux的connect
HOC(钩子)到ProductsPage
中:
1 2 3 4 export default connect ( mapStateToProps, mapDispatchToProps )(ProductsPage );
connect
钩子将组件和Redux存储连接起来,由最高层的Provider
提供。connect
钩子会调用映射函数进行两者的state状态传递。
最后验证我们的结果:
我们会发现页面的行为并没有跟原来的有差异。唯一不同的是state现在由Redux store进行管理。
接下来的小节,我们将商品页面添加加载进度条。
Connecting ProductsPage to the loading store state
本小节将添加一个加载进度条。在此之前,需要将商品信息进行萃取。然后添加withLoader
HOC到组件中:
首先为抽取的组件创建一个新文件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' ;
该组件props作为商品数组信息和查询字符串的入参:
1 2 3 4 interface IProps { products?: IProduct []; search : string ; }
我们将调用ProductList
组件作为一个SFC。
1 2 3 4 const ProductsList : React .SFC <IProps > = props => { const search = props.search ; return (); }
现在将来自组件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`的引用。
另外,还需要将组件暴露给钩子withLoader
:
1 export default withLoader (ProductsList );
修改原来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 import ProductsList from "./ProductsList" ;
最后,移除在ProductsPage.tsx
组件中的Link
引用。
运行应用,进入Products
页面,可以看到一个加载spinner:
现在,我们的Products
页面已经接入到Redux store。下个小节会将`Product也接入到store中。
Adding product stte and actions to the store
要将ProductPage
组件连接到Redux存储中,需要创建额外的state,表示商品是否被添加到购物车。另外,需要额外的action和reducer,来表述获取商品、添加到购物篮的行为。
首先,在ProductsTypes.ts
添加相应的state,表述“当前”商品:
1 2 3 4 export interface IProductsState { readonly currentProduct : IProduct || null ; ... }
为获取商品的行为添加相应action:
1 2 3 4 5 export enum ProductsActionTypes { GETALL = "PRODUCTS/GETALL" , GETSINGLE = "PRODUCTS/GETSINGLE" , LOADING = "PRODUCTS/LOADING" }
为获取商品的行为添加action type:
1 2 3 4 export interface IProductsGetSingleAction { type : ProductsActionTypes .GETSINGLE ; product : IProduct ; }
添加到联合类型(union actions type):
1 export type ProductsActions = IProductsGetAllAction | IProductsGetSingleAction | IProductsLoadingAction ;
接着创建新的action creator。首先,导入假的api,用于表示获取商品:
1 import { getProduct as getProductFromAPI, getProducts as getProductsFromAPI } from "./ProductsData" ;
接着为action creator导入需要用到的类型:
1 import { IProductsGetAllAction , IPrudctsGetSingleAction , IProductsLoadingAction , IProductsState , ProductsActionType } from "./productsTypes" ;
实现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。
在ProductsReducer.ts
中,首先设置初始状态为null:
1 2 3 4 const initialProductState : IProductsState = { currentProduct : null , ... }
在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
这里需要为购物篮添加状态管理。
首先,创建一个新文件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,表示向购物篮添加商品。
新建一个文件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。
在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,并将原来的数组以参数形式合并。
现在编译Store.ts
,导入新的reducer和state:
1 2 import { basketReducer } from "./BasketReducer" ;import { IBasketState } from "./BasketTypes" ;
将购物篮的state添加到store中:
1 2 3 4 export interface IApplicationState { basket : IBasketState ; products : IProductsState ; }
现在有两个reducer。因此,添加购物篮reducer到combineReducers
的函数调用中:
1 2 3 4 export const rootReducer = combineReducers<IApplicationState >({ basket : basketReducer, products : productsReducer })
现在可以从Store连接到ProductPage
组件了。
Connecting ProductPage to the store
首先在ProductPage.tsx
导入相应的组件:
1 2 3 4 import {connect} from 'react-redux' ;import {addToBasket} from './BasketActions' ;import {getProduct} from './ProductsActions' ;import {IApplicationState } from './Store' ;
因为是通过getProduct
从获取商品信息,ProductsData.ts
不再需要用到。移除该导入:
1 import { IProduct } from "./ProductsData" ;
接着,将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 > { ... }
我们可以移除掉构造函数,因为它不再需要初始化任何状态。
以及,需要在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
关键字,因为该方法不再是异步的。
移步到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 > ); }
在点击句柄中,对其重构为通过调用action creator来表示添加商品。
1 2 3 4 5 private handleAddClick = () => { if (this .props .product ) { this .props .addToBasket (this .props .product ); } };
最后一步。需要实现对应的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)) } }
以及添加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
。
剩余的需要映射的属性是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 ), ... } }
剩余步骤是,使用connect
钩子连接ProductPage
组件到store中:
1 2 3 4 export default connect ( mapStateToProps, mapDispatchToProps )(ProductPage );
现在回到页面App,尝试点击按钮,添加商品到购物篮看看效果,已添加的商品,再次进入商品的页面时,添加按钮会消失。
Creating and connecting BasketSummary to the store
在该小节,我们将创建新的组件BasketSummary
。它会显示购物篮中商品的个数,并显示在右上角。
首先创建一个新的文件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入参为商品个数,并显示值。
在index.class
添加对应的CSS类,
1 2 3 4 5 6 .basket-summary { display : inline-block; margin-left : 10px ; padding : 5px 10px ; border : white solid 2px ; }
我们需要将该组件添加到header component中。因此,在Header.tsx
添加:
1 2 3 import BasketSummary from "./BasketSummary" ;import { connect } from "react-redux" ;import { IApplicationState } from "./Store" ;
在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 ) { ... } ... }
添加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 2 3 4 5 const mapStateToProps = (store: IApplicationState ) => { return { basketCount : store.basket .products .length }; };
以及通过钩子暴露连接
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的时间,并显示。
首先打开Product.tsx
,创建一个接口,它包含了“like”的次数、最后“like”的时间:
1 2 3 4 interface ILikeState { likes : number ; lastLike : Date | null ; }
创建一个变量表述初始状态:
1 2 3 4 const initialLikeState : ILikeState = { likes : 0 , lastLike : null }
创建action类型:
1 2 3 4 5 6 7 enum LikeActionTypes { LIKE = "LIKE" } interface ILikeAction { type : LikeActionTypes .LIKE ; now : Date ; }
创建一个联合类型包含所有这些action。在我们的例子中,虽然仅只有一个action type,先理解这种方式带来的扩展性的好处:
1 type LikeAction = ILikeAction ;
在组件Product
内部,让我们调用useReducer
函数获取state和dispatch
:
1 const [state, dispatch]: [ILikeState , (action: ILikeAction ) => void ] = React .useReducer (reducer, initialLikeState);
让我们分解下:
我们向useReducer
传入一个函数reducer
函数参数。
另外也传入了useReducer
初始状态。
useReducer
返回一个数组包含两个元素。第一个元素为当前state,另一个是dispatch
用于调用一个action。
让我们重构该行,对state进行解构,这样我们就可以直接引用一系列state:
1 2 3 4 const [{ likes, lastLike }, dispatch]: [ ILikeState , (action: ILikeAction ) => void ] = React .useReducer (reducer, initialLikeState);
在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>
添加CSS样式:
1 2 3 4 5 6 .like -container { margin-top : 20px; } .like -container button { margin-top : 5px; }
为Like
按钮实现点击事件处理:
1 2 3 const handleLikeClick = ( ) => { dispatch ({ type : LikeActionTypes .LIKE , now : new Date () }); }
最后一步,在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中。