React报错:更新深度超限,useSyncExternalStore快照缓存疑问
问题背景
我正在测试用useSyncExternalStore替代原本useEffect实现的外部状态订阅,相关代码如下:
组件中使用useSyncExternalStore的代码
const state = BoardState.getInstance(); const boardUIstate = useSyncExternalStore( (observer) => state.subscribe(observer), () => getUiStateFromExternalState(state.getState()) );
外部状态类实现
单例状态类BoardState:
export class BoardState<T> extends Subject<T> { private static instance: BoardState<unknown> | undefined; private constructor() { super(); } public static getInstance<T>(): BoardState<T> { if (!BoardState.instance) { BoardState.instance = new BoardState(); return BoardState.instance as BoardState<T>; } return BoardState.instance as BoardState<T>; } }
基础观察者类Subject:
export class Subject<T> { protected state: T | undefined; private observers: any[] = []; subscribe(fn: (newState: T | undefined) => void) { this.observers.push(fn); return () => this.unsubscribe(fn); } private unsubscribe(fn: (newState: T | undefined) => void) { this.observers = this.observers.filter((ob) => ob !== fn); } setState(state: T) { this.state = state; this.observers.map((ob) => ob(this.state)); } // 省略其他方法 }
遇到的问题
警告信息
Warning: The result of getSnapshot should be cached to avoid an infinite loop
错误信息
Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
之前用useEffect实现时无此问题,现需分析原因并解决。
问题分析与解决
1. 快照缓存相关问题
什么是快照缓存?
useSyncExternalStore要求getSnapshot函数返回的快照值必须稳定:如果外部原始状态没变化,多次调用getSnapshot必须返回同一个引用的值。如果像你现在这样,每次调用getUiStateFromExternalState都生成新对象,React会判定状态一直在变更,进而触发无限重渲染——这就是警告的来源。
如何实现快照缓存?
核心逻辑是:仅当外部原始状态变化时,才重新生成转换后的UI状态,否则返回缓存值。有两种可行方案:
方案一:在外部状态类内部缓存快照
修改Subject类,维护一个UI状态缓存,原始状态更新时同步更新缓存:
export class Subject<T> { protected state: T | undefined; private observers: any[] = []; private uiStateCache: ReturnType<typeof getUiStateFromExternalState>; private updateUiStateCache() { this.uiStateCache = getUiStateFromExternalState(this.state); } setState(state: T) { this.state = state; this.updateUiStateCache(); // 原始状态更新时同步更新缓存 this.observers.forEach(ob => ob(this.state)); } getUiState() { return this.uiStateCache; } // 原有方法不变 }
组件中调用时直接取缓存值:
const boardUIstate = useSyncExternalStore( (observer) => state.subscribe(observer), () => state.getUiState() );
方案二:在组件内用useMemo缓存转换结果
先订阅原始状态,再通过useMemo缓存转换后的UI状态,确保仅原始状态变化时才重新计算:
const state = BoardState.getInstance(); // 先订阅原始状态,获取稳定的原始快照 const rawState = useSyncExternalStore( (observer) => state.subscribe(observer), () => state.getState() ); // 用useMemo缓存转换后的UI状态 const boardUIstate = useMemo(() => getUiStateFromExternalState(rawState), [rawState]);
是不是React的bug?
不是。这是useSyncExternalStore的设计要求——它依赖快照的稳定性来判断是否触发重渲染,以此保证并发模式下的正确性。useEffect的订阅模式没有这个严格约束,所以之前没出现问题。
2. 无限更新深度错误的解决
这个错误是快照不稳定导致的连锁反应:快照每次都是新对象→React判定状态变更→触发重渲染→再次生成新快照→循环往复。解决了快照缓存问题,这个无限循环错误会自动消失。
额外检查点:
- 确保
subscribe返回的取消订阅函数能正确移除观察者,避免重复触发更新; - 确认
getUiStateFromExternalState没有在转换过程中意外修改外部状态,导致触发订阅回调形成循环。
内容的提问来源于stack exchange,提问作者Dilshan

