You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React报错:更新深度超限,useSyncExternalStore快照缓存疑问

使用useSyncExternalStore替代useEffect订阅外部状态的问题排查

问题背景

我正在测试用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.09 17:25:26