[React] React 官方文档阅读

[React] React 官方文档阅读

·

4 min read

随着学习的不断深入,想系统地看看react的官方文档,于是整理一篇笔记来总结归纳

useState 和 useEffect

1. react 状态管理

react使用useState这个hook作为其基本的状态管理,需要注意的是,react的状态在更新时,整个组件会被完整地重新渲染,包括以该组件为根的整个组件树;同时,由于hook函数是默认无状态的,如果不使用useStateuseEffecthook,那么所有的普通变量都会被重新创建一次

2. 无限 re-render

在使用useStateuseEffect时,要注意不要陷入无限的re-render,例如:

  • 在组件hook函数内顶部直接setSsate
import { useEffect, useState } from "react"const Center = () => {
  const [res, setRes] = useState(0)
  setRes(2)

  return (
    <div>{ res }</div>
  )
}
​
export default Center
  • 使用useEffect时不传入依赖参数,同时在effect的回调中setState
import { useEffect, useState } from "react"const Center = () => {
  const [res, setRes] = useState(0)
​
  useEffect(() => {
    setRes(1)
  })
​
  return (
    <div>{ res }</div>
  )
}
​
export default Center
  • useEffectsetStateuseEffect有相同的state
mport { useEffect, useState } from "react"const Center = () => {
  const [res, setRes] = useState(0)
​
  // 设置的 state 和 依赖的 state 是相同的
  useEffect(() => {
    setRes(1)
  }, [res])
​
  return (
    <div>{ res }</div>
  )
}
​
export default Center

这是因为每次setState都会导致组件全量重新渲染,而每次全量渲染的时候又会执行setState,从而陷入死循环:

3. useEffect 执行机制

useEffect中可以return一个cleanup函数,每当依赖项变更后,先使用旧的state、props来运行cleanup函数,然后再使用新的state、props运行setup函数

通过这个机制,在useEffect内部的数据请求可以做一个 "竞态" 优化,实际上就是通过闭包来忽略之前的请求结果,以避免先发出的请求后获取到结果而将后发出的请求所获取到的结果覆盖,官方文档的示例:使用 Effect 同步 – React 中文文档

4. useEffect 的依赖项

useEffect的依赖列表不一定必须要响应式值,它可以是任意的在组件函数内部声明的变量或函数,只要在每一轮的渲染中reactObject.is来比较,发现前后的值不一样的话,就会触发这个useEffect的执行

在下面这个例子中,每次点击按钮更新state都会触发普通变量doubleNumber的更新,从而触发useEffect执行:

import { useEffect, useState } from "react"const EffectLearn = () => {
  const [number, setNumber] = useState(0)
​
  const doubleNumber = number * 2
​
  useEffect(() => {
    console.log("doubleNumber", doubleNumber)
  }, [doubleNumber])
​
  return (
    <>
      <button onClick={() => setNumber(number => number + 1)}>点击加一:{number}</button>
      double: {doubleNumber}
    </>
  )
}
​
export default EffectLearn

另一种状态 -- useRef

使用ref后,react会在后续的组件渲染中保存该值,不会像普通变量那样因组件重新渲染而重置;它与state的不同点是ref不会因修改而触发组件的重新渲染

ref有两个用途:

  • 用于引用并保存一个当前值

  • 用于引用dom或组件元素,可以获取到元素的实例

对于ref的读取与写入,官方文档有如下说明:

image-20240215115427777

组件 props

关于props的基本使用,大家肯定都不陌生,那么这里就只说一点:props不属于state,但它仍然可以被useEffect监听

实际上,只要是一个变量 ( 包括普通变量 ),就可以被useEffect监听;但是useEffect的定义是要在组件重新渲染后执行的,所以又必须通过修改state来触发组件的重新渲染,重新渲染后,useEffect会比较其依赖列表中上一次渲染时的值和本次渲染时的值是否相同,来判断是否要执行effect回调

createContext 和 useContext

这两个api有点类似与vueprovide、inject,但是createContextuseContext在获取数据时并不局限于组件树中的层级关系,而是可以将context抽离出来

1. 在组件树中提供数据

使用createContext创建一个context,然后用contextprovider来向其下的组件树提供该context

value就是你要通过这个provider向组件树提供的context

import { createContext } from 'react'const ThemeContext = createContext('light')
​
function App() {
  const [theme, setTheme] = useState('light')
​
  return (
    <ThemeContext.Provider value={theme}>
      <Page />
    </ThemeContext.Provider>
  );
}

2. 将 context 抽离到一个文件中并导出使用

在子组件中使用useContext来获取某个context,如果在父组件中的组件树都没有使用provider来提供值,那么就会使用createContext时所传入的初始值 ( 默认值 ),这个特性就使得了context可以被抽离出来到一个文件中

这个特性一般与mobx结合使用导出一个store,下面是使用示例

  • 创建一个store
import { makeAutoObservable } from 'mobx'// 定义一个类,作为 store
class Counter {
  // 属性是 store 要存储的数据
  count = 0constructor() {
    makeAutoObservable(this)
  }
​
  // 方法就是修改 state 的 action
  increase() {
    this.count += 1
  }
​
  decrease() {
    this.count -= 1
  }
}
​
export default Counter
  • 抽离到一个统一的store中导出使用:
import Counter from "./counter"
import { createContext, useContext } from "react"const store = {
    counterStore: new Counter()
}
​
const StoreContext = createContext(store)
​
// 自定义 hook,结合 context,用于获取 store
const useStore = () => useContext(StoreContext)
​
export default useStore
  • 在组件中使用store
import useStore from "@/store"
import { observable } from "mobx-react"function App() {
    const { counterStore } = useStore()

    return (
        <div>{counterStore.count}</div>
    )
}
​
// 用 observable 包裹组件,响应式监听 store
export default observable(App)

使用 useCallback 缓存函数

这是一个性能优化hook,用于将传入回调函数在组件首次渲染后缓存起来,即后续的重复渲染不会创建新的函数了

1. 在 useCallback 中更新 state

在一个函数中更新state是非常常见的,但如果你将他包裹到useCallback中,就有一些问题需要注意了,看下面的例子:

  • handleSetNumberCommon函数中,你多次点击后会使得number逐渐增加

  • handleSetNumberCallback函数中,你多次点击后number总是为 1

这就很好地阐述了useCallback的作用:将一个函数缓存起来;当你将一个函数缓存起来时,只有在组件首次渲染时会创建并返回这个函数,所以组件首次渲染时number为 0,那么number + 1就永远为 1 ,由于函数中使用的state只是一个值,所以它也永远不会改变

import { useCallback, useState } from "react"const UseCallbackLearn = () => {
  const [number, setNumber] = useState(0)
​
  const doubleNumber = number * 2const handleSetNumberCommon = () => {
    setNumber(number + 1)
  }
​
  const handleSetNumberCallback = useCallback(() => {
    setNumber(number + 1)
  }, [])
​
  return (
    <>
      <button onClick={handleSetNumberCommon}>不使用callback加一</button>
      <button onClick={handleSetNumberCallback}>使用callback加一</button>
      <div>number: {number}</div>
      <div>double: {doubleNumber}</div>
    </>
  )
}
​
export default UseCallbackLearn

要处理上述的bug,我们就要为useCallback声明依赖项,如下:

import { useCallback, useState } from "react"const UseCallbackLearn = () => {
  const [number, setNumber] = useState(0)
​
  const doubleNumber = number * 2// 声明依赖项,在 number 变化时重新创建这个函数
  const handleSetNumberCallback = useCallback(() => {
    setNumber(number + 1)
  }, [number])
​
  return (
    <>
      <button onClick={handleSetNumberCallback}>使用callback加一</button>
      <div>number: {number}</div>
      <div>double: {doubleNumber}</div>
    </>
  )
}
​
export default UseCallbackLearn

但是这种做法在react官方文档中并不推荐,我们可以将其改写为如下形式:

import { useCallback, useState } from "react"const UseCallbackLearn = () => {
  const [number, setNumber] = useState(0)
​
  const doubleNumber = number * 2// 向 set 函数传入一个更新函数,取消声明依赖项
  const handleSetNumberCallback = useCallback(() => {
    setNumber(number => number + 1)
  }, [])
​
  return (
    <>
      <button onClick={handleSetNumberCallback}>使用callback加一</button>
      <div>number: {number}</div>
      <div>double: {doubleNumber}</div>
    </>
  )
}
​
export default UseCallbackLearn

lazy() 函数

lazy函数接收一个回调函数,该回调在你真正使用导入的组件之前都不会执行,该回调执行了之后,组件的引入就会被缓存,之后使用该组件时,都不会重新引入了,除非手动刷新应用

lazy函数用于动态地引入组件,可以在打包时进行代码分割;除此之外,lazy还可以在组件初次挂载完成前触发Suspensefallback备用方案

lazy能够触发Suspense组件的fallback的原因是它返回一个promise,在组件引入完成之前,promise会一直被阻塞

Suspense 组件

在阅读react文档的 "参考" 的时候看到了Suspense组件,于是看了看官方文档的例子,发现每当组件重新获取数据(promise)时,都会触发Suspensefallback,但是文档中又提到了Suspense无法检测useEffect中的数据请求,那么它是如何触发Suspense的?我查了一下之后发现,应该是和react-cache结合使用的

1. Suspense fallback 的触发条件

官方文档对于Suspense fallback的触发条件的说明如下:

所以说,在正常情况下,使用CSR react时,Suspense的作用就是组件懒加载时显示fallback组件,在useEffect中进行异步数据请求时,Suspense组件并不起作用

实际上,Suspense组件会捕获它的children内抛出的任意东西,如果抛出一个promise,则Suspense会显示fallback组件,当这个promiseresolve后,它的children组件就会重新挂载

2. 结合 react-cache 使用

那么如果不在useEffect中请求数据呢?我们可以使用react-cache;具体的使用方法可以参考这篇文章:展望 react-cache,一个 React 官方的处理数据副作用方案 - 掘金 (juejin.cn)

现在我们手动实现一个简陋版的react-cache来看一下:

function wrapPromise(promise) {
    let status = 0;
    let result;
    // 调用promise,并在回调中更改加工函数中的状态
    const callPromise = promise.then(
        (res) => {
            status = 1;
            result = res;
        },
        (err) => {
            status = -1;
            result = err;
        }
    );
    // 返回一个对象,只需要提供read方法
    return {
        read() {
            switch (status) {
                case 0:
                    throw callPromise;
                case 1:
                    return result;
                case -1:
                    throw result;
            }
        },
    };
}
​
export default wrapPromise

然后我们在组件文件的顶部调用wrapPromise函数,并在组件hook函数内调用read()方法获取数据:

import { useEffect, useState } from "react"
import wrapPromise from "../../utils/throw-promise"
​
​
const promise = () => {
  return new Promise<number>((resolve) => {
    setTimeout(() => resolve(1), 2000)
  })
}
​
// 在文件顶部调用加工函数
const data = wrapPromise(promise())
​
const Center = () => {
​
  // 在组件 hook 内读取数据
  const res = data.read()
​
  return (
    <>
      <div>{ res }</div>
    </>
  )
}
​
export default Center

但是这样有一个坏处就是你在组件文件顶部调用wrapPromise,那么获取到的数据是固定的,即使组件后面会重新挂载;所以官方文档目前也是不推荐在实际开发中使用

我还看了一下这个包5年前就已经发布了,到现在都还没有什么大的进展,估计也没啥用了:react-cache - npm (npmjs.com)

那么,Suspense在客户端组件下就只有lazy能触发fallback吗?其实,还有一个非常流行的数据请求管理库--react-query,现在叫tanstack-query,可以在获取数据阶段触发Suspense组件的fallback,但是我打算后面再单独开一篇博客来讲一下,这里就仅仅只是引出这个库,不做过多介绍