用redux-toolkit 改造你的redux

20,216 阅读3分钟

背景

如果你的React项目中使用react hook、redux、redux-thunk,可能你需要用 redux-toolkit (以下简称RTK)优化你的项目结构,它看起来可以这么清爽

Redux 使用常见问题

  • 配置复杂,devtool...
  • 模板代码太多,创建constant,action,reducer...
  • 需要添加很多依赖包,如redux-thunk、immer...
# 优化前
 /counter
    constants.ts
    actions.ts
    reducer.ts
    saga.ts
    index.tsxÏ
# 优化后
/counter
    slice.ts
    index.tsx

RTK干了哪些事?

  • configureStore() 包裹createStore,并集成了redux-thunkRedux DevTools Extension,默认开启
  • createReducer() 创建一个reducer,action type 映射到 case reducer 函数中,不用写switch-case,并集成immer
  • createAction() 创建一个action,传入动作类型字符串,返回动作函数
  • createSlice() 创建一个slice,包含 createReducer、createAction的所有功能
  • createAsyncThunk() 创建一个thunk,接受一个动作类型字符串和一个Promise的函数
  • ...

怎么样使用

以 Counter Component 为例

新的项目

create-react-app 初始化项目,最受欢迎的脚手架之一

# 使用 redux-typescript 模板,推荐使用typescript
npx create-react-app react-rtk-ts --template redux-typescript

# 使用redux 模板
# npx create-react-app react-rtk-ts --template redux

老的项目

  1. 安装 @reduxjs/toolkit
# 使用 npm
npm install @reduxjs/toolkit

# 使用 yarn
yarn add @reduxjs/toolkit
  1. configureStore 替换 createStore
import React from "react";
import { render } from "react-dom";
- import { createStore } from "redux";
+ import { configureStore } from "@reduxjs/toolkit";
import { Provider } from "react-redux";
import App from "./components/App";
import rootReducer from "./reducers";

- const store = createStore(rootReducer);
+ const store = configureStore({
+   reducer: rootReducer,
+});

创建Action

# 创建 action
const increment = createAction('INCREMENT')
const decrement = createAction('DECREMENT')

# 创建reducer
const counter = createReducer(0, {
  [increment]: state => state + 1,
  [decrement]: state => state - 1
})

以上看起比原来结构上好一些,创建action、reducer方便了,但是看着还是不爽,action也可以去掉

创建Slice

const counterSlice = createSlice({
  name: 'counter',
  initialState: 0,
  reducers: {
    increment: state => state + 1,
    decrement: state => state - 1
  }
})

# action
counterSlice.action;
# reducer
counterSlice.reducer;

counterSlice 看起起来像

{
  name: "counter",
  reducer: (state, action) => newState,
  actions: {
    increment: (payload) => ({type: "counter/increment", payload}),
    increment: (payload) => ({type: "counter/increment", payload})
  },
  caseReducers: {
    increment: (state, action) => newState,
    increment: (state, action) => newState,
  }
}

只需要createSlice,包含着 action,reducer的创建

创建 thunk

# 对于一部请求API,可以配合async/await使用
export const incrementAsync = (amount: number): AppThunk => dispatch => {
  setTimeout(() => {
    dispatch(incrementByAmount(amount));
  }, 1000);
};

创建 selecter

# 配合 react-redux 中 useSelector hook使用
export const selectCount = (state: RootState) => state.counter.value;

完整 slice文件

import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { AppThunk, RootState } from '../../app/store';

interface CounterState {
  value: number;
}

const initialState: CounterState = {
  value: 0,
};

export const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    increment: state => {
      state.value += 1;
    },
    decrement: state => {
      state.value -= 1;
    },
    incrementByAmount: (state, action: PayloadAction<number>) => {
      state.value += action.payload;
    },
  },
});

export const { increment, decrement, incrementByAmount } = counterSlice.actions;

export const incrementAsync = (amount: number): AppThunk => dispatch => {
  setTimeout(() => {
    dispatch(incrementByAmount(amount));
  }, 1000);
};

export const selectCount = (state: RootState) => state.counter.value;

export default counterSlice.reducer;

页面使用

import React, { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import {
  decrement,
  increment,
  incrementByAmount,
  incrementAsync,
  selectCount,
} from './counterSlice';
import styles from './Counter.module.css';

export function Counter() {
  const count = useSelector(selectCount);
  const dispatch = useDispatch();
  const [incrementAmount, setIncrementAmount] = useState('2');

  return (
    <div>
      <div className={styles.row}>
        <button
          className={styles.button}
          aria-label="Increment value"
          onClick={() => dispatch(increment())}
        >
          +
        </button>
        <span className={styles.value}>{count}</span>
        <button
          className={styles.button}
          aria-label="Decrement value"
          onClick={() => dispatch(decrement())}
        >
          -
        </button>
      </div>
      <div className={styles.row}>
        <input
          className={styles.textbox}
          aria-label="Set increment amount"
          value={incrementAmount}
          onChange={e => setIncrementAmount(e.target.value)}
        />
        <button
          className={styles.button}
          onClick={() =>
            dispatch(incrementByAmount(Number(incrementAmount) || 0))
          }
        >
          Add Amount
        </button>
        <button
          className={styles.asyncButton}
          onClick={() => dispatch(incrementAsync(Number(incrementAmount) || 0))}
        >
          Add Async
        </button>
      </div>
    </div>
  );
}

常见问题

1. Typescript 必须的吗?

不是,都0202年了,可以尝试ts了

2. React hook 必须的吗?

如果你用RTK的话,最好所有的库都用hook,最好带上ts

3. 不用 redux-thunk,我想用redux-saga?

可以通过getDefaultMiddleware({ thunk: false })获取middleware,configureStore 引入 mmiddlewares

const middlewares = [...getDefaultMiddleware({ thunk: false }), sagaMiddleware];
 const store = configureStore({
   reducer: rootReducer,
   middleware: middlewares,
});

4. 只创建xxxSlice.ts 文件就可以吗?这个文件会不会很大?

可以,文件大不取决于很多处理都放在一起,多封装util,还有一个原因是设计问题,state 过大

5. 使用 react-router 等路由的库,怎么使用?

推荐使用,@reach/routerredux-first-history 来管理路由,也支持hook

Demo地址

github.com/acivan/reac…

参考

redux-toolkit.js.org/introductio…


如果不对或者存在异议的地方,可以评论留言,及时改正