一、官方文档 Redux 工具包 |Redux Toolkit
文章定位:官方推荐的一套redux+Toolkit观察者这套,也挺好用的;
一、概述
1、Redux Toolkit出现的原因
1、目的:帮助解决 Redux 的三个常见问题:
- 存在问题
- 配置 Redux 存储复杂
- 必须添加很多软件包才能让 Redux 做任何有用的事情
- Redux 需要太多的样板代码
- 解决方案:
- 模板化-提供一些工具;
- 在设置过程中抽象出来并处理最常见的用例,并包含一些有用的实用程序,简化代码。
2、新工具【本文中都可以找到样例】
2.1 Reducer中
1、 createSlice:简单明了的 Redux 使用说明
- 简化reducer的声明
2、 extraReducers:Redux-ReduxToolkit
- 使用 extraReducers 属性来定义异步操作的状态更新逻辑
- 处理在其他地方定义的动作,包括createAsyncThunk或其他片中生成的动作。
- 监听三种action说明:给status赋值 Redux 基础教程,第五节:异步逻辑与数据请求 | Redux 中文官网
- fetchPosts.pending 请求开始;
- fetchPosts.fulfilled 请求成功;
- fetchPosts.rejected 请求失败;
- 使用 Redux ToolkitcreateAsyncThunk API 来简化异步调用
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
- 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
- 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
- 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、 盘一下关系
- Vite:
- 为什么选 Vite | Vite 官方中文文档 (vitejs.dev)
- 使用了原生ES模块系统,支持热更新,优化构建性能
-
Create-React-App:是个常用的初始项目脚手架框架;
-
Next.js:又是一个 React web 应用框架;
-
TS和JS的关系:一篇让你完全够用的TS指南
- TS是JS的超集,简单的说就是在
JavaScript的基础上加入了类型系统,有点像Kotlin; - 浏览器是不识别
TS的,所以在编译的时候,TS文件会先编译为JS文件; - js语法:JavaScript 用法 | 菜鸟教程 (runoob.com)
3、 除此之外还用到了
- redux-thunk 中间件【第三篇文章】
- 这个函数可以直接传递
dispatch
- React-Redux 简化流程
- Provider提供器和connect连接器【第三篇文章】
- import { useSelector, useDispatch } from 'react-redux'; 使用useSelector useDispatch 替代connect
-
redux-thunk 中间件,可以让你编写可能直接包含异步逻辑的普通函数
-
React Hooks【第四篇文章】
- useState 计数器状态赋值及对应方法
2、 demo功能与UI图
- 数字的增减
- input框输入数字,点击添加可以增加,点击异步可以异步增加
- 条件判断
4、虽然没用到但可以提一下:
- axios:发起异步网络请求
二、查看样例的行为模式与项目结构
1、 先看差异最小的Js项目demo,和第三篇文章结构没啥太大区别
1、项目结构
- 创建 Redux store 实例;
- 新建一个features存放各个组件;
- 新建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:对应生成的单元测试文件。
- 其他的和第三篇的没啥区别,App.js,css里多了点样式
- 没用Antd
1、先定位index.js
- 使用Provider标签获取store
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逻辑,完全不涉及任何传参逻辑;
3、看到重要的Counter组件,就是上文提到的counter组件系列
1、 Counter.js
- className:统一指定样式
- aria-label:无障碍相关的属性
- import { useSelector, useDispatch } from 'react-redux';用到了新特性取代connect
- 共同点:都会和reducer也就是counterSlice.js文件进行联系
- 定义count:通过useDispatch从reducer中拿到state中的值;
- 定义dispatch:通过useDispatch派发reducer对应的Action改变state的value【理论上没有改变数据源也是返回新值-监听模式-但他的写法有意思,后面会讲到】
- 添加方法incrementAsync()里需要传值,reducer中可以通过action.payload取到
/* 定义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
- createSlice:声明reducer,其中定义对应Action的普通方法
- 这边定义的Action都是直接对state进行操作,不含其他逻辑;
- 可以通过action.payload,取到dispatch过来action对应的value;
- 把非直接操作state的方法抽到外面去,reducers内只保留最纯粹基类
- extraReducers 定义异步操作的状态更新逻辑:
- 这边发现虽然给status赋予了状态,但是没用到,async按钮是Counter.module.css里的样式,和这个逻辑没关系;这个就不展开细说了;
- 官方说UI组件可以根据状态做ui处理或者防再次点击
- 提到异步方法,我们可以看到: 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)
);
}