Skip to content

Commit aecc90d

Browse files
yujiayu02claude
andcommitted
fix: resolve infinite update loop when using createRoot (React 18)
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 <noreply@anthropic.com>
1 parent 93028bc commit aecc90d

2 files changed

Lines changed: 15 additions & 6 deletions

File tree

src/core/Bridge/Suspense.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,20 @@ const Lazy = isSupported ? lazy(() => new Promise(() => null)) : () => null
99

1010
class FallbackListener extends Component {
1111
componentDidMount() {
12-
run(this.props, 'onStart')
12+
// Defer onStart to a microtask to break the synchronous render loop
13+
// that occurs with React 18's createRoot. Without this, the cycle
14+
// componentDidMount → setState → re-render → throw promise → Suspense
15+
// fallback → componentDidMount repeats synchronously until React's
16+
// max update depth is exceeded.
17+
Promise.resolve().then(() => {
18+
if (!this._unmounted) {
19+
run(this.props, 'onStart')
20+
}
21+
})
1322
}
1423

1524
componentWillUnmount() {
25+
this._unmounted = true
1626
run(this.props, 'onEnd')
1727
}
1828

src/core/Keeper.js

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React, { PureComponent, Suspense } from 'react'
2-
import { flushSync } from 'react-dom'
2+
// flushSync removed — it forces synchronous rendering inside createRoot,
3+
// bypassing React 18's automatic batching and causing infinite update loops.
34
import { get, run, nextTick, EventBus } from 'szfe-tools'
45

56
import ReactFreeze from './Freeze'
@@ -103,10 +104,8 @@ export default class Keeper extends PureComponent {
103104
// 缓存后,延迟冻结,保证各项后续处理得以进行,如关闭弹窗等
104105
clearTimeout(this.freezeTimeout)
105106
this.freezeTimeout = setTimeout(() => {
106-
flushSync(() => {
107-
this.safeSetState({
108-
freeze: true,
109-
})
107+
this.safeSetState({
108+
freeze: true,
110109
})
111110
}, 1000)
112111
}

0 commit comments

Comments
 (0)