React+Typescript+Redux的使用步骤

1,338 阅读5分钟

​ 这是我参与11月更文挑战的第6天,活动详情查看:2021最后一次更文挑战

 目前的项目是基于Typescript下的React,接入Redux的话在部分接口声明和定义的地方是需要注意的,这里整理一下步骤并进行记录。

1. 安装Redux基本环境

我们目前需要使用的Redux相关库包括redux、react-redux、redux-saga,所以首先需要做的是通过npm或者yarn安装对应的库,注意这其中react-redux需要加上@types的前缀才行:

npm install --save @types/react-redux

这部分和是否TS环境下安装过程基本一致,只是注意有一些库需不需要@types声明就行了。

2. 为项目引入Redux

接着我们需要为我们的项目引入Redux,如果你之前从未使用过Redux,建议你先看看文章了解一下Redux的基本使用,

如果你已经了解了那么下面我就重点从需要注意的地方入手,这一章节只说一下Redux基础的文件创建过程。

2.1 rootReducer的创建

首先我们需要创建一个根Reducer,这个Reducer是通过combineReducers方法将各个子模块的Reducer进行整合(这部分其实和TS无关):

src/redux/reducers.tsx

import {combineReducers} from 'redux';
import {fileManagerReducer} from "../modules/workspace/containers/FileManager/reducer";

const rootReducer = combineReducers({
    fileManager: fileManagerReducer
});

export default rootReducer;
export type RootState = ReturnType<typeof rootReducer>;

这里我们只写了一个子模块作为示例。

这里注意我们需要export的包括rootReducer还有一个类型:RootState,因为后面在进行mapStateToProps的过程中我们需要给传入的state声明一个类型,而这个RootState就是他的类型!

2.2 生成store

这部分跟TS关系也不大,我就直接贴代码了:

src/redux/index.tsx

import createSagaMiddleware from "redux-saga";
import {applyMiddleware, createStore, compose} from "redux";
import rootReducer from "./rootReducer";
import myMiddleware from "./middleware";

//添加中间件和调试工具
const sagaMiddleware = createSagaMiddleware();
const composeEnhancers =
    typeof window === 'object' &&
    (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?
        (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose;
const store = createStore(rootReducer, composeEnhancers(applyMiddleware(myMiddleware, sagaMiddleware)));
// sagaMiddleware.run(mainSaga);

export default store;

我这边使用了中间件和Redux调试工具所以createStore多了一些参数。

2.3 包裹根组件

接着需要把根组件进行包裹,让其中的组件都可以使用Redux,这个是react-redux库的功能:

src/index.tsx

import React from "react";
import ReactDOM from "react-dom";
import {Provider} from 'react-redux';
import MainFrame from './components/MainFrame';
import store from "./redux";


ReactDOM.render(
    <Provider store={store}>
        <MainFrame/>
    </Provider>,
    document.getElementById('root') as HTMLElement
);

通过以上步骤就为我们的项目引入了Redux了,而下面我将具体举一个实例来分析针对某一个具体模块使用redux的步骤和注意事项。

3. 子模块中使用Redux

接着我们需要在一个列表子模块中实现这样一个功能,列表可以渲染state中的一个数组,同时列表中需要提供一个新增按钮来新增一个测试数据,同时实时更新当前列表。

3.1 创建types文件

首先我们需要创建一个types文件,这个文件主要定义这个模块中使用到的state和action的接口类型,这也是TS中的特色之一:

//State
export interface CardFile {
    id: string
    title: string
    creator: string
    desc: string
}

export interface FileManagerState {
    cardFiles: CardFile[]
    selectCardId: string
}

//Action
export const MAKE_NEW_CARD = 'MAKE_NEW_CARD';
export const SELECT_CARD = 'SELECT_CARD';

interface MakeNewCardAction {
    type: typeof MAKE_NEW_CARD
    payload: CardFile
}

interface SelectCardAction {
    type: typeof SELECT_CARD
    selectCardId: number | string
}

export type FileManagerActionTypes = MakeNewCardAction | SelectCardAction;

这里如果有多个Action,最后声明的Types就用|来表示。

3.2 回顾一下Redux的工作流

接着我们需要根据Redux的工作流来分别定义Action和Reducer了,这里我们利用下面这张图简单回顾一下吧:

---图片系统抽风了,后面能显示的话我贴下图

如果一个组件需要读取Store中存储的数据,那么他需要获取store并getState来获取其中的数据。

如果一个组件需要修改Store中存储的数据,那么他需要创建一个Action并Disptach出去,传递到Reducer生成一个新的State从而重新渲染原先的界面。

这里具体详细的步骤就不展开说了,不清楚的可以再去Google一下

3.3 分别创建子模块的reducer和actions

reducer

src/modules/workspace/containers/FileManager/reducer.tsx

import {FileManagerActionTypes, FileManagerState, MAKE_NEW_CARD, SELECT_CARD} from './types'

const initialState: FileManagerState = {
    cardFiles: [
        {
            id: '100',
            title: 'CardNo1',
            creator: 'Joern',
            desc: '空'
        },
        {
            id: '101',
            title: 'CardNo2',
            creator: 'Joern',
            desc: '空'
        },
        {
            id: '102',
            title: 'CardNo3',
            creator: 'Joern',
            desc: '空'
        },
    ],
    selectCardId: '100'
};

export function fileManagerReducer(
    state = initialState,
    action: FileManagerActionTypes
): FileManagerState {
    switch (action.type) {
        case MAKE_NEW_CARD:
            return Object.assign({}, state, {
                cardFiles: [
                    ...state.cardFiles,
                    action.payload
                ]
            });
        case SELECT_CARD:
            console.log(action.selectCardId);
            return Object.assign({}, state, {
                selectCardId: action.selectCardId
            });
        default:
            return state
    }
}

因为使用了TS,所以对应的state和action都需要进行类型声明,这里的类型就是我们之前在types文件里面声明好的,这里直接使用就可以了。

这里我们关注Reducer接受到一个type为MAKE_NEW_CARD类型的Action时,他会在原先State基础上更新cardFiles对象,将action的payload传入生成一个新的数组。

actions

src/modules/workspace/containers/FileManager/actions.tsx

import {CardFile, FileManagerActionTypes, MAKE_NEW_CARD, SELECT_CARD} from './types'

export function makeNewCard(newCard: CardFile): FileManagerActionTypes {
    return {
        type: MAKE_NEW_CARD,
        payload: newCard
    }
}

export function selectCard(selectedCardId: number | string): FileManagerActionTypes {
    return {
        type: SELECT_CARD,
        selectCardId: selectedCardId
    }
}

这里就是定义了两个Action,注意需要声明好Action的类型,并且传参的使用也需要和对应的字段类型匹配。

3.4 子模块主文件的编写

我们的主文件是index文件,在这个文件里面我们需要处理读取state和新增发送action的逻辑,我们来看一下。

react-redux的映射函数

如果基于react-redux库我们知道需要定义两个映射的函数,反别将State映射到Props,将Action插入到Props:

// 将reducer中的状态插入到组件的 props 中
const mapStateToProps = (state: RootState) => {
    const {fileManager} = state;
    return {
        cardFiles: fileManager.cardFiles,
        selectCardId: fileManager.selectCardId
    }
};

// 将对应action 插入到组件的props中
const mapDispatchToProps = (dispatch: Dispatch<AnyAction>) => {
    return {
        handleItemClick: (index: number | string) => {
            dispatch(selectCard(index));
        },
        makeNewCard: (newCard: CardFile) => {
            dispatch(makeNewCard(newCard));
        }
    }
};

这里有一个需要注意的地方,如果你使用的Reducer是基于combine来进行组合的,那么你的state的接口定义的是全局combine之后的Reducer,你需要获取具体的子Reducer才可以成功获取其中的state!

渲染列表

好了接着可以开始写TSX逻辑了,记得组件的Props先需要声明哈:

interface IProps {
    cardFiles: CardFile[];
    handleItemClick: (index: number | string) => void
    makeNewCard: (card: CardFile) => void
}
//测试数据-添加用的
const testNewCard: CardFile = {
    creator: "New", desc: "111", id: "98", title: "NewCard"
};

class FileManager extends Component<IProps> {...}

我们先实现拿到cardFiles来渲染List吧,这里直接贴代码:

class FileManager extends Component<IProps> {

    public render() {
        const {cardFiles, handleItemClick, makeNewCard} = this.props;
        console.log(this.props);
        return (
            <div id="file-manager">
                  ....
                <div id="file-manager-list">
                    <List
                        size="small"
                        itemLayout="horizontal"
                        dataSource={cardFiles}
                        split={false}
                        renderItem={(item, index) => (
                            // Antd的默认间距太大
                            <List.Item style={{padding: '0px'}} onClick={() => handleItemClick(item.id)}>
                                <List.Item.Meta
                                    avatar={<Icon type="form"/>}
                                    title={
                                        <div>
                                            <Icon type="file"/>
                                            <Button type={"link"}>{item.title}</Button>
                                        </div>
                                    }
                                />
                            </List.Item>
                        )}
                    />
                </div>
            </div>
        )
    }
}

直接通过this.props.cardFiles拿到就可以使用了,注意List中的item类型就是一个cardFile,如果使用IDE你会发现会有代码提示补齐,这也是使用TS的好处之一~

新增一个Card

接着我们希望新增一个Card并实时渲染出来,我们直接在TSX中嵌入这么一段:

<Button
    style={{fontSize: '12px'}}
    className={styles.newPage}
    type="link"
    icon="file-add"
    onClick={() => makeNewCard(testNewCard)}>
    新建Card
</Button>

这样就可以了,点击之后就会生成对应的action并传递给reducer进行cardFiles的新增,之后state会出现差异,于是对应的列表就会重新渲染拉。

注意点

这里还有一个注意事项就是onClick这些事件传递函数的不能这样写,会报错的:(图片上传不了,不知道为啥,其实就是你直接调用函数的话会报错,如果传递函数需要在TSX里面入参的话就要用箭头函数构造一遍!)

主要是因为返回类型的校验

有误,需要构造一个箭头函数解决!

以上基本就完成了一个基于React在TS环境下的Redux工作流的搭建工作了~

4. 参考资料

这里贴出一下我参考和解决问题的链接,供大家深入学习:

合理布置Redux目录:如何合理布置React/Redux的目录结构 - 掘金

官方Redux在TS中使用:Usage With TypeScript | Redux

TypeScript + React + Redux 的个人总结:TypeScript + React + Redux 的个人总结 - 简书

React And TypeScript(二:集成Redux):React And TypeScript(二:集成Redux)_JUSTIN的博客-CSDN博客

Reducer详述:cn.redux.js.org/docs/basics…

React状态管理框架Redux使用:React状态管理框架Redux使用 - 掘金