这是一篇系统性的选型文章,帮助你在 React 项目中判断:什么时候应该使用 createContext/Provider 模式,什么时候应该使用 useSyncExternalStore(完整拼写,不简写)。文章覆盖两者的定位、适用场景、使用原因、常见陷阱、优化策略和落地示例,并给出最终的决策清单。
1. 两种模式的定位
-
createContext/Provider(以下简称"Context")
- 定位:树内作用域化的数据传递(scoped configuration),强调就近覆盖、嵌套生效和与渲染同步的读取。
- 典型:主题 Token、国际化、方向、组件尺寸/禁用状态、局部业务配置、交互实例注入(如 antd 的 App.useApp)。
-
useSyncExternalStore
- 定位:并发渲染下对"外部可变状态源"的一致性订阅 API,解决"撕裂"(tearing)问题,确保一次提交中所有消费者看到同一版本快照。
- 典型:Redux/Zustand/Jotai 等外部状态库、自建全局 store、浏览器环境状态(媒体查询、网络状态、可见性、时钟、WebSocket 推送等)。
两者并非对立,而是面向不同问题域,常在同一个项目里互补使用。
2. 适用场景与使用原因
-
优先使用 Context 的场景
-
树内可覆盖的 UI 配置:主题/Design Token、国际化、方向、prefixCls、全局尺寸与禁用。
-
低频变化、读多写少的配置型数据。
-
需要作用域隔离与嵌套覆盖(同页多主题、局部禁用)。
-
需要与渲染时序严格对齐(尤其 CSS-in-JS、SSR 水合一致性)。
-
局部业务配置(如当前页面/表单的业务规则)。
-
使用原因:
- 作用域语义天然吻合;
- 与渲染同步,SSR 友好;
- 低频更新配合拆分 Context 与 useMemo 已足够高效;
- 工程心智与实现复杂度低。
-
-
优先使用 useSyncExternalStore 的场景
-
外部可变全局状态:跨页面共享、更新中高频、可能被非 React 事件驱动。
-
并发渲染(Transition/Suspense/重试)下,必须避免撕裂。
-
希望进行选择性订阅(仅订阅切片)以获得更细粒度性能优化。
-
浏览器/系统环境订阅,如媒体查询、online/offline、可见性、时间源、WebSocket。
-
使用原因:
- 让 React 协调"外部快照",避免同一提交内的版本不一致;
- 订阅管理契约清晰,适配并发特性;
- 便于封装 selector,做到按需更新。
-
3. 核心对比(帮助你把握边界)
-
数据来源与归属
- Context:树内数据(由 Provider 提供),属于 React 的数据流。
- useSyncExternalStore:树外可变源(全局单例或外部库),不受 React 调度直接管理。
-
作用域与覆盖
- Context:天然作用域隔离与嵌套覆盖,非常适合"就近覆盖"配置。
- useSyncExternalStore:默认是全局单例订阅;若要支持子树差异,需要额外实例与封装。
-
并发一致性与撕裂
- Context:React 自带一致性保障,不会 tearing。
- useSyncExternalStore:专门解决外部源在并发下的 tearing。
-
选择性订阅
- Context:默认"全量值变化即所有消费者更新";可用多 Context 拆分或 use-context-selector 优化。
- useSyncExternalStore:天然易于封装 selector,只让订阅的切片变化时更新。
-
SSR/样式系统
- Context:渲染期读取,SSR 与 CSS-in-JS 注入顺序易于对齐。
- useSyncExternalStore:需提供稳定的 getServerSnapshot,避免水合不一致。
-
复杂度与体积
- Context:最小心智成本;但要注意 value 引用稳定与拆分粒度。
- useSyncExternalStore:需要订阅契约、快照可比较、选择器与缓存等样板。
4. 反模式与常见陷阱
-
Context 侧
- value 每次 render 都新建对象/函数:导致所有消费者无谓重渲染。应 useMemo/useCallback 稳定引用。
- 大而全的 Context:把无关字段塞在一个 value 内,任何字段变化都会触发所有消费者更新。应拆分多 Context 或使用 use-context-selector。
- 指望 React.memo 抵消 useContext 触发的更新:做不到。优化要点在"订阅粒度",不是组件记忆。
-
useSyncExternalStore 侧
- getSnapshot 每次返回新对象:会被判定为"始终变化",导致频繁重渲染。需要结构共享或缓存,保持未变切片的引用不变。
- subscribe 通知时序不规范:应在状态更新完成后再通知,确保快照一致。
- 忽略 SSR:未提供稳定的 getServerSnapshot,导致水合不一致。
5. 实战示例
- Context:主题与交互实例(典型于 antd v5)
tsx
import { ConfigProvider, App as AntdApp, theme } from 'antd'
import zhCN from 'antd/locale/zh_CN'
export function Bootstrap() {
return (
<ConfigProvider
locale={zhCN}
theme={{ token: { colorPrimary: '#1677ff' } }}
>
<AntdApp>
<App />
</AntdApp>
</ConfigProvider>
)
}
function CardX() {
const { token } = theme.useToken()
const { message } = AntdApp.useApp()
return (
<div style={{ color: token.colorText }} onClick={() => message.success('OK')}>
Hello
</div>
)
}
- Context 优化:拆分 Context + useMemo 稳定引用
tsx
const ThemeContext = React.createContext<'light'|'dark'>('light')
const LangContext = React.createContext<'zh'|'en'>('zh')
function Providers({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = React.useState<'light'|'dark'>('light')
const [lang, setLang] = React.useState<'zh'|'en'>('zh')
const themeVal = React.useMemo(() => theme, [theme])
const langVal = React.useMemo(() => lang, [lang])
return (
<ThemeContext.Provider value={themeVal}>
<LangContext.Provider value={langVal}>
{children}
</LangContext.Provider>
</ThemeContext.Provider>
)
}
- useSyncExternalStore:极简外部 store 与订阅
tsx
type State = { count: number }
let state: State = { count: 0 }
const listeners = new Set<() => void>()
const store = {
getSnapshot: () => state,
subscribe: (l: () => void) => (listeners.add(l), () => listeners.delete(l)),
set(next: Partial<State>) {
state = { ...state, ...next }
listeners.forEach(l => l())
}
}
function useCount() {
return React.useSyncExternalStore(
store.subscribe,
() => store.getSnapshot().count,
() => 0 // SSR fallback
)
}
function Counter() {
const count = useCount()
return <button onClick={() => store.set({ count: count + 1 })}>{count}</button>
}
- useSyncExternalStore + 选择器封装
tsx
function useStoreSelector<T>(selector: (s: State) => T) {
const getSnap = React.useCallback(() => selector(store.getSnapshot()), [selector])
return React.useSyncExternalStore(store.subscribe, getSnap, getSnap)
}
function CountOnly() {
const count = useStoreSelector(s => s.count)
return <span>{count}</span>
}
- 想保留 Context 又要"选择性订阅":use-context-selector
ts
import { createContext, useContextSelector } from 'use-context-selector'
type Ctx = { user: { name: string } | null; lang: 'zh' | 'en' }
const Ctx = createContext<Ctx>({ user: null, lang: 'zh' })
function Provider({ children }: { children: React.ReactNode }) {
const [user, setUser] = React.useState<Ctx['user']>(null)
const [lang, setLang] = React.useState<Ctx['lang']>('zh')
const value = React.useMemo(() => ({ user, lang }), [user, lang])
return <Ctx.Provider value={value}>{children}</Ctx.Provider>
}
function UserName() {
const name = useContextSelector(Ctx, v => v.user?.name ?? 'Guest')
return <span>{name}</span>
}
6. 性能优化清单
-
使用 Context 时
- 拆分 Context,降低"无关字段变化"造成的重渲染。
- Provider 的 value 用 useMemo,回调用 useCallback。
- 避免在 value 中放每次都新建的对象/数组/函数。
-
使用 useSyncExternalStore 时
- 确保 getSnapshot 返回可比较、稳定的值;对复杂切片做结构共享/缓存。
- 订阅通知的时序保持"先更新,再通知"。
- 需要选择器时封装 useStoreSelector,避免在组件层每次创建选择逻辑。
- SSR 提供稳定 getServerSnapshot,防止水合差异。
7. 决策树(选型速查)
-
你是否需要"树内就近覆盖/嵌套覆盖"的作用域?
- 是 → createContext/Provider。
-
状态是否来自"树外可变全局源",且更新频繁或由外部事件驱动?
- 是 → useSyncExternalStore 或采用内置它的状态库(Redux/Zustand/Jotai)。
-
是否只在局部组件内、短生命周期使用?
- 是 → useState/useReducer。
-
想保留 Context,又要按字段订阅降低无关渲染?
- 用 use-context-selector。
-
是否有严格的 SSR 与 CSS-in-JS 样式注入顺序要求?
- 是 → Context(更易保障渲染-样式一致性)。
8. 总结
- createContext/Provider 解决"树内作用域化配置"的问题,简单、可覆盖、SSR 友好。对于主题、国际化、尺寸/禁用与局部业务配置,应优先选择它,并通过"拆分 Context + useMemo"获得足够的性能。
- useSyncExternalStore 解决"树外可变源在并发渲染下的一致性(防撕裂)",适用于外部全局状态与环境订阅;若你使用 Redux/Zustand/Jotai 等,它们已在内部适配了 useSyncExternalStore。
- 在同一项目中,两者通常互补:UI 配置走 Context,业务状态走 useSyncExternalStore(或状态库),局部状态走 useState/useReducer。这是清晰、稳定、可维护的组合策略。