如何在 React + TypeScript 应用中的更加便捷的使用 Redux-Toolkit?

avatar
前端 @CVTE_希沃

前言

本文介绍的主要内容是 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>
      <h2>Number of cakes - {numOfCakes}</h2>
      <h2>Name of cakes - {nameOfCake}</h2>
      <button onClick={updateName}>change cake's name</button>
      <button onClick={updateNum}>change cake's number</button>
    </div>
  );
};

现状的繁琐点:

  1. 每次使用 useSelector 来获取 redux 中状态的时候,都需要给 state 加一个 ts 类型 RootState
  2. 每次修改 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>
      <h2>Number of cakes - {numOfCakes}</h2>
      <h2>Name of cakes - {nameOfCake}</h2>
      <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 还会存在的一些繁琐点。