React中的并发/异步API

146 阅读6分钟

useDeferredValue

简述

取得一个值的延迟版本

示例

export const SearchPage: FC = () => {
  const [query, setQuery] = useState('')
  const deferredQuery = useDeferredValue(query)
  return (
    <>
      <input value={query} onChange={(e) => setQuery(e.target.value)}></input>
      <ContentComp query={deferredQuery}></ContentComp>
    </>
  )
}

用户连续输入的时候 query不断变化 但deferredQuery与最初的内容一致 直到用户停止输入才会同步到最新值
此时ContentComp的参数才会变化 并进一步引起展示内容的变化
这个现象在需要ContentComp的函数体耗时较长才会有明显表现

原理

  1. 在接收到新值时,useDeferredValue会以新值额外安排一个处于后台的、优先级较低的重新渲染.这个渲染总是安排在正常渲染任务之后,且可以取消.
  2. 如果渲染过程中useDeferredValue再一次取得了新值,本次渲染会被取消,并根据新值重复上一步.
  3. 如果渲染顺利完成,则将渲染结果和组件state同步至最新的结果
  4. 渲染完成后才会执行Effect,被中断的渲染不会进入Effect阶段

说明

  • 函数签名是useDeferredValue(value, initialValue?),可以接受第二个参数作为初始值
  • 此函数使用Object.is判断变更
  • 当状态更新发生在startTransition内部时,useDeferredValue不生效.本次更新已经被标记为低优先级,此hook不会有额外动作,而是立刻返回接受到的最新值.
  • Suspense一起使用时,被此hook延迟后的状态,其更新不会导致fallback展示.页面会维持旧版本,直到新页面完全准备好.
  • useDeferredValue的本质是降低组件更新的优先级.每当参数变化时,当前组件及其子组件的函数都是可能被调用的("可能"是因为在调用之前渲染有可能被中断了),只是没有实际渲染.因此,如果组件函数的函数体内发起了网络请求,那么这个请求会在每次参数变化时发起.
  • 在上面这种情况下,如果网络请求位于effect中,就会渲染新组件后仅发起一次.但网络请求触发的fallback会显示出来.
  • 这个钩子的延迟效果仅限react调度范围内 如果组件函数简单但渲染内容较多 例如几千个重复的dom 还是会卡顿
  • 被延迟的组件需要被memo包裹 否则无效
  • 这个hook适用于组件本身较为复杂耗时的场景

useTransition

简述

延迟指定的更新行为

示例

export const SearchPage: FC = () => {
  const [query, setQuery] = useState(query)
  const [isPending, startTransition] = useTransition()
  return (
    <>
      <input
        onChange={(e) =>
          startTransition(() => setQuery(e.target.value))
        }
      ></input>
      <ContentComp query={query}></ContentComp>
    </>
  )
}

直到用户停止连续输入才显示新ui.
isPending表明是否有更新还未应用,这个值仅指示与之同时返回的那个startTransition.

原理

startTransition会将其内部的状态更新标记为低优先级的.和useDeferredValue类似,它内部的状态变更也总是会引起后台的、可中断的渲染.

说明

  • 可以直接从react引入startTransition,但这样会失去isPending的信息
  • 这个hook的原理和useDeferredValue类似,效果和适用场景也相同
  • startTransition会立刻执行接受的函数.它对变更的延迟体现在,其内部的变更被标记为低优先级,而不是延迟执行.可以将startTransition简单地理解为
let isInsideTransition = false;  
function startTransition(action) {
  isInsideTransition = true  
  action()
  isInsideTransition = false;  
}
  • 从上一点可以知道 startTransition仅标记同步部分的状态变更 如果异步部分也需要延迟 需要在异步部分再调用一次startTransition
  • 在异步函数中进行状态更新可能顺序混乱.如果函数发起网络请求,并根据结果更新状态,并且恰好请求"后发先至",那么后发起的请求会先变更状态.这是正常的,这是因为react无法判断这些变更在逻辑上的顺序,需要自己维护队列.
    对于这种情况,可以使用useActionState,这个hook可以确保状态按照调用顺序更新

useActionState

简述

管理异步状态.如果一个状态的更新是异步的,可以使用这个hook.
仅在react19中可用.

示例

async function increaseNumber(currentState: number, arg: any) {
  await new Promise((res) => setTimeout(res, 1000))
  return currentState + 1
}

const Counter: FC = () => {
  const [state, dispatch, isPending] = useActionState(increaseNumber, 0)
  return (
    <form action={dispatch}>
      <button type='submit'>{isPending ? '...正在更新' : state}</button>
    </form>
  )
}

参数是更新函数、初始值,返回结果是当前状态、状态更新函数、是否正在更新
更新函数的第一个参数是当前状态 第二个参数类型任意 在调用dispatch时提供 这一点和useReducer是类似的

说明

  • 这个hook可以确保状态按调用状态进行更新 无论dispatch的调用情况和网络状况 下一次increaseNumber调用时 上一次一定已经结束
  • dispatch函数的作用是发起一次更新.
    它可以直接用在form的action属性上 或者在startTransition内部被调用
<button onClick={() => startTransition(dispatch)}>
   {isPending ? '...正在更新' : state} 
</button>
  • 如果不包裹在startTransition内 就会出现警告
    image.png
    这是因为react认为异步的dispatch是一个不紧急的更新 而直接调用此函数时 react无法将其标记为低优先级 需要手动管理
    但这只是个警告 直接写onClick={dispatch}不影响使用

use

简述

use可以当作useContext用,也可以use(Promise)从promise里取值
hook不同 use可以放在条件和循环中

说明

  • use(Promise)中 这个promise对象必须具有一定的不可变性 可以来自父组件、memo、或者全局状态等 不能搞use(fetch(xxx)) 不然会无限循环
  • 值类型为plain object的promise 可以作为参数 从服务端组件传给客户端组件

useSyncExternalStore

参考 juejin.cn/post/738944…
这个hook的用处是连接一个react调度范围之外的数据源
来自useSyncExternalStore的状态更新无法被标记为低优先级,总是会触发fallback

useOptimistic

简述

获取一个状态的乐观版本.
乐观更新是一种使页面表现更加灵敏的呈现方式.
例如,对于一个聊天软件,在消息发出后立刻将其渲染至聊天框中,如果发起消息的网络请求失败,则改为显示失败ui.
对异步行为的结果进行乐观地预期、并按这个预期展示页面就叫做乐观更新.

示例

const updateFn = (state: string, val: string) => state + val

const Comp: FC = () => {
  const [state, setState] = useState<string>('value')
  const [optimisticState, addOptimisticState] = useOptimistic(state, updateFn)

  const formAction = async () => {
    addOptimisticState('(pending)')
    setState((state) => updateFn(state, 'a'))
    await new Promise((res) => setTimeout(res, 2000))
  }

  return (
    <form action={formAction}>
      {optimisticState}
      <button type='submit'>发送</button>
    </form>
  )
}

formAction未完成时,optimisticState等于updateFn返回的"乐观值";在其完成后,optimisticState与参数中的state相同.
因此 即使在action中立刻更新了state,页面上也会先显示2s的'pending' 然后切换到 'valuea'

说明

  • addOptimisticState只能用于表单提交.
    Next项目中实测,无论是否包裹startTransition,只要在formAction之外的地方(例如onClick)调用addOptimisticState都会导致页面立刻刷新.浏览器会发起一个http://localhost:3000/? 的GET请求并引发页面刷新.

    此外,可以看到react对表单的action属性做了处理 image.png
    因此 useOptimistic的特殊性可能是react对表单的特殊设计导致的

  • 后续的formAction不会将之前的覆盖.
    将formAction改为下图

let num = 1

const Comp: FC = () => {
  // ...
  const formAction = async () => {
    addOptimisticState('(pending)')
    setState((state) => updateFn(state, 'a'))
    --num
    if (num === 0) return
    await new Promise((res) => setTimeout(res, 3000))
  }

连续点击两次 不会因为后一个action的结束而将optimisticState脱离挂起状态