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的函数体耗时较长才会有明显表现
原理
- 在接收到新值时,
useDeferredValue会以新值额外安排一个处于后台的、优先级较低的重新渲染.这个渲染总是安排在正常渲染任务之后,且可以取消. - 如果渲染过程中
useDeferredValue再一次取得了新值,本次渲染会被取消,并根据新值重复上一步. - 如果渲染顺利完成,则将渲染结果和组件state同步至最新的结果
- 渲染完成后才会执行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内 就会出现警告
这是因为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属性做了处理
因此 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脱离挂起状态