[Practice] Seven, Hook, routing, and URL state management (middle) —— React17+React Hook+TS4 best practice, imitating Jira enterprise-level project (12)


Source of learning content: React + React Hook + TS Best Practice - MOOC


Compared with the original tutorial, I used the latest version at the beginning of my study (2023.03):

item Version
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

The specific configuration, operation and content will be different, and the "pit" will also be different. . .


1. Project launch: project initialization and configuration

2. React and Hook application: implement the project list

3. TS Application: JS God Assist - Strong Type

4. JWT, user authentication and asynchronous request


5. CSS is actually very simple - add styles with CSS-in-JS


6. User experience optimization - loading and error state handling



7. Hook, routing, and URL state management

1+2.

3. Add project list and project details routing

Home v6.11.2 | React Router

Install react-router react-router-dom

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

The latest version

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

New project details page src\screens\ProjectDetail\index.tsx:

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

Modification src\authenticated-app.tsx(extract PageHeader, introduce and use routing-related components):

...
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>
}
...

Modify the project list page src\screens\ProjectList\components\List.tsx(introduce the routing component 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>
  );
};

View the effect:

4. Add kanban and task group routing

Create a new Kanban page src\screens\ViewBoard\index.tsx:

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

New task group page src\screens\TaskGroup\index.tsx:

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

Modify the project details page src\screens\ProjectDetail\index.tsx(redirect to Kanban by default, and you can also switch Kanban and task groups freely):

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>
};

  • /Represents the root route. If you want to specify an additional path after the current route, you only need to write the path without adding/
  • using Navigateredirected can be written Routeaspath='/'index
  • An error is reported during the use of Navigate:[Navigate] is not a <Route> component. All component children of <Routes> must be a <Route> or <React.Fragment>

Similarly, after logging in, you need to jump to the default path /projects, and click PageHeaderto logoautomatically jump to the root path

Modification src\utils\index.ts(addition resetRoute):

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

Modify 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. Preliminary implementation of useUrlQueryParam to manage URL parameter status

New 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
}

There will be some problems with this part of the type system, other visible annotations

  • as const is a type of type assertion that avoids using its default type inference behavior, resulting in a wider or more general type.
    • for example:
      • let a = ['string', 123, true], awill be inferred as(string | number | boolean)[]
      • let a = ['string', 123, true, {}], abut it will be inferred that{}[]
    • as constAfter adding let a = ["string", 123, true, {}] as const, ait will be inferred asreadonly ["string", 123, true, {}]

Modification src\screens\ProjectList\index.tsx(use useUrlQueryParamfrom urlto get parameters):

...
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;
`;

So far, the desired function has been realized, but the page is continuously rendered in a loop. . . Let's find out why

6. Use useMemo to solve the dependency cycle problem-Detailed explanation of Hook's dependency problem

install why-did-you-render:

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

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

New 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.tsxIntroduce globally on the first line:

import './wdyr'

src\screens\ProjectList\index.tsxAdd to the component that needs to track the problem :

ProjectList.whyDidYouRenger = true

Or the question of which component is not sure which component can be set src\wdyr.tstotrackAllPureComponentstrue

I tried to add it alone, but it didn't work, so I used the default configuration, global tracking

The running result is shown in the figure:

insert image description here

Re-rendered because of hook changes:

  e-rendered because of props changes:"

    different objects that are equal by value.

It is not difficult to find that the values ​​of two different objects are equal, and they are monitored in useEffect, and then there is an infinite loop. . .

That means that the two objects are not the same (the reference address is different), that is to say, every time useEffect (useDebounce) is a newly created object

Then trace the source to find useUrlQueryParam ( src\utils\url.ts).

Aha, here is the problem, searchParamsit is new every time, how to solve it? Use 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 Index – React

Basic types and component states can be placed in dependencies; non-component state reference types (objects, arrays, methods) cannot.
When the component statevalue (defined by useState) is placed in the dependency list of useEffector , the value will be triggered useMemobecause only the corresponding is recognized changes, so it doesn't cause an infinite loop.setStatestate

View the page after modification, the infinite loop is gone

The bug is gone, you can set src\wdyr.tsmiddle to , or in the component:trackAllPureComponentsfalse

ProjectList.whyDidYouRenger = false

Some reference notes are still in draft stage, so stay tuned. . .

Guess you like

Origin blog.csdn.net/qq_32682301/article/details/131757003