692 字
3 分钟
useSyncExternalStore

什么时候用它#

React 之外有一份会自己变化的数据:浏览器 API、第三方状态库、事件总线。你想在组件里读它,还希望它变了界面跟着更新,就用这个 Hook。

官方有一条注记:能放进 useState 的状态就放进去。这个 API 主要是给「对接已有的非 React 代码」用的。

两个函数的分工#

const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)

subscribe(callback) 负责订阅,数据变化时调用 callback,并返回一个取消订阅的函数。

getSnapshot() 负责读当前值。数据没变时,它必须返回同一个值,React 用 Object.is 比较。

getServerSnapshot 是服务端渲染时用的快照,可选。组件要在服务端渲染却不给这个参数,会直接报错。

一个能跑的例子#

import { useSyncExternalStore } from 'react'
function subscribe(callback) {
window.addEventListener('online', callback)
window.addEventListener('offline', callback)
return () => {
window.removeEventListener('online', callback)
window.removeEventListener('offline', callback)
}
}
function getSnapshot() {
return navigator.onLine
}
export function useOnlineStatus() {
return useSyncExternalStore(subscribe, getSnapshot)
}

两个函数都写在组件外,引用天然稳定,不需要额外包 useCallback。

getSnapshot 必须返回稳定的值#

这是最容易踩的坑。每次调用都新建对象,React 会认为值一直在变:

// 错:每次调用都返回新对象
function getSnapshot() {
return { width: window.innerWidth, height: window.innerHeight }
}

控制台会报「The result of getSnapshot should be cached」。窗口宽度这种单个值直接返回原始类型即可:

function getSnapshot() {
return window.innerWidth
}

确实需要返回对象时,自己缓存:数据没变就把上一次那个对象原样返回。

subscribe 也要稳定#

把 subscribe 定义在组件内,每次渲染都是新函数,React 会退订再重新订阅。定义在组件外即可;订阅需要跟着参数走(比如房间号)时,用 useCallback 把它固定住。

服务端渲染#

省略第三个参数时,组件在服务端渲染会抛错。给它一个服务端也成立的快照,比如 () => true,并保证服务端与水合时用的值一致。

并发下它保证什么#

外面的世界不受 React 调度控制。一次非阻塞更新期间外部数据被改动时,React 会把这次更新降级为阻塞更新、从头重算,避免屏幕上同时出现两个版本的数据。这就是它存在的理由。

几条边界#

别用外部快照的值来挂起渲染,比如据此条件渲染一个 lazy 组件。官方不建议这么做:外部数据的变化无法标记为非阻塞更新,会把屏幕上已经显示的内容换成加载态。

读到的值不要自己改,它属于外部数据源。用现成的状态库时,直接用库提供的 Hook,不必自己写这一层。

第 23 篇,共 39 篇本文属于系列

useSyncExternalStore
https://www.choria.top/posts/hook-use-sync-external-store/
作者
乔瑞雅·克林威尔
发布于
2026-05-02
许可协议
CC BY-NC-SA 4.0

分享文章

生成精美分享图或复制链接,与更多人分享本文。

继续阅读

沿着主题读

基于共同的标签与分类

换条路线

从其他文章中稳定抽取

最后更新于 ,距今已过 190 天

部分内容可能已过时