[Practice] Seven, Hook, routing, and URL state management (Part 1) —— React17+React Hook+TS4 best practice, imitating Jira enterprise-level projects (11)


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. Implement useDocumentTitle with useRef - detailed explanation of useRef and Hook closure

(1) Use Helmeta custom title

Install react-helmetand its type declaration files @types/react-helmet:

npm i react-helmet
npm i -D @types/react-helmet
  • "react-helmet": "^6.1.0"
  • "@types/react-helmet": "^6.1.6"
  • high probability need--force

Edit src\unauthenticated-app\index.tsx(use Helmetcustom header):

...
import {
    
     Helmet } from 'react-helmet'

export const UnauthenticatedApp = () => {
    
    
  ...
  return (
    <Container>
      <Helmet>
        <title>请登录或注册以继续</title>
      </Helmet>
      ...
    </Container>
  );
};
...

Edit src\authenticated-app.tsx(use Helmetcustom header):

...
import {
    
     Helmet } from 'react-helmet'

export const AuthenticatedApp = () => {
    
    
  ...
  return (
    <Container>
      <Helmet>
        <title>项目列表</title>
      </Helmet>
      ...
    </Container>
  );
};
...

View the effect

(2) Implement useDocumentTitle with useRef

EDIT src\utils\index.ts(Added useDocumentTitle):

...
export const useDocumentTitle = (title: string, keepOnUnmount: boolean = true) => {
    
    
  const oldTitle = document.title

  useEffect(() => {
    
    
    document.title = title
  }, [title])

  useEffect(() => () => {
    
    
    if (!keepOnUnmount) {
    
    
      document.title = oldTitle
    }
  }, [])
}

keepOnUnmountdefault keep existing titlewill not restore

Modify the system default title public\index.html:

<title>Jira任务管理系统</title>

Edit src\unauthenticated-app\index.tsx(use useDocumentTitleinstead Helmetof custom header):

...
import {
    
     useDocumentTitle } from "utils";

export const UnauthenticatedApp = () => {
    
    
  ...
  useDocumentTitle('请登录或注册以继续')
  ...
};
...

Edit src\authenticated-app.tsx(use useDocumentTitleinstead Helmetof custom header):

...
import {
    
     useDocumentTitle } from "utils";

export const AuthenticatedApp = () => {
    
    
  ...
  useDocumentTitle('项目列表', false)
  ...
};
...

View the effect and switch pages

Submit the code to the remote warehouse, and then it’s lunch time (this part of the changed code will be gitcleared later)

edited src\utils\index.tsby useDocumentTitle:

...
export const useDocumentTitle = (title: string, keepOnUnmount: boolean = true) => {
    
    
  const oldTitle = document.title
  console.log('渲染时的oldTitle', oldTitle)

  useEffect(() => {
    
    
    document.title = title
  }, [title])

  useEffect(() => () => {
    
    
    if (!keepOnUnmount) {
    
    
      console.log('卸载时的oldTitle', oldTitle)
      document.title = oldTitle
    }
  }, [])
}

Open the login page, clear the console, and click Login to see:

渲染时的oldTitle 请登录或注册以继续

log out to see

渲染时的 oldTitle 项目列表
卸载时的 oldTitle 请登录或注册以继续

Analysis process:

  • login: AuthenticatedApppreload( willMount)
    • useDocumentTitleimplement
      • Save current oldTitlePlease log in or register to continue
      • Execute the first useEffectchange titleto list of items (load didMount)
      • save the second one useEffect( callbackpre-pre-uninstall)
  • logout: UnauthenticatedApppreload( willMount)
    • useDocumentTitleimplement
      • Store current oldTitleproject list
      • Execute the second of the previous useEffectpage stored before the first useEffect( callbackpre-unload willUnmount)
    • Restore the page titleto what was saved at that time (not logged out) oldTitlePlease log in or register to continue (the step of restoring will be executed even if UnauthenticatedAppit is not executed ) useDocumentTitlecallback
      • Execute useDocumentTitlethe first useEffectchange titleto the incomingtitle

Let's take a deep simulation to explore this process:

New: src\screens\ProjectList\test.tsx:

import {
    
     useEffect, useState } from "react"
import {
    
     useMount } from "utils"

export const Test = () => {
    
    
  const [num, setNum] = useState(0)

  const add = () => setNum(num + 1)

  useMount(() => {
    
    
    setInterval(() => {
    
    
      console.log('useMount setInterval', num)
    }, 1000)
  })

  useEffect(() => () => {
    
    
    console.log(num)
  }, [])

  return <div>
    <button onClick={
    
    add}>add</button>
    <p>
      number: {
    
    num}
    </p>
  </div>
}

EDIT src\screens\ProjectList\index.tsx(using component Test):

...
import {
    
     Test } from "./test";

export const ProjectList = () => {
    
    
  ...
  return (
    <Container>
      <Test/>
      ...
    </Container>
  );
};
...

Check the effect, click addthe button to increase randomly, the timer has always been 0, and after logging out, check the console, it is also 0, and it has been changed to "item list" in before, but after the AuthenticatedAppfinal oldTitlelogout, it is "Please log in or register to continue" strikingly similar

This is react hookthe classic pit with closures

Next, customize the function to simulate this process:

EDIT: src\screens\ProjectList\test.tsx:

import {
    
     useEffect, useState } from "react"
import {
    
     useMount } from "utils"

const test = () => {
    
    
  let num = 0

  const effect = () => {
    
    
    num += 1
    const message = `现在的num值${
      
      num}`
    return function unmount() {
    
    
      console.log(message)
    }
  }

  return effect
}

const add = test()
const unmount = add()
add()
add()
unmount() // 按照直觉,add()执行三次,这里应该打印3,但是实际是1

export const Test = () => {
    
    ...}

Refresh the page and print 1. Analyze the execution process according to the closure idea:

// 执行 test 返回 effect 函数
const add = test()
// 执行 effect 函数,返回引用了 message1 的 unmount 函数
const unmount = add()
// 执行 effect 函数,返回引用了 message2 的 unmount 函数
add()
// 执行 effect 函数,返回引用了 message3 的 unmount 函数
add()
unmount() // 按照直觉,add()执行三次,这里应该打印3,但是实际是1

understand? Each time the closure is called callback effect()is unique, there is no relationship between them, and const unmountonly the return value of a certain time is obtained callback effect(), of course it is also unique. This is the advantage of the closure, and it is also unnoticed. pit

How to avoid it? In fact, the code has already reminded:

React Hook useEffect has a missing dependency: 'num'. Either include it or remove the dependency array.

EDIT: src\screens\ProjectList\test.tsx(Deprecated useMountin exchange for both listening useEffectin and clearing in ):useEffectnumunmountInterval

...
export const Test = () => {
    
    
  ...
  useEffect(() => {
    
    
    const id = setInterval(() => {
    
    
      console.log('useMount setInterval', num)
    }, 1000)
    return () => clearInterval(id)
  }, [num])

  useEffect(() => () => {
    
    
    console.log(num)
  }, [num])

  ...
}

Re-execution: addafter each time, the timer will output the latest num, the second useEffect, that is, unmountoutput the last timenum

Return project code (see comments for understanding):

export const useDocumentTitle = (title: string, keepOnUnmount: boolean = true) => {
    
    
  const oldTitle = document.title;
  // 这里的 oldTitle 一直都是最新的
  console.log('渲染时的oldTitle', oldTitle)

  useEffect(() => {
    
    
    document.title = title;
  }, [title]);

  useEffect(() => () => {
    
    
    if (!keepOnUnmount) {
    
    
      console.log('卸载时的oldTitle', oldTitle)
      // 若不指定监听依赖 title,这里读到的就是 旧oldTitle
      document.title = oldTitle;
    }
  }, []);
};

This intentionally or unintentionally uses the characteristics of closures, but it is not friendly. Next, change the way and use useRefit to achieve useDocumentTitle:

Modify src\utils\index.ts:

import {
    
     useEffect, useRef, useState } from "react";
...
export const useDocumentTitle = (title: string, keepOnUnmount: boolean = true) => {
    
    
  const oldTitle = useRef(document.title).current;

  useEffect(() => {
    
    
    document.title = title;
  }, [title]);

  useEffect(() => () => {
    
    
    if (!keepOnUnmount) {
    
    
      document.title = oldTitle;
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);
};

Check the effect, clean up the test code and submit


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

Guess you like

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