diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js
index 996bc72603f56..ac25828400aa6 100644
--- a/packages/react-reconciler/src/ReactFiber.js
+++ b/packages/react-reconciler/src/ReactFiber.js
@@ -24,7 +24,6 @@ import type {ActivityInstance, SuspenseInstance} from './ReactFiberConfig';
import type {
LegacyHiddenProps,
OffscreenProps,
- OffscreenInstance,
} from './ReactFiberOffscreenComponent';
import type {ViewTransitionState} from './ReactFiberViewTransitionComponent';
import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent';
@@ -76,7 +75,6 @@ import {
ViewTransitionComponent,
ActivityComponent,
} from './ReactWorkTags';
-import {OffscreenVisible} from './ReactFiberOffscreenComponent';
import {getComponentNameFromOwner} from 'react-reconciler/src/getComponentNameFromFiber';
import {isDevToolsPresent} from './ReactFiberDevToolsHook';
import {
@@ -831,13 +829,6 @@ export function createFiberFromOffscreen(
): Fiber {
const fiber = createFiber(OffscreenComponent, pendingProps, key, mode);
fiber.lanes = lanes;
- const primaryChildInstance: OffscreenInstance = {
- _visibility: OffscreenVisible,
- _pendingMarkers: null,
- _retryCache: null,
- _transitions: null,
- };
- fiber.stateNode = primaryChildInstance;
return fiber;
}
export function createFiberFromActivity(
@@ -885,15 +876,6 @@ export function createFiberFromLegacyHidden(
const fiber = createFiber(LegacyHiddenComponent, pendingProps, key, mode);
fiber.elementType = REACT_LEGACY_HIDDEN_TYPE;
fiber.lanes = lanes;
- // Adding a stateNode for legacy hidden because it's currently using
- // the offscreen implementation, which depends on a state node
- const instance: OffscreenInstance = {
- _visibility: OffscreenVisible,
- _pendingMarkers: null,
- _transitions: null,
- _retryCache: null,
- };
- fiber.stateNode = instance;
return fiber;
}
diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js
index 7a3bb4ef814cc..372a74f97b205 100644
--- a/packages/react-reconciler/src/ReactFiberBeginWork.js
+++ b/packages/react-reconciler/src/ReactFiberBeginWork.js
@@ -280,6 +280,7 @@ import {
createCapturedValueFromError,
createCapturedValueAtFiber,
} from './ReactCapturedValue';
+import {OffscreenVisible} from './ReactFiberOffscreenComponent';
import {
createClassErrorUpdate,
initializeClassErrorUpdate,
@@ -620,6 +621,18 @@ function updateOffscreenComponent(
const prevState: OffscreenState | null =
current !== null ? current.memoizedState : null;
+ if (current === null && workInProgress.stateNode === null) {
+ // We previously reset the work-in-progress.
+ // We need to create a new Offscreen instance.
+ const primaryChildInstance: OffscreenInstance = {
+ _visibility: OffscreenVisible,
+ _pendingMarkers: null,
+ _retryCache: null,
+ _transitions: null,
+ };
+ workInProgress.stateNode = primaryChildInstance;
+ }
+
if (
nextProps.mode === 'hidden' ||
(enableLegacyHidden && nextProps.mode === 'unstable-defer-without-hiding')
@@ -788,6 +801,26 @@ function updateOffscreenComponent(
return workInProgress.child;
}
+function bailoutOffscreenComponent(
+ current: Fiber | null,
+ workInProgress: Fiber,
+): Fiber | null {
+ if (
+ (current === null || current.tag !== OffscreenComponent) &&
+ workInProgress.stateNode === null
+ ) {
+ const primaryChildInstance: OffscreenInstance = {
+ _visibility: OffscreenVisible,
+ _pendingMarkers: null,
+ _retryCache: null,
+ _transitions: null,
+ };
+ workInProgress.stateNode = primaryChildInstance;
+ }
+
+ return workInProgress.sibling;
+}
+
function deferHiddenOffscreenComponent(
current: Fiber | null,
workInProgress: Fiber,
@@ -1095,9 +1128,13 @@ function updateActivityComponent(
if (nextProps.mode === 'hidden') {
// SSR doesn't render hidden Activity so it shouldn't hydrate,
// even at offscreen lane. Defer to a client rendered offscreen lane.
- mountActivityChildren(workInProgress, nextProps, renderLanes);
+ const primaryChildFragment = mountActivityChildren(
+ workInProgress,
+ nextProps,
+ renderLanes,
+ );
workInProgress.lanes = laneToLanes(OffscreenLane);
- return null;
+ return bailoutOffscreenComponent(null, primaryChildFragment);
} else {
// We must push the suspense handler context *before* attempting to
// hydrate, to avoid a mismatch in case it errors.
@@ -2373,7 +2410,7 @@ function updateSuspenseComponent(
if (showFallback) {
pushFallbackTreeSuspenseHandler(workInProgress);
- const fallbackFragment = mountSuspenseFallbackChildren(
+ mountSuspenseFallbackChildren(
workInProgress,
nextPrimaryChildren,
nextFallbackChildren,
@@ -2408,7 +2445,7 @@ function updateSuspenseComponent(
}
}
- return fallbackFragment;
+ return bailoutOffscreenComponent(null, primaryChildFragment);
} else if (
enableCPUSuspense &&
typeof nextProps.unstable_expectedLoadTime === 'number'
@@ -2417,7 +2454,7 @@ function updateSuspenseComponent(
// unblock the surrounding content. Then immediately retry after the
// initial commit.
pushFallbackTreeSuspenseHandler(workInProgress);
- const fallbackFragment = mountSuspenseFallbackChildren(
+ mountSuspenseFallbackChildren(
workInProgress,
nextPrimaryChildren,
nextFallbackChildren,
@@ -2444,7 +2481,7 @@ function updateSuspenseComponent(
// RetryLane even if it's the one currently rendering since we're leaving
// it behind on this node.
workInProgress.lanes = SomeRetryLane;
- return fallbackFragment;
+ return bailoutOffscreenComponent(null, primaryChildFragment);
} else {
pushPrimaryTreeSuspenseHandler(workInProgress);
return mountSuspensePrimaryChildren(
@@ -2479,7 +2516,7 @@ function updateSuspenseComponent(
const nextFallbackChildren = nextProps.fallback;
const nextPrimaryChildren = nextProps.children;
- const fallbackChildFragment = updateSuspenseFallbackChildren(
+ updateSuspenseFallbackChildren(
current,
workInProgress,
nextPrimaryChildren,
@@ -2532,7 +2569,7 @@ function updateSuspenseComponent(
renderLanes,
);
workInProgress.memoizedState = SUSPENDED_MARKER;
- return fallbackChildFragment;
+ return bailoutOffscreenComponent(current.child, primaryChildFragment);
} else {
if (
prevState !== null &&
@@ -2788,7 +2825,7 @@ function updateSuspenseFallbackChildren(
primaryChildFragment.sibling = fallbackChildFragment;
workInProgress.child = primaryChildFragment;
- return fallbackChildFragment;
+ return bailoutOffscreenComponent(null, primaryChildFragment);
}
function retrySuspenseComponentWithoutHydrating(
@@ -3094,14 +3131,13 @@ function updateDehydratedSuspenseComponent(
const nextPrimaryChildren = nextProps.children;
const nextFallbackChildren = nextProps.fallback;
- const fallbackChildFragment =
- mountSuspenseFallbackAfterRetryWithoutHydrating(
- current,
- workInProgress,
- nextPrimaryChildren,
- nextFallbackChildren,
- renderLanes,
- );
+ mountSuspenseFallbackAfterRetryWithoutHydrating(
+ current,
+ workInProgress,
+ nextPrimaryChildren,
+ nextFallbackChildren,
+ renderLanes,
+ );
const primaryChildFragment: Fiber = (workInProgress.child: any);
primaryChildFragment.memoizedState =
mountSuspenseOffscreenState(renderLanes);
@@ -3111,7 +3147,7 @@ function updateDehydratedSuspenseComponent(
renderLanes,
);
workInProgress.memoizedState = SUSPENDED_MARKER;
- return fallbackChildFragment;
+ return bailoutOffscreenComponent(null, primaryChildFragment);
}
}
}
diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js
index 3637093529f9a..a5c4282e9e785 100644
--- a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js
@@ -4141,4 +4141,28 @@ describe('ReactSuspenseWithNoopRenderer', () => {
>,
);
});
+
+ it('can rerender after resolving a promise', async () => {
+ const promise = Promise.resolve(null);
+ const root = ReactNoop.createRoot();
+
+ await act(() => {
+ startTransition(() => {
+ root.render({promise});
+ });
+ });
+
+ assertLog([]);
+ expect(root).toMatchRenderedOutput(null);
+
+ await act(() => {
+ startTransition(() => {
+ root.render(
+
+
+ ,
+ );
+ });
+ });
+ });
});