Skip to content

Commit c54d8bd

Browse files
committed
fix: retry on Suspense render and preserve hook state
- If a parent rerenders while a child suspends, retry rerendering suspended children. This supports patterns like react-freeze where a never-resolving thenable freezes a subtree and a parent re-render clears it. - Preserve hook state (useState, etc.) across suspend/unsuspend for already-mounted components. Discard hook state for components that suspend during initial mount (matching React's WIP fiber discard). - Run effect cleanups (both useEffect and useLayoutEffect) uniformly during suspension. Effects rerun when unsuspended.
1 parent 2459326 commit c54d8bd

File tree

2 files changed

+279
-123
lines changed

2 files changed

+279
-123
lines changed

compat/src/suspense.js

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,21 @@ options.unmount = function (vnode) {
4242
function detachedClone(vnode, detachedParent, parentDom) {
4343
if (vnode) {
4444
if (vnode._component && vnode._component.__hooks) {
45-
vnode._component.__hooks._list.forEach(effect => {
46-
if (typeof effect._cleanup == 'function') effect._cleanup();
47-
});
48-
49-
vnode._component.__hooks = null;
45+
if (vnode._dom != null) {
46+
// Already-mounted component: preserve hook state but run
47+
// effect cleanups. Effects will rerun when unsuspended.
48+
vnode._component.__hooks._list.forEach(effect => {
49+
if (typeof effect._cleanup == 'function') {
50+
effect._cleanup();
51+
effect._cleanup = undefined;
52+
}
53+
});
54+
} else {
55+
// Component suspended during initial mount (never committed
56+
// to DOM). Discard hook state entirely, matching React's
57+
// behavior of discarding the WIP fiber.
58+
vnode._component.__hooks = null;
59+
}
5060
}
5161

5262
vnode = assign({}, vnode);
@@ -206,7 +216,34 @@ Suspense.prototype.render = function (props, state) {
206216
);
207217
}
208218

219+
// Save reference to this vnode. After diffChildren, its _children
220+
// array becomes the "old children" for future diffs. We need this
221+
// reference to swap in original vnodes when retrying after suspension.
222+
this._suspendedVNode = this._vnode;
209223
this._detachOnNextRender = null;
224+
} else if (state._suspended) {
225+
// Parent re-rendered while suspended (not from _childDidSuspend).
226+
// Try rendering children again. If they re-throw, _childDidSuspend
227+
// will re-catch and re-suspend. This supports patterns like
228+
// react-freeze where a never-resolving thenable is thrown to freeze
229+
// a subtree and later cleared by a parent re-render.
230+
231+
// Restore the original vnode tree into the old children array
232+
// so the diff can reuse existing component instances (preserving
233+
// hook state like useState).
234+
const suspendedVNode = state._suspended;
235+
if (this._suspendedVNode && this._suspendedVNode._children) {
236+
this._suspendedVNode._children[0] = removeOriginal(
237+
suspendedVNode,
238+
suspendedVNode._component._parentDom,
239+
suspendedVNode._component._originalParentDom
240+
);
241+
}
242+
243+
this._suspendedVNode = null;
244+
this._pendingSuspensionCount = 0;
245+
this._suspenders = null;
246+
state._suspended = null;
210247
}
211248

212249
return [

0 commit comments

Comments
 (0)