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

这篇具有很好参考价值的文章主要介绍了【实战】 七、Hook,路由,与 URL 状态管理(中) —— React17+React Hook+TS4 最佳实践,仿 Jira 企业级项目(十二)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。


学习内容来源: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
prettier 2.8.4
json-server 0.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

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

运行结果如图:

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

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

部分引用笔记还在草稿阶段,敬请期待。。。文章来源地址https://www.toymoban.com/news/detail-571504.html

到了这里,关于【实战】 七、Hook,路由,与 URL 状态管理(中) —— React17+React Hook+TS4 最佳实践,仿 Jira 企业级项目(十二)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包