【实战】 七、Hook,路由,与 URL 状态管理(中) —— React17+React Hook+TS4 最佳实践,仿 Jira 企业级项目(十二)

news2024/11/24 8:51:04

文章目录

    • 一、项目起航:项目初始化与配置
    • 二、React 与 Hook 应用:实现项目列表
    • 三、TS 应用:JS神助攻 - 强类型
    • 四、JWT、用户认证与异步请求
    • 五、CSS 其实很简单 - 用 CSS-in-JS 添加样式
    • 六、用户体验优化 - 加载中和错误状态处理
    • 七、Hook,路由,与 URL 状态管理
      • 1+2.
      • 3.添加项目列表和项目详情路由
      • 4. 添加看板和任务组路由
      • 5. 初步实现 useUrlQueryParam 管理 URL 参数状态
      • 6.用useMemo解决依赖循环问题 - Hook的依赖问题详解


学习内容来源: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 状态管理

1+2.

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

3.添加项目列表和项目详情路由

Home v6.11.2 | React Router

安装 react-router react-router-dom

# 可能会需要 --force
npm i react-router react-router-dom history

目前最新版

  • “react-router”: “^6.11.2”
  • “react-router-dom”: “^6.11.2”
  • “history”: “^5.3.0”

新建项目详情页 src\screens\ProjectDetail\index.tsx

export const ProjectDetail = () => {
  return <h1>ProjectDetail</h1>
};

修改 src\authenticated-app.tsx(将 PageHeader 提取出来,引入并使用 路由相关组件):

...
import { Routes, Route } from "react-router";
import { BrowserRouter as Router } from "react-router-dom";
import { ProjectDetail } from "screens/ProjectDetail";

export const AuthenticatedApp = () => {

  useDocumentTitle("项目列表", false);

  return (
    <Container>
      <PageHeader/>
      <Main>
        <Router>
          <Routes>
            <Route path={'/projects'} element={<ProjectList/>}/>
            <Route path={'/projects/:projectId/*'} element={<ProjectDetail/>}/>
          </Routes>
        </Router>
      </Main>
    </Container>
  );
};
const PageHeader = () => {
  const { logout, user } = useAuth();
  const items: MenuProps["items"] = [
    {
      key: 1,
      label: "登出",
      onClick: logout,
    },
  ];

  return <Header between={true}>
    <HeaderLeft gap={true}>
      <SoftwareLogo width="18rem" color="rgb(38,132,255)" />
      <h2>项目</h2>
      <h2>用户</h2>
    </HeaderLeft>
    <HeaderRight>
      <Dropdown menu={{ items }}>
        <Button type="link" onClick={(e) => e.preventDefault()}>
          Hi, {user?.name}
        </Button>
      </Dropdown>
    </HeaderRight>
  </Header>
}
...

修改项目列表页 src\screens\ProjectList\components\List.tsx(引入 路由组件 Link):

...
// react-router 和 react-router-dom 的关系,类似于 react 和 react-dom/react-native/react-vr...
import { Link } from 'react-router-dom'

// TODO 把所有 id personId 都改为 number 类型
...

// type PropsType = Omit<ListProps, 'users'>
export const List = ({ users, ...props }: ListProps) => {
  return (
    <Table
      pagination={false}
      columns={[
        {
          title: "名称",
          dataIndex: "name",
          sorter: (a, b) => a.name.localeCompare(b.name),
          render: (text, record) => <Link to={String(record.id)}>{text}</Link>
        },
        ...
      ]}
      {...props}
    ></Table>
  );
};

查看效果:

  • 访问项目列表页:http://localhost:3000/projects
  • 点击项目名称访问项目详情页

4. 添加看板和任务组路由

新建看板页 src\screens\ViewBoard\index.tsx

export const ViewBoard = () => {
  return <h1>ViewBoard</h1>
};

新建任务组页 src\screens\TaskGroup\index.tsx

export const TaskGroup = () => {
  return <h1>TaskGroup</h1>
};

修改项目详情页 src\screens\ProjectDetail\index.tsx(默认重定向到看板,也可自由切换看板和任务组):

import { Link, Navigate } from "react-router-dom";
import { Route, Routes } from "react-router";
import { TaskGroup } from "screens/TaskGroup";
import { ViewBoard } from "screens/ViewBoard";

export const ProjectDetail = () => {
  return <div>
    <h1>ProjectDetail</h1>
    <Link to='viewboard'>看板</Link>
    <Link to='taskgroup'>任务组</Link>
    <Routes>
      <Route path='/viewboard' element={<ViewBoard/>}/>
      <Route path='/taskgroup' element={<TaskGroup/>}/>
      <Route index element={<Navigate to='viewboard'/>}/>
    </Routes>
  </div>
};

  • / 代表根路由,若要在当前路由后指定追加路径,只需要写路径即可不需要加 /
  • 使用 Navigate 需要重定向的 Routepath='/' 可以写成 index
  • Navigate 使用过程中报错:[Navigate] is not a <Route> component. All component children of <Routes> must be a <Route> or <React.Fragment>
    • 博主代码是已经可以正常运行的代码,其他思路可参考:【必看,重要提示】React Router 版本已更新-慕课网

同理登录后需要默认跳转到 /projects,且点击 PageHeaderlogo 自动跳转根路径

修改 src\utils\index.ts(新增 resetRoute):

...
export const resetRoute = () => window.location.href = window.location.origin

修改 src\authenticated-app.tsx

...
import { resetRoute, useDocumentTitle } from "utils";
...
import { Navigate, BrowserRouter as Router } from "react-router-dom";
...

export const AuthenticatedApp = () => {
  ...
  return (
    <Container>
      <PageHeader />
      <Main>
        <Router>
          <Routes>
            ...
            <Route index element={<Navigate to='projects'/>}/>
          </Routes>
        </Router>
      </Main>
    </Container>
  );
};
const PageHeader = () => {
  ...

  return (
    <Header between={true}>
      <HeaderLeft gap={true}>
        <Button type='link' onClick={resetRoute}>
          <SoftwareLogo width="18rem" color="rgb(38,132,255)" />
        </Button>
        ...
      </HeaderLeft>
      ...
    </Header>
  );
};
...

5. 初步实现 useUrlQueryParam 管理 URL 参数状态

新建 src\utils\url.ts

import { useSearchParams } from "react-router-dom";

/**
 * 返回页面 url 中,指定键的参数值
 * @param keys 
 *  - keys 的类型“{ [x: string]: string; }”缺少类型“{ name: string; personId: string; }”中的以下属性: name, personId
 *  - 由于数据的下游要求指定的 key name 且是 string 类型,因此 keys 需要设定为泛型以做兼容
 *  - 计算属性名的类型必须为 "string"、"number"、"symbol" 或 "any"。泛型 K 需要 `extends string` 约束
 * @returns 
 */
export const useUrlQueryParam = <K extends string>(keys: K[]) => {
  const [ searchParams, setSearchParams ] = useSearchParams()
  return [
    keys.reduce((prev, key) => {
      // searchParams.get 可能会返回 null,需要预设值来兼容
      return {...prev, [key]: searchParams.get(key) || ''}
    // 初始值会对类型造成影响,需要手动指定
    }, {} as { [key in K]: string }),
    setSearchParams
  ] as const
}
  • URLSearchParams - Web API 接口参考 | MDN

这部分类型系统会有些问题,其他可见注释

  • as const 是类型断言的一种,避免使用其默认类型推断行为,导致更广泛或更一般的类型。
    • 比如:
      • let a = ['string', 123, true], a 会被推断为 (string | number | boolean)[]
      • let a = ['string', 123, true, {}], a 却会被推断为 {}[]
    • 加上 as constlet a = ["string", 123, true, {}] as const, a 会被推断为 readonly ["string", 123, true, {}]

修改 src\screens\ProjectList\index.tsx (使用 useUrlQueryParamurl 中拿参数):

...
import { useUrlQueryParam } from "utils/url";

export const ProjectList = () => {
  const [, setParam] = useState({
    name: "",
    personId: "",
  });

  const [param] = useUrlQueryParam(['name', 'personId'])
  ...
};

const Container = styled.div`
  padding: 3.2rem;
`;

至此,想要的功能实现了,但是页面在持续循环渲染。。。下面来找下原因

6.用useMemo解决依赖循环问题 - Hook的依赖问题详解

安装 why-did-you-render

# 可能会需要 --force
npm i @welldone-software/why-did-you-render

“@welldone-software/why-did-you-render”: “^7.0.1”

新建 src\wdyr.ts

import React from 'react';

if (process.env.NODE_ENV === 'development') {
  const whyDidYouRender = require('@welldone-software/why-did-you-render');
  whyDidYouRender(React, {
    // 跟踪所有组件
    trackAllPureComponents: false,
  });
}

src\index.tsx 第一行全局引入:

import './wdyr'

在需要追踪问题的组件 src\screens\ProjectList\index.tsx 中加入:

ProjectList.whyDidYouRenger = true

或者不确定哪个组件的问题可以将 src\wdyr.tstrackAllPureComponents 设为 true

我尝试单独加不管用,所以就使用了默认配置,全局追踪

运行结果如图:

在这里插入图片描述

Re-rendered because of hook changes:

  e-rendered because of props changes:"

    different objects that are equal by value.

不难发现,两个不同的 objects 值相等,而且在 useEffect 中监听了,然后就死循环了。。。

那说明两个 objects 不是同一个(引用地址不同),也就是说每次给 useEffect(useDebounce) 的都是新创建的对象

然后追根溯源找到 useUrlQueryParam (src\utils\url.ts)。

啊哈,问题就在这里了,每次 searchParams 都是新的,如何解决呢?使用 useMemo

import { useMemo } from "react";
...
export const useUrlQueryParam = <K extends string>(keys: K[]) => {
  const [searchParams, setSearchParams] = useSearchParams();
  return [
    useMemo(
      () => keys.reduce((prev, key) => {
        // searchParams.get 可能会返回 null,需要预设值来兼容
        return { ...prev, [key]: searchParams.get(key) || "" };
        // 初始值会对类型造成影响,需要手动指定
      }, {} as { [key in K]: string }),
      // eslint-disable-next-line react-hooks/exhaustive-deps
      [searchParams]
    ),
    setSearchParams,
  ] as const;
};

Hook API 索引 – React

基本类型,组件状态可以放到依赖里;非组件状态的引用类型(对象,数组,方法)不可以
当组件 state 值(通过 useState 定义)放在 useEffectuseMemo 的依赖列表中时,由于只承认对应 setState 才会触发 state 值的改变,因此不会造成无限循环。

修改后查看页面,无限循环没有了

bug 没了,可以将 src\wdyr.tstrackAllPureComponents 设为 false,或是组件中:

ProjectList.whyDidYouRenger = false

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/761050.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Docker基础(二)

1、Docker工作原理 Docker是一个Clinet-Server结构的系统&#xff0c;Docker守护进程运行在主机上&#xff0c;然后通过Socket连接从客户端访问&#xff0c;守护进程从客户端接受命令并管理运行在主机上的容器。 容器&#xff0c;是一个运行时环境&#xff0c;就是我们前面说的…

论文笔记:Deep Spatio-Temporal Residual Networks for Citywide Crowd FlowsPrediction

2017 AAAI 使用时空残差网络ST-ResNet 进行 城市区域流入流出客流量预测 1 研究对象 城市客流流入流出 根据经纬度将城市划分为网格 IJ 1.1 难点 空间依赖性 时间依赖性 外部影响 2 模型 3 实验 北京出租车数据纽约自行车数据 评价指标&#xff1a;RMSE

Java List中通过对象属性排序,可实现多条件排序

直接上代码&#xff1a; import com.google.common.collect.Lists; import lombok.AllArgsConstructor; import lombok.Data;import java.util.Comparator; import java.util.List; import java.util.stream.Collectors;/*** List 对象属性排序*/Data AllArgsConstructor clas…

[java安全]动态代理

文章目录 【java安全】动态代理前言本质重要方法Proxy#newProxyInstance()InvocationHandler#invoke() 举例 【java安全】动态代理 前言 java中代理分为两种&#xff1a;静态代理、动态代理 而动态代理又分为&#xff1a;jdk动态代理、CGLIB动态代理 本文我们来谈谈jdk动态代…

Iceberg从入门到精通系列之十七:Apache InLong往Iceberg同步数据

Iceberg从入门到精通系列之十七&#xff1a;Apache InLong往Iceberg同步数据 一、概览二、版本支持三、依赖项四、SQL API 用法五、多表写入六、动态表名映射七、动态建库、建表八、动态schema变更九、Iceberg Load 节点参数十、数据类型映射 一、概览 Apache Iceberg是一种用…

【机器学习算法】主成分分析(PCA)

主成分分析(PCA) PCA(Principal Component Analysis) 是实现数据降维的一种算法。正如其名&#xff0c;假设有一份数据集&#xff0c;每条数据的维度是d&#xff0c;PCA通过分析这d个维度的前k个主要特征(这k个维度在原有d维特征的基础上重新构造出来&#xff0c;且是全新的正交…

SpringBoot+React学科竞赛管理系统 附带详细运行指导视频

文章目录 一、项目演示二、项目介绍三、运行截图四、主要代码 一、项目演示 项目演示地址&#xff1a; 视频地址 二、项目介绍 项目描述&#xff1a;这是一个基于SpringBootReact框架开发的学科竞赛管理系统。首先&#xff0c;这是一个前后端分离的项目&#xff0c;代码简洁…

初学者怎么学习c++(合集)

学习c方法1 找一本好的书本教材&#xff0c;辅助看教学视频。好的教材&#xff0c;可以让你更快更好的进入C/C的世界。在校学生的话&#xff0c;你们的教材通常都是不错的。如果是自学&#xff0c;推荐使用谭浩强出的C/C经典入门教材。看视频是学习比较直观的方式。建议先看课本…

从零开始理解Linux中断架构(20)--关于二级中断控制-链式chained Handler

二级中断控制器是个双重角色,在上级中断控制器看来他是个中断设备,在连接到他的下级设备来看,他是个中断控制器。所以处理完成基本的中断控制器管理功能:映射本地中断,还要多个动作:修改上级中断的默认irq Handler,向上级中断设置自己的链式中断处理函数。 中断控制的层…

Springboot实现过滤器

一、导言 在Spring Boot中&#xff0c;过滤器是一种用于对HTTP请求进行预处理和后处理的组件。相较于拦截器&#xff0c;过滤器属于Servlet规范的一部分&#xff0c;它能够在请求进入Web容器之前或返回给客户端之前进行操作。 要在Spring Boot中实现过滤器&#xff0c;可以按…

指针进阶(万字深层次指针解析)

❤️ 作者简介 &#xff1a;对纯音乐情有独钟的阿甘 致力于C、C、数据结构、TCP/IP、数据库等等一系列知识&#xff0c;对纯音乐有独特的喜爱 &#x1f4d7; 日后方向 : 偏向于CPP开发以及大数据方向&#xff0c;如果你也感兴趣的话欢迎关注博主&#xff0c;期待更新 指针进阶 …

Java正则表达式,不定期更新

Java正则表达式 1. 匹配数字&#xff08;包含负数、小数&#xff09;2. 匹配不是纯数字和纯字母且需要8位以上的密码3. 密码&#xff1a;字母、数字、符号&#xff08;_-*.,!#符号可自定义&#xff09;三选二4. 密码&#xff1a;必须包含大写、小写、数字、符号&#xff08;_-*…

车道线检测|利用边缘检测的原理对车道线图片进行识别

前言 那么这里博主先安利一些干货满满的专栏了&#xff01; 这两个都是博主在学习Linux操作系统过程中的记录&#xff0c;希望对大家的学习有帮助&#xff01; 操作系统Operating Syshttps://blog.csdn.net/yu_cblog/category_12165502.html?spm1001.2014.3001.5482Linux S…

工程监测振弦采集仪的解决方案案例解释

振弦采集仪是一种用于测量结构物的振动状态和应力变化的高精度仪器&#xff0c;广泛应用于建筑、桥梁、隧道、地铁等工程领域。以下是一些常见的解决方案案例分析&#xff1a; 基础监测方案&#xff1a;对于大型建筑或桥梁工程&#xff0c;需要对基础进行实时监测。使用振弦采集…

System类 BigInterger BigDecimal

System类 常用方法和案例 exit&#xff1a; 退出当前程序 System.out.println("zhang"); // 0表示一个正常退出的状态 System.exit(0); System.out.println("cheng");System.arraycopy&#xff1a; 复制数组元素&#xff0c;比较适合底层的调用&#xf…

基于linux下的高并发服务器开发(第二章)- 2.2 进程状态转换

01 / 进程的状态 &#xff08;1&#xff09;三态模型 进程状态分为三个基本状态&#xff0c;即就绪态&#xff0c;运行态&#xff0c;阻塞态 &#xff08;2&#xff09;五态模型 在五态模型中&#xff0c;进程分为新建态&#xff0c;就绪态&#xff0c;运行态&#xff0c;阻…

mongodb练习---增删改查

环境&#xff1a; 1. 创建一个数据库 名字grade 2. 数据库中创建一个集合名字 class 3. 集合中插入若干数据 文档格式如下 &#xff5b;name:zhang,age&#xff1b;10,sex:m,hobby:[a,b,c]&#xff5d; hobby: draw sing dance basketball football pingpong compu…

Java8实战-总结2

Java8实战-总结2 基础知识方法和Lambda传递代码&#xff1a;一个例子从传递方法到Lambda 基础知识 方法和Lambda Scala和Groovy等语言的实践已经证明&#xff0c;让方法等概念作为一等值可以扩充程序员的工具库&#xff0c;从而让编程变得更容易。一旦程序员熟悉了这个强大的…

理解LLM中的ReAct

large language models (LLMs)大语言模型在语义理解和交互式决策方面有着不错的表现。ReAct在一次交互中循环使用推理和行动两个操作解决复杂问题&#xff0c;推理即利用模型自身语义理解能力&#xff0c;行动则利用模型以外的能力&#xff08;如计算、搜索最新消息&#xff0c…

OpenCv之滤波器

目录 一、卷积 二、方盒滤波与均值滤波 三、高斯滤波 四、中值滤波 五、双边滤波 一、卷积 图像卷积就是卷积核在图像上按行华东遍历像素时不断的相乘求和的过程 相关知识点: 步长:就是卷积核在图像上移动的步幅.(为充分扫描图片&#xff0c;步长一般为1)padding:指在图片…