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

文章目录

    • 一、项目起航:项目初始化与配置
    • 二、React 与 Hook 应用:实现项目列表
    • 三、TS 应用:JS神助攻 - 强类型
    • 四、JWT、用户认证与异步请求
    • 五、CSS 其实很简单 - 用 CSS-in-JS 添加样式
    • 六、用户体验优化 - 加载中和错误状态处理
    • 七、Hook,路由,与 URL 状态管理
      • 1.⽤useRef实现useDocumentTitle - useRef与Hook 闭包详解
        • (1)使用 `Helmet` 自定义标题
        • (2)⽤ useRef 实现 useDocumentTitle


学习内容来源: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.⽤useRef实现useDocumentTitle - useRef与Hook 闭包详解

(1)使用 Helmet 自定义标题

安装 react-helmet 和它的类型声明文件 @types/react-helmet

npm i react-helmet
npm i -D @types/react-helmet
  • "react-helmet": "^6.1.0"
  • "@types/react-helmet": "^6.1.6"
  • 大概率需要 --force

修改 src\unauthenticated-app\index.tsx(使用 Helmet 自定义标题):

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

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

修改 src\authenticated-app.tsx(使用 Helmet 自定义标题):

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

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

查看效果

(2)⽤ useRef 实现 useDocumentTitle

编辑 src\utils\index.ts(新增 useDocumentTitle):

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

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

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

keepOnUnmount 默认保持现有 title 不会还原

修改系统默认标题 public\index.html

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

修改 src\unauthenticated-app\index.tsx(使用 useDocumentTitle 代替 Helmet 自定义标题):

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

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

修改 src\authenticated-app.tsx(使用 useDocumentTitle 代替 Helmet 自定义标题):

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

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

查看效果,并切换页面

提交代码到远程仓库,接下来是下饭时间(这部分更改的代码后续会通过 git 清掉)

编辑 src\utils\index.tsuseDocumentTitle

...
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
    }
  }, [])
}

打开登录页,清理控制台,点击登录可看到:

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

登出,可看到

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

分析过程:

  • 登录:AuthenticatedApp 预加载(willMount
    • useDocumentTitle 执行
      • 存储当前 oldTitle 请登录或注册以继续
      • 执行第一个 useEffect 更改 title 为 项目列表(加载 didMount
      • 将第二个 useEffectcallback 存起来(预预卸载)
  • 登出:UnauthenticatedApp 预加载(willMount
    • useDocumentTitle 执行
      • 存储当前 oldTitle 项目列表
      • 在第一个 useEffect 前执行之前存储的 上个页面的 第二个 useEffectcallback(预卸载 willUnmount
    • 将页面 title 还原为当时(未登出)存起来的 oldTitle 请登录或注册以继续(还原这一步即使 UnauthenticatedApp 没有执行 useDocumentTitle 也会执行 callback
      • 执行 useDocumentTitle 第一个 useEffect 更改 title 为 传入的 title

接下来深度模拟探究一下这一过程:

新建: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>
}

编辑 src\screens\ProjectList\index.tsx(使用组件 Test):

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

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

查看效果,点击 add 按钮随机增加,定时器中一直是0,登出后查看控制台,也是 0 ,和之前明明在 AuthenticatedAppoldTitle 已经改为“项目列表”,最终登出后却是“请登录或注册以继续”惊人的相似

这就是 react hook 与 闭包 经典的坑

接下来自定义函数来模拟这个过程:

编辑: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 = () => {...}

刷新页面,打印的是1,按闭包思路分析执行过程:

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

懂了吗?每次调用闭包中 callback effect() 都是唯一的,之间并无关联,而 const unmount 拿到的只是某一次 callback effect() 的返回值,当然也是独一无二的,这即是闭包的优势,也是注意不到时,闭包的坑

如何避免呢?其实代码中已经提醒了:

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

编辑:src\screens\ProjectList\test.tsx(弃用 useMount 换回 useEffect,并在 useEffect 中都监听 num,且在 unmount 时清除 Interval):

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

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

  ...
}

重新执行:每次 add 后,定时器都会 输出最新 num, 第二个 useEffect,即 unmount 输出上一次的 num

回归项目代码(理解看注释):

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;
    }
  }, []);
};

这样有意无意地使用了闭包的特性,但是并不友好。接下来换个方式,使用 useRef 实现 useDocumentTitle:

修改 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
  }, []);
};
  • Hook API 索引 – React

查看效果,清理测试代码并提交


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

你可能感兴趣的:(react.js,前端,前端框架)