【React阶段二 分支一 Redux】2、Redux Toolkit概述 & 官方模板

889 阅读7分钟

一、官方文档 Redux 工具包 |Redux Toolkit

文章定位:官方推荐的一套redux+Toolkit观察者这套,也挺好用的;

一、概述

1、Redux Toolkit出现的原因

1、目的:帮助解决 Redux 的三个常见问题:

  1. 存在问题
  • 配置 Redux 存储复杂
  • 必须添加很多软件包才能让 Redux 做任何有用的事情
  • Redux 需要太多的样板代码
  1. 解决方案:
  • 模板化-提供一些工具;
  • 在设置过程中抽象出来并处理最常见的用例,并包含一些有用的实用程序,简化代码。

2、新工具【本文中都可以找到样例】

2.1 Reducer中

1、 createSlice简单明了的 Redux 使用说明

  • 简化reducer的声明

2、 extraReducersRedux-ReduxToolkit

  1. 使用 extraReducers 属性来定义异步操作的状态更新逻辑
  2. 处理在其他地方定义的动作,包括createAsyncThunk或其他片中生成的动作。
  3. 监听三种action说明:给status赋值 Redux 基础教程,第五节:异步逻辑与数据请求 | Redux 中文官网
  • fetchPosts.pending 请求开始;
  • fetchPosts.fulfilled 请求成功;
  • fetchPosts.rejected 请求失败;

image.png

image.png

  1. 使用 Redux ToolkitcreateAsyncThunk API 来简化异步调用

image.png

image.png

image.png

2、官方样例Demo

1、样例代码库:reduxjs/redux-templates:用于 Vite、Create-React-App 等的官方 Redux 模板 (github.com)

1、含有三个项目,官方推荐模式Redux+TS template for Vite, or by creating a new Next.js project using Next's with-redux template

  1. vite-template-redux: Vite, with TypeScript;【main.tsx】
  • error workbox-webpack-plugin@6.6.1: The engine "node" is incompatible with this module. Expected version ">=16.0.0". Got "14.17.0" error Found incompatible module.
npx degit reduxjs/redux-templates/packages/vite-template-redux my-app
  1. cra-template-redux-typescript: Create-React-App, with TypeScript;【index.tsx】
  • 要求 node版本>16
npx create-react-app my-app --template redux-typescript
# or
yarn create react-app my-app --template redux-typescript
  1. cra-template-redux: Create-React-App, with JavaScript;【index.js】很多公司目前用的还是这个
npx create-react-app my-app --template redux
# or
yarn create react-app my-app --template redux

2、 盘一下关系

  1. Vite:
  1. Create-React-App:是个常用的初始项目脚手架框架;

  2. Next.js:又是一个 React web 应用框架;

  3. TS和JS的关系:一篇让你完全够用的TS指南

  • TS是JS的超集,简单的说就是在 JavaScript 的基础上加入了类型系统,有点像Kotlin;
  • 浏览器是不识别TS的,所以在编译的时候,TS文件会先编译为JS文件;
  • js语法:JavaScript 用法 | 菜鸟教程 (runoob.com)

3、 除此之外还用到了

  1. redux-thunk 中间件【第三篇文章】
  • 这个函数可以直接传递dispatch
  1. React-Redux 简化流程
  1. redux-thunk 中间件,可以让你编写可能直接包含异步逻辑的普通函数

  2. React Hooks【第四篇文章】

  • useState 计数器状态赋值及对应方法

2、 demo功能与UI图

  • 数字的增减
  • input框输入数字,点击添加可以增加,点击异步可以异步增加
  • 条件判断

4、虽然没用到但可以提一下:

  1. axios:发起异步网络请求

image.png

二、查看样例的行为模式与项目结构

1、 先看差异最小的Js项目demo,和第三篇文章结构没啥太大区别

image.png 1、项目结构

  1. 创建 Redux store 实例;
  2. 新建一个features存放各个组件;
  3. 新建counter文件夹存放counter组件
  • Counter.js :定义Counter组件UI部分;展示 counter 特性的 React 组件;
  • Counter.module.css:定义Counter使用的Style;
  • counterAPI.js:定义Slice中调用的公用API,项目里是异步请求;
  • counterSlice.js:定义reducer数据源及其方法;counter 特性相关的 redux 逻辑;
  • counterSlice.spec.js:对应生成的单元测试文件。
  1. 其他的和第三篇的没啥区别,App.js,css里多了点样式
  2. 没用Antd

image.png

image.png

image.png

image.png

1、先定位index.js

  1. 使用Provider标签获取store

image.png

2、 找到对应的驱动核心 store.js【有个问题,有没有可能存在多个reducer/功能】

/* 1. 引入configureStore 用于初始化绑定Reducer */

import { configureStore } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';

/* 2. 配置reducer注入项目中的counterReducer对象 */
export const store = configureStore({
  reducer: {
    counter: counterReducer,
  },
});

2、App.js

1、顺着index往下走,检查App组件

  • css啥的可以先不管,发现这边用了Counter组件;
  • App.js只有自己的UI逻辑,完全不涉及任何传参逻辑;

image.png

3、看到重要的Counter组件,就是上文提到的counter组件系列

1、 Counter.js

  1. className:统一指定样式
  2. aria-label:无障碍相关的属性
  3. import { useSelector, useDispatch } from 'react-redux';用到了新特性取代connect
  • 共同点:都会和reducer也就是counterSlice.js文件进行联系
  • 定义count:通过useDispatch从reducer中拿到state中的值;
  • 定义dispatch:通过useDispatch派发reducer对应的Action改变state的value【理论上没有改变数据源也是返回新值-监听模式-但他的写法有意思,后面会讲到】
  • 添加方法incrementAsync()里需要传值,reducer中可以通过action.payload取到

image.png

/* 定义Counter组件结构 及点击事件*/
import React, { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import {
  decrement,
  increment,
  incrementByAmount,
  incrementAsync,
  incrementIfOdd,
  selectCount,
} from './counterSlice';
import styles from './Counter.module.css';

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

  const incrementValue = Number(incrementAmount) || 0;

  return (
    <div>
      {/* - 0 + 这一行内容*/}
      <div className={styles.row}>
        <button
          className={styles.button}
          aria-label="Decrement value"
          /*  */
          onClick={() => dispatch(decrement())}
        >
          -
        </button>
        <span className={styles.value}>{count}</span>
        <button
          className={styles.button}
          aria-label="Increment value"
          onClick={() => dispatch(increment())}
        >
          +
        </button>
      </div>

      {/* input框与三个按钮*/}
      <div className={styles.row}>
        {/* input框 初始值与对应赋值方法*/}
        <input
          className={styles.textbox}
          aria-label="Set increment amount"
          value={incrementAmount}
          onChange={(e) => setIncrementAmount(e.target.value)}
        />
        <button
          className={styles.button}
          onClick={() => dispatch(incrementByAmount(incrementValue))}
        >
          Add Amount
        </button>
        <button
          className={styles.asyncButton}
          onClick={() => dispatch(incrementAsync(incrementValue))}
        >
          Add Async
        </button>
        <button
          className={styles.button}
          onClick={() => dispatch(incrementIfOdd(incrementValue))}
        >
          Add If Odd
        </button>
      </div>
    </div>
  );
}

2、 接下来盘 counterSlice.js

  1. createSlice:声明reducer,其中定义对应Action的普通方法
  • 这边定义的Action都是直接对state进行操作,不含其他逻辑
  • 可以通过action.payload,取到dispatch过来action对应的value;
  1. 把非直接操作state的方法抽到外面去,reducers内只保留最纯粹基类
  2. extraReducers 定义异步操作的状态更新逻辑:
  • 这边发现虽然给status赋予了状态,但是没用到,async按钮是Counter.module.css里的样式,和这个逻辑没关系;这个就不展开细说了;
  • 官方说UI组件可以根据状态做ui处理或者防再次点击

image.png

  1. 提到异步方法,我们可以看到: const response = await fetchCount(amount); 这个是counterAPI.js里的内容,待会会盘;感觉网络请求的逻辑应该和这个差不多
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import { fetchCount } from './counterAPI';

/* 定义初始数据源,俩参数  value和status */
const initialState = {
  value: 0,
  status: 'idle',
};

// The function below is called a thunk and allows us to perform async logic. It
// can be dispatched like a regular action: `dispatch(incrementAsync(10))`. This
// will call the thunk with the `dispatch` function as the first argument. Async
// code can then be executed and other actions can be dispatched. Thunks are
// typically used to make async requests.


/* createSlice方法创建一个 */
export const counterSlice = createSlice({
  name: 'counter',
  initialState,
  // The `reducers` field lets us define reducers and generate associated actions
  reducers: {
    increment: (state) => {
      // Redux Toolkit allows us to write "mutating" logic in reducers. It
      // doesn't actually mutate the state because it uses the Immer library,
      // which detects changes to a "draft state" and produces a brand new
      // immutable state based off those changes
      state.value += 1;
    },
    decrement: (state) => {
      state.value -= 1;
    },
    // Use the PayloadAction type to declare the contents of `action.payload`
    incrementByAmount: (state, action) => {
      state.value += action.payload;
    },
  },
  // The `extraReducers` field lets the slice handle actions defined elsewhere,
  // including actions generated by createAsyncThunk or in other slices.
  extraReducers: (builder) => {
    builder
      .addCase(incrementAsync.pending, (state) => {
        //state.status = 'loading';
      })
      .addCase(incrementAsync.fulfilled, (state, action) => {
        //state.status = 'idle';
        state.value += action.payload;
      });
  },
});

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

// The function below is called a selector and allows us to select a value from
// the state. Selectors can also be defined inline where they're used instead of
// in the slice file. For example: `useSelector((state: RootState) => state.counter.value)`
export const selectCount = (state) => state.counter.value;

// We can also write thunks by hand, which may contain both sync and async logic.
// Here's an example of conditionally dispatching actions based on current state.
/*   reducers的直接列表只对纯state进行操作,有其他判断的都抽出去的action */
export const incrementIfOdd = (amount) => (dispatch, getState) => {
  const currentValue = selectCount(getState());
  if (currentValue % 2 === 1) {
    dispatch(incrementByAmount(amount));
  }
};

/* 供组件调用的方法区  异步方法调用了countryAPI里的方法*/
export const incrementAsync = createAsyncThunk(
  'counter/fetchCount',
  async (amount) => {
    const response = await fetchCount(amount);
    // The value we return becomes the `fulfilled` action payload
    return response.data;
  }
);

export default counterSlice.reducer;

2、 盘最后一个 counterAPI.js,返回包裹的Promise类型数据,联动

  • 看起来像是设置了超时与返回的默认值
// A mock function to mimic making an async request for data
export function fetchCount(amount = 1) {
  return new Promise((resolve) =>
    setTimeout(() => resolve({ data: amount }), 500)
  );
}

参考文档

Vite+React+TS基础学习,看这里就够了!(上) - 掘金 (juejin.cn)