diff --git a/compat/src/suspense.js b/compat/src/suspense.js
index 8a55b0971e..a962821e53 100644
--- a/compat/src/suspense.js
+++ b/compat/src/suspense.js
@@ -144,6 +144,12 @@ Suspense.prototype._childDidSuspend = function (promise, suspendingVNode) {
suspendingComponent._onResolve = onResolved;
+ // Store and null _parentDom to prevent setState/forceUpdate from
+ // scheduling renders while suspended. Render would be a no-op anyway
+ // since renderComponent checks _parentDom, but this avoids queue churn.
+ const originalParentDom = suspendingComponent._parentDom;
+ suspendingComponent._parentDom = null;
+
const onSuspensionComplete = () => {
if (!--c._pendingSuspensionCount) {
// If the suspension was during hydration we don't need to restore the
@@ -161,6 +167,8 @@ Suspense.prototype._childDidSuspend = function (promise, suspendingVNode) {
let suspended;
while ((suspended = c._suspenders.pop())) {
+ // Restore _parentDom before forceUpdate so render can proceed
+ suspended._parentDom = originalParentDom;
suspended.forceUpdate();
}
}
diff --git a/compat/test/browser/suspense.test.js b/compat/test/browser/suspense.test.js
index 9eaaf326fb..99f8b3bfa1 100644
--- a/compat/test/browser/suspense.test.js
+++ b/compat/test/browser/suspense.test.js
@@ -2543,4 +2543,114 @@ describe('suspense', () => {
`
Memod effect executedeffect executed
`
);
});
+
+ it('should not schedule renders for setState on suspended component', async () => {
+ let suspenderSetState;
+ let renderCount = 0;
+
+ class Suspender extends Component {
+ constructor(props) {
+ super(props);
+ this.state = { count: 0 };
+ suspenderSetState = this.setState.bind(this);
+ }
+
+ render(props, state) {
+ renderCount++;
+ if (props.suspend && !props.resolved) {
+ throw props.promise;
+ }
+ return Count: {state.count}
;
+ }
+ }
+
+ let resolve;
+ const promise = new Promise(r => {
+ resolve = r;
+ });
+
+ act(() => {
+ render(
+ Loading...}>
+
+ ,
+ scratch
+ );
+ });
+
+ rerender();
+
+ expect(scratch.innerHTML).to.equal('Loading...
');
+ const renderCountAfterSuspend = renderCount;
+
+ // Call setState on the suspended component multiple times
+ suspenderSetState({ count: 1 });
+ suspenderSetState({ count: 2 });
+ suspenderSetState({ count: 3 });
+ rerender();
+
+ // Render count should not have increased - setState should not trigger re-renders while suspended
+ expect(renderCount).to.equal(renderCountAfterSuspend);
+ expect(scratch.innerHTML).to.equal('Loading...
');
+
+ // Resolve the suspension
+ resolve();
+ await promise;
+
+ render(
+ Loading...}>
+
+ ,
+ scratch
+ );
+ rerender();
+
+ // After resolving, the state should have been buffered and applied
+ expect(scratch.innerHTML).to.equal('Count: 3
');
+ });
+
+ it('should not schedule renders for forceUpdate on suspended component', () => {
+ let suspenderForceUpdate;
+ let renderCount = 0;
+
+ class Suspender extends Component {
+ constructor(props) {
+ super(props);
+ suspenderForceUpdate = this.forceUpdate.bind(this);
+ }
+
+ render(props) {
+ renderCount++;
+ if (props.suspend && !props.resolved) {
+ throw props.promise;
+ }
+ return Rendered {renderCount} times
;
+ }
+ }
+
+ const promise = new Promise(() => {});
+
+ act(() => {
+ render(
+ Loading...}>
+
+ ,
+ scratch
+ );
+ });
+
+ rerender();
+
+ expect(scratch.innerHTML).to.equal('Loading...
');
+ const renderCountAfterSuspend = renderCount;
+
+ // Call forceUpdate on the suspended component
+ suspenderForceUpdate();
+ suspenderForceUpdate();
+ rerender();
+
+ // Render count should not have increased
+ expect(renderCount).to.equal(renderCountAfterSuspend);
+ expect(scratch.innerHTML).to.equal('Loading...
');
+ });
});