React TypeScript 应用中便捷使用Redux Toolkit方法详解
BokFang 人气:0前言
本文介绍的主要内容是 Redux-Toolkit 在 React + TypeScript 大型应用中的实践,主要解决的问题是使用 createSlice 的前提下消费 redux 状态仍旧有点繁琐的问题。
阅读本文需要的前置知识:了解 React 、Redux-Toolkit 、TypeScript 的使用。
关于 Redux-Toolkit 提供的各种函数的使用,大家可以去官网 redux-toolkit.js.org/ 学习。
背景
前阵子接到一个任务:在使用 redux 作为状态管理工具的前提下,优化一下消费 redux 的步骤,并制定一套使用规范,让大家在开发这个项目消费 redux 状态时能按照规范来。
说到简化消费 redux 步骤,我第一时间想到的就是 redux 官方推荐的 Redux-Toolkit,于是我就去学习了一下 Redux-Toolkit。
了解完官网和网上各种文章后,我知道了 Redux-Toolkit 在项目中的使用,但是仍然有一个疑问:使用了 createSlice 后,仍然需要在项目的组件中使用 useDispatch 来更新状态,还是有点麻烦。对于组件使用者来说,有没有更方便的方式消费 redux 状态?
在网上逛来逛去,没找到有人发相关的文章,所以我准备自己动手试试看。
Redux-Toolkit 常规使用
我们先来看看目前使用 Redux-Toolkit 消费 redux 状态的方式。举个例子,假设我们现在的业务和蛋糕有关,有两个状态存在 redux,分别是 nameOfCake 和 numOfCakes,我们使用 createSlice 来创建 reducer 和 actions:
// 文件位置: app/src/store/reducers/cake.ts import { createSlice, PayloadAction } from '@reduxjs/toolkit'; type InitialState = { numOfCakes: number; nameOfCake: string; }; const initialState: InitialState = { numOfCakes: 20, nameOfCake: 'great cake', }; const cakeSlice = createSlice({ name: 'cake', initialState, reducers: { updateCakeNum: (state, action: PayloadAction<number>) => { state.numOfCakes = action.payload; }, updateCakeName: (state, action: PayloadAction<string>) => { state.nameOfCake = action.payload; }, }, }); export default cakeSlice.reducer; export const { updateCakeNum, updateCakeName } = cakeSlice.actions;
接着我们在组件中消费:
// 文件位置: app/src/pages/Cake/components/CakeView/index.tsx import { RootState } from '@/src/store'; import { useSelector, useDispatch } from 'react-redux'; import { useRecordReduxFunction, updateCakeNum, updateCakeName, } from '@/src/store/reducers/cake'; export const CakeView = () => { const numOfCakes = useSelector((state: RootState) => state.cake.numOfCakes); const nameOfCake = useSelector((state: RootState) => state.cake.nameOfCake); const dispatch = useDispatch(); const updateNum = () => { dispatch(updateCakeNum(100)); }; const updateName = () => { dispatch(updateCakeName('best cake')); }; return ( <div> <h3>Number of cakes - {numOfCakes}</h3> <h3>Name of cakes - {nameOfCake}</h3> <button onClick={updateName}>change cake's name</button> <button onClick={updateNum}>change cake's number</button> </div> ); };
现状的繁琐点:
- 每次使用 useSelector 来获取 redux 中状态的时候,都需要给 state 加一个 ts 类型 RootState
- 每次修改 redux 状态时,都需要引入 useDispatch 和一个 updateData,再将调用 updateData 返回的action 给 dispatch
我们的目标就是优化现状的这两个繁琐点。
优化方案
优化 useDispatch 和 useSelector
对于繁琐点1,我们可以对 useDispatch 和 useSelector 进行简单的封装,增加 ts 类型校验。代码如下:
// 文件位置: app/src/hooks/useReduxHook.ts import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; import type { RootState, AppDispatch } from '../store'; export const useAppDispatch = () => useDispatch<AppDispatch>(); export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
其中 RootState 和 AppDispatch 在 store 中导出:
import { configureStore } from '@reduxjs/toolkit'; import cakeReducer from './reducers/cake'; const store = configureStore({ reducer: { cake: cakeReducer, } }) export default store; export type RootState = ReturnType<typeof store.getState>; export type AppDispatch = typeof store.dispatch;
通过简单的封装,我们在业务层就用 useAppSelector 和 useAppDispatch 代替 useSelector 和 useDispatch:
import { useAppSelector, useAppDispatch } from '@src/hooks/useReduxHook'; // ... export const CakeView = () => { const numOfCakes = useAppSelector((state) => state.cake.numOfCakes); const nameOfCake = useAppSelector((state) => state.cake.nameOfCake); const dispatch = useAppDispatch(); // ... return ( // ... ); };
可以看到这样就有 ts 类型提示或者校验了:
优化修改 redux 状态的步骤
我们现在想要在业务组件中修改 redux 状态,我们就需要引入 useDispatch 和通过 createSlice 生成的 updateData 函数,再调用 dispatch(updateData(data))
来更新状态。
能不能优化成,业务组件只需要调用一个 updateData 函数,就可以更新 redux 状态呢?
最简单的方式就是将 dispatch(updateData(data))
给抽出去:
const useUpdateCakeName = () => { const dispatch = useAppDispatch(); return (payload: InitialState['nameOfCake']) => { dispatch(updateCakeName(payload)); }; };
但是这对于开发者来讲,换汤不换药,还是需要写一个 hook 来 dispatch action。
那我们就来写一个能自动生成 「用于dispatch action 的 updateData 函数」的 hook 吧。先来个 js 版:
const useCakeReduxFunction = (action) => { const dispatch = useAppDispatch(); return (payload) => { dispatch(cakeSlice.actions[action](payload)); }; };
这个倒是好用了一些,业务组件使用起来是这样的:
import { useCakeReduxFunction } from '@/src/store/reducers/cake'; // ... export const CakeView = () => { const updateNum = useCakeReduxFunction('updateCakeNum'); const updateName = useCakeReduxFunction('updateCakeName'); // ... return ( // ... ); };
但是还有个问题,这样的话每个 reducer 里都要写一个 useDataReduxFunction,还是不够便捷。我们就自然而然的想到写一个 useCreateReduxFunction 来简化开发流程。useCreateReduxFunction 的代码如下;
type GetArrFirst<T> = T extends [infer Res, ...infer P] ? Res : unknown; export const useCreateReduxFunction = <S extends Slice>(slice: S) => { return <T extends keyof S['actions']>(name: T) => { const dispatch = useAppDispatch(); const actionCreator = (slice.actions as S['actions'])[name]; return (payload: GetArrFirst<Parameters<typeof actionCreator>>) => { dispatch(actionCreator(payload)); }; }; };
这样就好办了,我们在 reducer 中这样使用:
import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { useCreateReduxFunction } from '../../app/hooks'; type InitialState = { numOfCakes: number; cakeName: string; }; const initialState: InitialState = { numOfCakes: 20, cakeName: 'great cake', }; const cakeSlice = createSlice({ name: 'cake', initialState, reducers: { updateCakeNum: (state, action: PayloadAction<number>) => { state.numOfCakes = action.payload; }, updateCakeName: (state, action: PayloadAction<string>) => { state.cakeName = action.payload; }, }, }); export const useCakeReduxFunction = useCreateReduxFunction(cakeSlice); export default cakeSlice.reducer; export const { updateCakeNum, updateCakeName } = cakeSlice.actions;
在业务方这么使用:
import { useAppSelector } from '@src/hooks/useReduxHook'; import { useCakeReduxFunction } from '@src/store/reducers/cake'; export const CakeView = () => { const numOfCakes = useAppSelector((state) => state.cake.numOfCakes); const nameOfCake = useAppSelector((state) => state.cake.cakeName); const updateNum = useCakeReduxFunction('updateCakeNum'); const updateName = useCakeReduxFunction('updateCakeName'); return ( <div> <h3>Number of cakes - {numOfCakes}</h3> <h3>Name of cakes - {nameOfCake}</h3> <button onClick={() => {updateName('best cake')}}>change cake's name</button> <button onClick={() => {updateNum(100)}}>change cake's number</button> </div> ); };
这样就达成我们的目标了。
总结
核心代码如下:
type GetArrFirst<T> = T extends [infer Res, ...infer P] ? Res : unknown; export const useAppDispatch = () => useDispatch<AppDispatch>(); export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector; export const useCreateReduxFunction = <S extends Slice>(slice: S) => { return <T extends keyof S['actions']>(name: T) => { const dispatch = useAppDispatch(); const actionCreator = (slice.actions as S['actions'])[name]; return (payload: GetArrFirst<Parameters<typeof actionCreator>>) => { dispatch(actionCreator(payload)); }; }; };
经过上面的优化方案,我们封装了 useAppSelector 、 useAppDispatch 和 useCreateReduxFunction三个 hook,解决了目前使用 Redux-Toolkit 还会存在的一些繁琐点。
加载全部内容