【实战】 九、深入React 状态管理与Redux机制(五) —— React17+React Hook+TS4 最佳实践,仿 Jira 企业级项目(二十)

news/2025/2/19 13:25:05/

文章目录

    • 一、项目起航:项目初始化与配置
    • 二、React 与 Hook 应用:实现项目列表
    • 三、TS 应用:JS神助攻 - 强类型
    • 四、JWT、用户认证与异步请求
    • 五、CSS 其实很简单 - 用 CSS-in-JS 添加样式
    • 六、用户体验优化 - 加载中和错误状态处理
    • 七、Hook,路由,与 URL 状态管理
    • 八、用户选择器与项目编辑功能
    • 九、深入React 状态管理与Redux机制
      • 1&2
      • 3&4
      • 5~8
      • 9&10
      • 11.用redux-thunk管理登录状态


学习内容来源:React + React Hook + TS 最佳实践-慕课网


相对原教程,我在学习开始时(2023.03)采用的是当前最新版本:

版本
react & react-dom^18.2.0
react-router & react-router-dom^6.11.2
antd^4.24.8
@commitlint/cli & @commitlint/config-conventional^17.4.4
eslint-config-prettier^8.6.0
husky^8.0.3
lint-staged^13.1.2
prettier2.8.4
json-server0.17.2
craco-less^2.0.0
@craco/craco^7.1.0
qs^6.11.0
dayjs^1.11.7
react-helmet^6.1.0
@types/react-helmet^6.1.6
react-query^6.1.0
@welldone-software/why-did-you-render^7.0.1
@emotion/react & @emotion/styled^11.10.6

具体配置、操作和内容会有差异,“坑”也会有所不同。。。


一、项目起航:项目初始化与配置

  • 一、项目起航:项目初始化与配置

二、React 与 Hook 应用:实现项目列表

  • 二、React 与 Hook 应用:实现项目列表

三、TS 应用:JS神助攻 - 强类型

  • 三、 TS 应用:JS神助攻 - 强类型

四、JWT、用户认证与异步请求

  • 四、 JWT、用户认证与异步请求(上)

  • 四、 JWT、用户认证与异步请求(下)

五、CSS 其实很简单 - 用 CSS-in-JS 添加样式

  • 五、CSS 其实很简单 - 用 CSS-in-JS 添加样式(上)

  • 五、CSS 其实很简单 - 用 CSS-in-JS 添加样式(下)

六、用户体验优化 - 加载中和错误状态处理

  • 六、用户体验优化 - 加载中和错误状态处理(上)

  • 六、用户体验优化 - 加载中和错误状态处理(中)

  • 六、用户体验优化 - 加载中和错误状态处理(下)

七、Hook,路由,与 URL 状态管理

  • 七、Hook,路由,与 URL 状态管理(上)

  • 七、Hook,路由,与 URL 状态管理(中)

  • 七、Hook,路由,与 URL 状态管理(下)

八、用户选择器与项目编辑功能

  • 八、用户选择器与项目编辑功能(上)

  • 八、用户选择器与项目编辑功能(下)

九、深入React 状态管理与Redux机制

1&2

  • 九、深入React 状态管理与Redux机制(一)

3&4

  • 九、深入React 状态管理与Redux机制(二)

5~8

  • 九、深入React 状态管理与Redux机制(三)

9&10

  • 九、深入React 状态管理与Redux机制(四)

11.用redux-thunk管理登录状态

既然模态框使用 redux 来管理了,而 reduxcontext 是竞争关系,那可以尝试将之前管理登录状态的 context 改为 redux

新建 src\store\auth.slice.ts

import { User } from "screens/ProjectList/components/SearchPanel";
import { createSlice } from "@reduxjs/toolkit";
import * as auth from 'auth-provider'
import { AuthForm, initUser as _initUser } from "context/auth-context";
import { AppDispatch, RootState } from "store";interface State {user: User | null
}const initialState: State = {user: null
}export const authSlice = createSlice({name: 'auth',initialState,reducers: {setUser(state, action) {state.user = action.payload}}
})const { setUser } = authSlice.actionsexport const selectUser = (state: RootState) => state.auth.userexport const login = (form: AuthForm) => (dispatch: AppDispatch) => auth.login(form).then(user => dispatch(setUser(user)))
export const register = (form: AuthForm) => (dispatch: AppDispatch) => auth.register(form).then(user => dispatch(setUser(user)))
export const logout = () => (dispatch: AppDispatch) => auth.logout().then(() => dispatch(setUser(null)))
export const initUser = () => (dispatch: AppDispatch) => _initUser().then(user => dispatch(setUser(user)))

需要提前将 src\context\auth-context.tsx 中的 interface AuthForminitUser(视频中对应bootstrapUser) 导出

最后在 src\store\index.ts 中统一注册:

...
import { authSlice } from "./auth.slice";// 集中状态注册
export const rootReducer = {projectList: projectListSlice.reducer,auth: authSlice.reducer
};
...

接下来使用 redux 中的 auth 相关功能替换原有 context 提供的 auth 的功能

重构需要找代码上游中集中的一个点:useAuth

修改 src\context\auth-context.tsx

+ import { ReactNode, useCallback } from "react";
...
+ import * as authStore from 'store/auth.slice'
+ import { useDispatch, useSelector } from "react-redux";
+ import { AppDispatch } from "store";- interface AuthForm {
+ export interface AuthForm {username: string;password: string;
}- const initUser = async () => {
+ export const initUser = async () => {let user = null;const token = auth.getToken();if (token) {// 由于要自定义 token ,这里使用 http 而非 useHttpconst data = await http("me", { token });user = data.user;}return user
};- const AuthContext = React.createContext<
-   | {
-       user: User | null;
-       login: (form: AuthForm) => Promise<void>;
-       register: (form: AuthForm) => Promise<void>;
-       logout: () => Promise<void>;
-     }
-   | undefined
- >(undefined);- AuthContext.displayName = "AuthContext";export const AuthProvider = ({ children }: { children: ReactNode }) => {// 这里要考虑到初始值的类型与后续值类型,取并组成一个泛型const {
-     data: user,error,isLoading,isReady,isError,run,
-     setData: setUser,} = useAsync<User | null>();
+   // const dispatch: (...args: unknown[]) => Promise<User> = useDispatch()
+   // const dispatch: AppDispatch = useDispatch()
+ 
+   // 这种写法虽然消除了代码中的报错,但是控制台会有报错
+   // useMount(async () => run(dispatch(await initUser())));
+   // useMount(() => run(dispatch(initUser())));
+   // 还原后登录保持功能已经是不好使的了useMount(() => run(initUser()));-   const login = (form: AuthForm) => auth.login(form).then(setUser);
-   const register = (form: AuthForm) => auth.register(form).then(setUser);
-   const logout = () => auth.logout().then(() => setUser(null));if (isReady || isLoading) {return <FullPageLoading />;}if (isError) {return <FullPageErrorFallback error={error} />;}-   return (
-     <AuthContext.Provider
-       children={children}
-       value={{ user, login, register, logout }}
-     />
-   );
+   return <div>
+     { children }
+   </div>
};export const useAuth = () => {
-   const context = React.useContext(AuthContext);
-   if (!context) {
-     throw new Error("useAuth 必须在 AuthProvider 中使用");
-   }
-   return context;
+   // 这种写法有报错
+   // const dispatch: (...args: unknown[]) => Promise<User> = useDispatch()
+   const dispatch: AppDispatch = useDispatch()
+   const user = useSelector(authStore.selectUser)
+   const login = useCallback((form: AuthForm) => dispatch(authStore.login(form)), [dispatch])
+   const register = useCallback((form: AuthForm) => dispatch(authStore.register(form)), [dispatch])
+   const logout = useCallback(() => dispatch(authStore.logout()), [dispatch])
+   // const login = useCallback((form: AuthForm) => authStore.login(form), [])
+   // const register = useCallback((form: AuthForm) => authStore.register(form), [])
+   // const logout = useCallback(() => authStore.logout(), [])
+   // login({ username: '123', password: '123' }).then()
+   return { user, login, register, logout };
};

这种写法会报错 const dispatch: (...args: unknown[]) => Promise<User> = useDispatch()

不能将类型“Dispatch<AnyAction>”分配给类型“(...args: unknown[]) => Promise<User>”。参数“action”和“args” 的类型不兼容。不能将类型“unknown”分配给类型“AnyAction”

替换为 const dispatch: AppDispatch = useDispatch() 后正常

useMount(async () => run(dispatch(await initUser()))) 中不加 async..await 的话会有如下报错提示:

没有与此调用匹配的重载。第 1 个重载(共 3 个),“(thunkAction: ThunkAction<Promise<User | null>, { projectList: State; auth: State; }, undefined, AnyAction>): Promise<User | null>”,出现以下错误。类型“Promise<any>”的参数不能赋给类型“ThunkAction<Promise<User | null>, { projectList: State; auth: State; }, undefined, AnyAction>”的参数。类型“Promise<any>”提供的内容与签名“(dispatch: ThunkDispatch<{ projectList: State; auth: State; }, undefined, AnyAction>, getState: () => { projectList: State; auth: State; }, extraArgument: undefined): Promise<...>”不匹配。第 2 个重载(共 3 个),“(action: AnyAction): AnyAction”,出现以下错误。类型“Promise<any>”的参数不能赋给类型“AnyAction”的参数。类型 "Promise<any>" 中缺少属性 "type",但类型 "AnyAction" 中需要该属性。第 3 个重载(共 3 个),“(action: AnyAction | ThunkAction<Promise<User | null>, { projectList: State; auth: State; }, undefined, AnyAction>): AnyAction | Promise<...>”,出现以下错误。类型“Promise<any>”的参数不能赋给类型“AnyAction | ThunkAction<Promise<User | null>, { projectList: State; auth: State; }, undefined, AnyAction>”的参数。不能将类型“Promise<any>”分配给类型“AnyAction”。ts(2769)
auth-context.tsx(41, 31): 是否忘记使用 "await"?
index.d.ts(19, 3): 在此处声明了 "type"。
auth-context.tsx(41, 31): 是否忘记使用 "await"?
auth-context.tsx(41, 31): 是否忘记使用 "await"?

最终运行结果:登录,注册,登出功能都正常,只有登录保持不正常


部分引用笔记还在草稿阶段,敬请期待。。。


http://www.ppmy.cn/news/1016567.html

相关文章

Electron+vue3项目使用SQLite3数据库

SQLite 是一个进程内的库&#xff0c;实现了自给自足的、无服务器的、零配置的、事务性的 SQL 数据库引擎。它是一个零配置的数据库&#xff0c;这意味着与其他数据库不一样&#xff0c;我们不需要在系统中配置。 就像其他数据库&#xff0c;SQLite 引擎不是一个独立的进程&am…

Go Fyne:一种现代的,跨平台的 GUI 开发工具集

Fyne 是一个用 Go 语言写的&#xff0c;提供丰富&#xff0c;现代化的用户界面&#xff08;UI&#xff09;的开发工具集。Fyne 提供了一种简单而直观的方式来构建应用程序&#xff0c;并且注意到它是跨平台的&#xff0c;可以在 Windows&#xff0c;macOS&#xff0c;Linux&…

leetcode2808. 使循环数组所有元素相等的最少秒数 处理循环数组

https://leetcode.cn/problems/minimum-seconds-to-equalize-a-circular-array 给你一个下标从 0 开始长度为 n 的数组 nums 。 每一秒&#xff0c;你可以对数组执行以下操作&#xff1a; 对于范围在 [0, n - 1] 内的每一个下标 i &#xff0c;将 nums[i] 替换成 nums[i] &a…

【Rust】Rust学习 第六章枚举和模式匹配

本章介绍 枚举&#xff08;enumerations&#xff09;&#xff0c;也被称作 enums。枚举允许你通过列举可能的 成员&#xff08;variants&#xff09; 来定义一个类型。首先&#xff0c;我们会定义并使用一个枚举来展示它是如何连同数据一起编码信息的。接下来&#xff0c;我们会…

Linux学习之awk

awk多数情况下作为sed的补充使用&#xff0c;awk会对sed处理过的内容进行格式的调整并输出。awk处理“比较规范”的文件&#xff0c;使用方法比较像脚本文件&#xff0c;sed把比较不规范的文件处理成比较规范的文件。 awk有三部分组成&#xff1a; 输入数据前例程 BEGIN{} 主输…

仅使用 CSS 创建打字机动画效果

创建打字机效果比您想象的要容易。虽然实现这种效果的最常见方法是使用 JavaScript&#xff0c;但我们也可以使用纯 CSS 来创建我们的打字机动画。 在本文中&#xff0c;我们将了解如何仅使用 CSS 创建打字机动画效果。它简单、漂亮、容易。我们还将看看使用 CSS 与 JavaScrip…

第0章 环境搭建汇总

mini商城第0章 环境搭建汇总 本文是整个mini商城的前置文档,所有用到的技术安装都在本篇文档中有详细描述。所有软件安装不分先后顺序,只是作为一个参考文档,需要用到什么技术软件,就按照文档安装什么软件,切不可一上来全部安装一遍。 文章中有些截图中服务器地址是192.16…

2012年数学建模竞赛脑卒中发病环境因素分析及干预日期数据处理代码

因四个表格日期数据处理有些复杂&#xff0c;故作此代码一次性处理四组数据 详细说明在main函数中&#xff0c;复制粘贴即可使用&#xff1a; import datetime import pandas as pddef check(string, df, i, num, error_list):if is_valid(pd.to_datetime(string, errorscoe…