From c81a4d55602a859a0a72270b0c5bd06fc6c85337 Mon Sep 17 00:00:00 2001 From: zerosrat Date: Thu, 16 Apr 2026 20:56:51 +0800 Subject: [PATCH] fix: resolve infinite update loop when using createRoot (React 18) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When using React 18's `createRoot` instead of `ReactDOM.render`, two code paths cause "Maximum update depth exceeded" errors: 1. FallbackListener.componentDidMount calls setState synchronously, creating a tight loop: componentDidMount → setState → re-render → throw promise → Suspense fallback → componentDidMount → ... Fix: defer onStart to a microtask with Promise.resolve().then(), breaking the synchronous cycle. Added unmount guard to prevent setState after unmount. 2. Keeper uses flushSync to set freeze state, which forces synchronous rendering inside createRoot and bypasses React 18's automatic batching, amplifying the infinite loop. Fix: remove flushSync wrapper — the setTimeout already provides the necessary delay. Closes #336, Closes #257 Co-Authored-By: Claude Opus 4 --- src/core/Bridge/Suspense.js | 12 +++++++++++- src/core/Keeper.js | 9 ++++----- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/core/Bridge/Suspense.js b/src/core/Bridge/Suspense.js index 1828fb9..91afeb1 100644 --- a/src/core/Bridge/Suspense.js +++ b/src/core/Bridge/Suspense.js @@ -9,10 +9,20 @@ const Lazy = isSupported ? lazy(() => new Promise(() => null)) : () => null class FallbackListener extends Component { componentDidMount() { - run(this.props, 'onStart') + // Defer onStart to a microtask to break the synchronous render loop + // that occurs with React 18's createRoot. Without this, the cycle + // componentDidMount → setState → re-render → throw promise → Suspense + // fallback → componentDidMount repeats synchronously until React's + // max update depth is exceeded. + Promise.resolve().then(() => { + if (!this._unmounted) { + run(this.props, 'onStart') + } + }) } componentWillUnmount() { + this._unmounted = true run(this.props, 'onEnd') } diff --git a/src/core/Keeper.js b/src/core/Keeper.js index cc7fb6d..fc337cc 100644 --- a/src/core/Keeper.js +++ b/src/core/Keeper.js @@ -1,5 +1,6 @@ import React, { PureComponent, Suspense } from 'react' -import { flushSync } from 'react-dom' +// flushSync removed — it forces synchronous rendering inside createRoot, +// bypassing React 18's automatic batching and causing infinite update loops. import { get, run, nextTick, EventBus } from 'szfe-tools' import ReactFreeze from './Freeze' @@ -103,10 +104,8 @@ export default class Keeper extends PureComponent { // 缓存后,延迟冻结,保证各项后续处理得以进行,如关闭弹窗等 clearTimeout(this.freezeTimeout) this.freezeTimeout = setTimeout(() => { - flushSync(() => { - this.safeSetState({ - freeze: true, - }) + this.safeSetState({ + freeze: true, }) }, 1000) }