diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
index 97642ad45a09f..1271e091fe859 100644
--- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
+++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
@@ -90,6 +90,77 @@ describe('ReactDOMServerPartialHydration', () => {
expect(ref.current).toBe(span);
});
+ it('warns and replaces the boundary content in legacy mode', async () => {
+ let suspend = false;
+ let resolve;
+ let promise = new Promise(resolvePromise => (resolve = resolvePromise));
+ let ref = React.createRef();
+
+ function Child() {
+ if (suspend) {
+ throw promise;
+ } else {
+ return 'Hello';
+ }
+ }
+
+ function App() {
+ return (
+
+
+
+
+
+
+
+ );
+ }
+
+ // Don't suspend on the server.
+ suspend = false;
+ let finalHTML = ReactDOMServer.renderToString();
+
+ let container = document.createElement('div');
+ container.innerHTML = finalHTML;
+
+ let span = container.getElementsByTagName('span')[0];
+
+ // On the client we try to hydrate.
+ suspend = true;
+ expect(() => {
+ act(() => {
+ ReactDOM.hydrate(, container);
+ });
+ }).toWarnDev(
+ 'Warning: Cannot hydrate Suspense in legacy mode. Switch from ' +
+ 'ReactDOM.hydrate(element, container) to ' +
+ 'ReactDOM.unstable_createSyncRoot(container, { hydrate: true })' +
+ '.render(element) or remove the Suspense components from the server ' +
+ 'rendered components.' +
+ '\n in Suspense (at **)' +
+ '\n in div (at **)' +
+ '\n in App (at **)',
+ );
+
+ // We're now in loading state.
+ expect(container.textContent).toBe('Loading...');
+
+ let span2 = container.getElementsByTagName('span')[0];
+ // This is a new node.
+ expect(span).not.toBe(span2);
+ expect(ref.current).toBe(span2);
+
+ // Resolving the promise should render the final content.
+ suspend = false;
+ resolve();
+ await promise;
+ Scheduler.unstable_flushAll();
+ jest.runAllTimers();
+
+ // We should now have hydrated with a ref on the existing span.
+ expect(container.textContent).toBe('Hello');
+ });
+
it('can insert siblings before the dehydrated boundary', () => {
let suspend = false;
let promise = new Promise(() => {});
@@ -135,7 +206,8 @@ describe('ReactDOMServerPartialHydration', () => {
suspend = true;
act(() => {
- ReactDOM.hydrate(, container);
+ let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
+ root.render();
});
expect(container.firstChild.firstChild.tagName).not.toBe('DIV');
@@ -191,7 +263,8 @@ describe('ReactDOMServerPartialHydration', () => {
// hydrating anyway.
suspend = true;
act(() => {
- ReactDOM.hydrate(, container);
+ let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
+ root.render();
});
expect(container.firstChild.children[1].textContent).toBe('Middle');
diff --git a/packages/react-dom/src/__tests__/ReactDOMServerSuspense-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerSuspense-test.internal.js
index 434fc6c8a507f..b5ecccca7e9df 100644
--- a/packages/react-dom/src/__tests__/ReactDOMServerSuspense-test.internal.js
+++ b/packages/react-dom/src/__tests__/ReactDOMServerSuspense-test.internal.js
@@ -37,7 +37,7 @@ function initModules() {
};
}
-const {resetModules, serverRender, itRenders} = ReactDOMServerIntegrationUtils(
+const {resetModules, serverRender} = ReactDOMServerIntegrationUtils(
initModules,
);
@@ -102,8 +102,8 @@ describe('ReactDOMServerSuspense', () => {
);
});
- itRenders('a SuspenseList component and its children', async render => {
- const element = await render(
+ it('server renders a SuspenseList component and its children', async () => {
+ const example = (
A
@@ -111,8 +111,9 @@ describe('ReactDOMServerSuspense', () => {
B
- ,
+
);
+ const element = await serverRender(example);
const parent = element.parentNode;
const divA = parent.children[0];
expect(divA.tagName).toBe('DIV');
@@ -120,5 +121,16 @@ describe('ReactDOMServerSuspense', () => {
const divB = parent.children[1];
expect(divB.tagName).toBe('DIV');
expect(divB.textContent).toBe('B');
+
+ ReactTestUtils.act(() => {
+ const root = ReactDOM.unstable_createSyncRoot(parent, {hydrate: true});
+ root.render(example);
+ });
+
+ const parent2 = element.parentNode;
+ const divA2 = parent2.children[0];
+ const divB2 = parent2.children[1];
+ expect(divA).toBe(divA2);
+ expect(divB).toBe(divB2);
});
});
diff --git a/packages/react-reconciler/src/ReactDebugFiberPerf.js b/packages/react-reconciler/src/ReactDebugFiberPerf.js
index 6abf5df279874..c08276945c2ec 100644
--- a/packages/react-reconciler/src/ReactDebugFiberPerf.js
+++ b/packages/react-reconciler/src/ReactDebugFiberPerf.js
@@ -21,7 +21,6 @@ import {
ContextConsumer,
Mode,
SuspenseComponent,
- DehydratedSuspenseComponent,
} from 'shared/ReactWorkTags';
type MeasurementPhase =
@@ -317,8 +316,7 @@ export function stopFailedWorkTimer(fiber: Fiber): void {
}
fiber._debugIsCurrentlyTiming = false;
const warning =
- fiber.tag === SuspenseComponent ||
- fiber.tag === DehydratedSuspenseComponent
+ fiber.tag === SuspenseComponent
? 'Rendering was suspended'
: 'An error was thrown inside this error boundary';
endFiberMark(fiber, null, warning);
diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js
index 0463f0a9e889b..92b1455c1b329 100644
--- a/packages/react-reconciler/src/ReactFiber.js
+++ b/packages/react-reconciler/src/ReactFiber.js
@@ -24,6 +24,7 @@ import type {ExpirationTime} from './ReactFiberExpirationTime';
import type {UpdateQueue} from './ReactUpdateQueue';
import type {ContextDependency} from './ReactFiberNewContext';
import type {HookType} from './ReactFiberHooks';
+import type {SuspenseInstance} from './ReactFiberHostConfig';
import invariant from 'shared/invariant';
import warningWithoutStack from 'shared/warningWithoutStack';
@@ -48,6 +49,7 @@ import {
Profiler,
SuspenseComponent,
SuspenseListComponent,
+ DehydratedFragment,
FunctionComponent,
MemoComponent,
SimpleMemoComponent,
@@ -843,6 +845,14 @@ export function createFiberFromHostInstanceForDeletion(): Fiber {
return fiber;
}
+export function createFiberFromDehydratedFragment(
+ dehydratedNode: SuspenseInstance,
+): Fiber {
+ const fiber = createFiber(DehydratedFragment, null, null, NoMode);
+ fiber.stateNode = dehydratedNode;
+ return fiber;
+}
+
export function createFiberFromPortal(
portal: ReactPortal,
mode: TypeOfMode,
diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js
index 88fdf4c1ac417..e05fcfba34cbf 100644
--- a/packages/react-reconciler/src/ReactFiberBeginWork.js
+++ b/packages/react-reconciler/src/ReactFiberBeginWork.js
@@ -36,7 +36,6 @@ import {
Profiler,
SuspenseComponent,
SuspenseListComponent,
- DehydratedSuspenseComponent,
MemoComponent,
SimpleMemoComponent,
LazyComponent,
@@ -93,6 +92,7 @@ import {processUpdateQueue} from './ReactUpdateQueue';
import {
NoWork,
Never,
+ Sync,
computeAsyncExpiration,
} from './ReactFiberExpirationTime';
import {
@@ -115,7 +115,6 @@ import {pushHostContext, pushHostContainer} from './ReactFiberHostContext';
import {
suspenseStackCursor,
pushSuspenseContext,
- popSuspenseContext,
InvisibleParentSuspenseContext,
ForceSuspenseFallback,
hasSuspenseContext,
@@ -1475,8 +1474,9 @@ function validateFunctionComponentInDev(workInProgress: Fiber, Component: any) {
}
}
-// TODO: This is now an empty object. Should we just make it a boolean?
-const SUSPENDED_MARKER: SuspenseState = ({}: any);
+const SUSPENDED_MARKER: SuspenseState = {
+ dehydrated: null,
+};
function shouldRemainOnFallback(
suspenseContext: SuspenseContext,
@@ -1511,21 +1511,23 @@ function updateSuspenseComponent(
let suspenseContext: SuspenseContext = suspenseStackCursor.current;
- let nextState = null;
let nextDidTimeout = false;
+ const didSuspend = (workInProgress.effectTag & DidCapture) !== NoEffect;
if (
- (workInProgress.effectTag & DidCapture) !== NoEffect ||
+ didSuspend ||
shouldRemainOnFallback(suspenseContext, current, workInProgress)
) {
// Something in this boundary's subtree already suspended. Switch to
// rendering the fallback children.
- nextState = SUSPENDED_MARKER;
nextDidTimeout = true;
workInProgress.effectTag &= ~DidCapture;
} else {
// Attempting the main content
- if (current === null || current.memoizedState !== null) {
+ if (
+ current === null ||
+ (current.memoizedState: null | SuspenseState) !== null
+ ) {
// This is a new mount or this boundary is already showing a fallback state.
// Mark this subtree context as having at least one invisible parent that could
// handle the fallback state.
@@ -1582,29 +1584,24 @@ function updateSuspenseComponent(
// custom reconciliation logic to preserve the state of the primary
// children. It's essentially a very basic form of re-parenting.
- // `child` points to the child fiber. In the normal case, this is the first
- // fiber of the primary children set. In the timed-out case, it's a
- // a fragment fiber containing the primary children.
- let child;
- // `next` points to the next fiber React should render. In the normal case,
- // it's the same as `child`: the first fiber of the primary children set.
- // In the timed-out case, it's a fragment fiber containing the *fallback*
- // children -- we skip over the primary children entirely.
- let next;
if (current === null) {
if (enableSuspenseServerRenderer) {
// If we're currently hydrating, try to hydrate this boundary.
// But only if this has a fallback.
if (nextProps.fallback !== undefined) {
tryToClaimNextHydratableInstance(workInProgress);
- // This could've changed the tag if this was a dehydrated suspense component.
- if (workInProgress.tag === DehydratedSuspenseComponent) {
- popSuspenseContext(workInProgress);
- return updateDehydratedSuspenseComponent(
- null,
- workInProgress,
- renderExpirationTime,
- );
+ // This could've been a dehydrated suspense component.
+ const suspenseState: null | SuspenseState =
+ workInProgress.memoizedState;
+ if (suspenseState !== null) {
+ const dehydrated = suspenseState.dehydrated;
+ if (dehydrated !== null) {
+ return mountDehydratedSuspenseComponent(
+ workInProgress,
+ dehydrated,
+ renderExpirationTime,
+ );
+ }
}
}
}
@@ -1646,26 +1643,121 @@ function updateSuspenseComponent(
);
fallbackChildFragment.return = workInProgress;
primaryChildFragment.sibling = fallbackChildFragment;
- child = primaryChildFragment;
// Skip the primary children, and continue working on the
// fallback children.
- next = fallbackChildFragment;
+ workInProgress.memoizedState = SUSPENDED_MARKER;
+ workInProgress.child = primaryChildFragment;
+ return fallbackChildFragment;
} else {
// Mount the primary children without an intermediate fragment fiber.
const nextPrimaryChildren = nextProps.children;
- child = next = mountChildFibers(
+ workInProgress.memoizedState = null;
+ return (workInProgress.child = mountChildFibers(
workInProgress,
null,
nextPrimaryChildren,
renderExpirationTime,
- );
+ ));
}
} else {
// This is an update. This branch is more complicated because we need to
// ensure the state of the primary children is preserved.
- const prevState = current.memoizedState;
- const prevDidTimeout = prevState !== null;
- if (prevDidTimeout) {
+ const prevState: null | SuspenseState = current.memoizedState;
+ if (prevState !== null) {
+ if (enableSuspenseServerRenderer) {
+ const dehydrated = prevState.dehydrated;
+ if (dehydrated !== null) {
+ if (!didSuspend) {
+ return updateDehydratedSuspenseComponent(
+ current,
+ workInProgress,
+ dehydrated,
+ renderExpirationTime,
+ );
+ } else if (
+ (workInProgress.memoizedState: null | SuspenseState) !== null
+ ) {
+ // Something suspended and we should still be in dehydrated mode.
+ // Leave the existing child in place.
+ workInProgress.child = current.child;
+ // The dehydrated completion pass expects this flag to be there
+ // but the normal suspense pass doesn't.
+ workInProgress.effectTag |= DidCapture;
+ return null;
+ } else {
+ // Suspended but we should no longer be in dehydrated mode.
+ // Therefore we now have to render the fallback. Wrap the children
+ // in a fragment fiber to keep them separate from the fallback
+ // children.
+ const nextFallbackChildren = nextProps.fallback;
+ const primaryChildFragment = createFiberFromFragment(
+ // It shouldn't matter what the pending props are because we aren't
+ // going to render this fragment.
+ null,
+ mode,
+ NoWork,
+ null,
+ );
+ primaryChildFragment.return = workInProgress;
+
+ // This is always null since we never want the previous child
+ // that we're not going to hydrate.
+ primaryChildFragment.child = null;
+
+ if ((workInProgress.mode & BatchedMode) === NoMode) {
+ // Outside of batched mode, we commit the effects from the
+ // partially completed, timed-out tree, too.
+ let progressedChild = (primaryChildFragment.child =
+ workInProgress.child);
+ while (progressedChild !== null) {
+ progressedChild.return = primaryChildFragment;
+ progressedChild = progressedChild.sibling;
+ }
+ } else {
+ // We will have dropped the effect list which contains the deletion.
+ // We need to reconcile to delete the current child.
+ reconcileChildFibers(
+ workInProgress,
+ current.child,
+ null,
+ renderExpirationTime,
+ );
+ }
+
+ // Because primaryChildFragment is a new fiber that we're inserting as the
+ // parent of a new tree, we need to set its treeBaseDuration.
+ if (enableProfilerTimer && workInProgress.mode & ProfileMode) {
+ // treeBaseDuration is the sum of all the child tree base durations.
+ let treeBaseDuration = 0;
+ let hiddenChild = primaryChildFragment.child;
+ while (hiddenChild !== null) {
+ treeBaseDuration += hiddenChild.treeBaseDuration;
+ hiddenChild = hiddenChild.sibling;
+ }
+ primaryChildFragment.treeBaseDuration = treeBaseDuration;
+ }
+
+ // Create a fragment from the fallback children, too.
+ const fallbackChildFragment = createFiberFromFragment(
+ nextFallbackChildren,
+ mode,
+ renderExpirationTime,
+ null,
+ );
+ fallbackChildFragment.return = workInProgress;
+ primaryChildFragment.sibling = fallbackChildFragment;
+ fallbackChildFragment.effectTag |= Placement;
+ primaryChildFragment.childExpirationTime = NoWork;
+
+ workInProgress.memoizedState = SUSPENDED_MARKER;
+ workInProgress.child = primaryChildFragment;
+
+ // Skip the primary children, and continue working on the
+ // fallback children.
+ return fallbackChildFragment;
+ }
+ }
+ }
// The current tree already timed out. That means each child set is
// wrapped in a fragment fiber.
const currentPrimaryChildFragment: Fiber = (current.child: any);
@@ -1721,11 +1813,12 @@ function updateSuspenseComponent(
);
fallbackChildFragment.return = workInProgress;
primaryChildFragment.sibling = fallbackChildFragment;
- child = primaryChildFragment;
primaryChildFragment.childExpirationTime = NoWork;
// Skip the primary children, and continue working on the
// fallback children.
- next = fallbackChildFragment;
+ workInProgress.memoizedState = SUSPENDED_MARKER;
+ workInProgress.child = primaryChildFragment;
+ return fallbackChildFragment;
} else {
// No longer suspended. Switch back to showing the primary children,
// and remove the intermediate fragment fiber.
@@ -1745,7 +1838,8 @@ function updateSuspenseComponent(
// the stateNode?
// Continue rendering the children, like we normally do.
- child = next = primaryChild;
+ workInProgress.memoizedState = null;
+ return (workInProgress.child = primaryChild);
}
} else {
// The current tree has not already timed out. That means the primary
@@ -1813,29 +1907,26 @@ function updateSuspenseComponent(
fallbackChildFragment.return = workInProgress;
primaryChildFragment.sibling = fallbackChildFragment;
fallbackChildFragment.effectTag |= Placement;
- child = primaryChildFragment;
primaryChildFragment.childExpirationTime = NoWork;
// Skip the primary children, and continue working on the
// fallback children.
- next = fallbackChildFragment;
+ workInProgress.memoizedState = SUSPENDED_MARKER;
+ workInProgress.child = primaryChildFragment;
+ return fallbackChildFragment;
} else {
// Still haven't timed out. Continue rendering the children, like we
// normally do.
+ workInProgress.memoizedState = null;
const nextPrimaryChildren = nextProps.children;
- next = child = reconcileChildFibers(
+ return (workInProgress.child = reconcileChildFibers(
workInProgress,
currentPrimaryChild,
nextPrimaryChildren,
renderExpirationTime,
- );
+ ));
}
}
- workInProgress.stateNode = current.stateNode;
}
-
- workInProgress.memoizedState = nextState;
- workInProgress.child = child;
- return next;
}
function retrySuspenseComponentWithoutHydrating(
@@ -1843,94 +1934,90 @@ function retrySuspenseComponentWithoutHydrating(
workInProgress: Fiber,
renderExpirationTime: ExpirationTime,
) {
- // Detach from the current dehydrated boundary.
- current.alternate = null;
- workInProgress.alternate = null;
-
- // Insert a deletion in the effect list.
- let returnFiber = workInProgress.return;
- invariant(
- returnFiber !== null,
- 'Suspense boundaries are never on the root. ' +
- 'This is probably a bug in React.',
- );
- const last = returnFiber.lastEffect;
- if (last !== null) {
- last.nextEffect = current;
- returnFiber.lastEffect = current;
- } else {
- returnFiber.firstEffect = returnFiber.lastEffect = current;
- }
- current.nextEffect = null;
- current.effectTag = Deletion;
-
- popSuspenseContext(workInProgress);
-
- // Upgrade this work in progress to a real Suspense component.
- workInProgress.tag = SuspenseComponent;
- workInProgress.stateNode = null;
+ // We're now not suspended nor dehydrated.
workInProgress.memoizedState = null;
- // This is now an insertion.
- workInProgress.effectTag |= Placement;
- // Retry as a real Suspense component.
- return updateSuspenseComponent(null, workInProgress, renderExpirationTime);
+ // Retry with the full children.
+ const nextProps = workInProgress.pendingProps;
+ const nextChildren = nextProps.children;
+ // This will ensure that the children get Placement effects and
+ // that the old child gets a Deletion effect.
+ // We could also call forceUnmountCurrentAndReconcile.
+ reconcileChildren(
+ current,
+ workInProgress,
+ nextChildren,
+ renderExpirationTime,
+ );
+ return workInProgress.child;
}
-function updateDehydratedSuspenseComponent(
- current: Fiber | null,
+function mountDehydratedSuspenseComponent(
workInProgress: Fiber,
+ suspenseInstance: SuspenseInstance,
renderExpirationTime: ExpirationTime,
-) {
- pushSuspenseContext(
- workInProgress,
- setDefaultShallowSuspenseContext(suspenseStackCursor.current),
- );
- const suspenseInstance = (workInProgress.stateNode: SuspenseInstance);
- if (current === null) {
- // During the first pass, we'll bail out and not drill into the children.
- // Instead, we'll leave the content in place and try to hydrate it later.
- if (isSuspenseInstanceFallback(suspenseInstance)) {
- // This is a client-only boundary. Since we won't get any content from the server
- // for this, we need to schedule that at a higher priority based on when it would
- // have timed out. In theory we could render it in this pass but it would have the
- // wrong priority associated with it and will prevent hydration of parent path.
- // Instead, we'll leave work left on it to render it in a separate commit.
-
- // TODO This time should be the time at which the server rendered response that is
- // a parent to this boundary was displayed. However, since we currently don't have
- // a protocol to transfer that time, we'll just estimate it by using the current
- // time. This will mean that Suspense timeouts are slightly shifted to later than
- // they should be.
- let serverDisplayTime = requestCurrentTime();
- // Schedule a normal pri update to render this content.
- let newExpirationTime = computeAsyncExpiration(serverDisplayTime);
- if (enableSchedulerTracing) {
- markSpawnedWork(newExpirationTime);
- }
- workInProgress.expirationTime = newExpirationTime;
- } else {
- // We'll continue hydrating the rest at offscreen priority since we'll already
- // be showing the right content coming from the server, it is no rush.
- workInProgress.expirationTime = Never;
- if (enableSchedulerTracing) {
- markSpawnedWork(Never);
- }
+): null | Fiber {
+ // During the first pass, we'll bail out and not drill into the children.
+ // Instead, we'll leave the content in place and try to hydrate it later.
+ if ((workInProgress.mode & BatchedMode) === NoMode) {
+ if (__DEV__) {
+ warning(
+ false,
+ 'Cannot hydrate Suspense in legacy mode. Switch from ' +
+ 'ReactDOM.hydrate(element, container) to ' +
+ 'ReactDOM.unstable_createSyncRoot(container, { hydrate: true })' +
+ '.render(element) or remove the Suspense components from ' +
+ 'the server rendered components.',
+ );
+ }
+ workInProgress.expirationTime = Sync;
+ } else if (isSuspenseInstanceFallback(suspenseInstance)) {
+ // This is a client-only boundary. Since we won't get any content from the server
+ // for this, we need to schedule that at a higher priority based on when it would
+ // have timed out. In theory we could render it in this pass but it would have the
+ // wrong priority associated with it and will prevent hydration of parent path.
+ // Instead, we'll leave work left on it to render it in a separate commit.
+
+ // TODO This time should be the time at which the server rendered response that is
+ // a parent to this boundary was displayed. However, since we currently don't have
+ // a protocol to transfer that time, we'll just estimate it by using the current
+ // time. This will mean that Suspense timeouts are slightly shifted to later than
+ // they should be.
+ let serverDisplayTime = requestCurrentTime();
+ // Schedule a normal pri update to render this content.
+ let newExpirationTime = computeAsyncExpiration(serverDisplayTime);
+ if (enableSchedulerTracing) {
+ markSpawnedWork(newExpirationTime);
+ }
+ workInProgress.expirationTime = newExpirationTime;
+ } else {
+ // We'll continue hydrating the rest at offscreen priority since we'll already
+ // be showing the right content coming from the server, it is no rush.
+ workInProgress.expirationTime = Never;
+ if (enableSchedulerTracing) {
+ markSpawnedWork(Never);
}
-
- return null;
- }
-
- if ((workInProgress.effectTag & DidCapture) !== NoEffect) {
- // Something suspended. Leave the existing children in place.
- // TODO: In non-concurrent mode, should we commit the nodes we have hydrated so far?
- workInProgress.child = null;
- return null;
}
+ return null;
+}
+function updateDehydratedSuspenseComponent(
+ current: Fiber,
+ workInProgress: Fiber,
+ suspenseInstance: SuspenseInstance,
+ renderExpirationTime: ExpirationTime,
+): null | Fiber {
// We should never be hydrating at this point because it is the first pass,
// but after we've already committed once.
warnIfHydrating();
+ if ((workInProgress.mode & BatchedMode) === NoMode) {
+ return retrySuspenseComponentWithoutHydrating(
+ current,
+ workInProgress,
+ renderExpirationTime,
+ );
+ }
+
if (isSuspenseInstanceFallback(suspenseInstance)) {
// This boundary is in a permanent fallback state. In this case, we'll never
// get an update and we'll never be able to hydrate the final content. Let's just try the
@@ -1967,8 +2054,8 @@ function updateDehydratedSuspenseComponent(
// these should update this boundary to the permanent Fallback state instead.
// Mark it as having captured (i.e. suspended).
workInProgress.effectTag |= DidCapture;
- // Leave the children in place. I.e. empty.
- workInProgress.child = null;
+ // Leave the child in place. I.e. the dehydrated fragment.
+ workInProgress.child = current.child;
// Register a callback to retry this boundary once the server has sent the result.
registerSuspenseInstanceRetry(
suspenseInstance,
@@ -1977,7 +2064,10 @@ function updateDehydratedSuspenseComponent(
return null;
} else {
// This is the first attempt.
- reenterHydrationStateFromDehydratedSuspenseInstance(workInProgress);
+ reenterHydrationStateFromDehydratedSuspenseInstance(
+ workInProgress,
+ suspenseInstance,
+ );
const nextProps = workInProgress.pendingProps;
const nextChildren = nextProps.children;
workInProgress.child = mountChildFibers(
@@ -2732,8 +2822,21 @@ function beginWork(
break;
case SuspenseComponent: {
const state: SuspenseState | null = workInProgress.memoizedState;
- const didTimeout = state !== null;
- if (didTimeout) {
+ if (state !== null) {
+ if (enableSuspenseServerRenderer) {
+ if (state.dehydrated !== null) {
+ pushSuspenseContext(
+ workInProgress,
+ setDefaultShallowSuspenseContext(suspenseStackCursor.current),
+ );
+ // We know that this component will suspend again because if it has
+ // been unsuspended it has committed as a resolved Suspense component.
+ // If it needs to be retried, it should have work scheduled on it.
+ workInProgress.effectTag |= DidCapture;
+ break;
+ }
+ }
+
// If this boundary is currently timed out, we need to decide
// whether to retry the primary children, or to skip over it and
// go straight to the fallback. Check the priority of the primary
@@ -2780,19 +2883,6 @@ function beginWork(
}
break;
}
- case DehydratedSuspenseComponent: {
- if (enableSuspenseServerRenderer) {
- pushSuspenseContext(
- workInProgress,
- setDefaultShallowSuspenseContext(suspenseStackCursor.current),
- );
- // We know that this component will suspend again because if it has
- // been unsuspended it has committed as a regular Suspense component.
- // If it needs to be retried, it should have work scheduled on it.
- workInProgress.effectTag |= DidCapture;
- }
- break;
- }
case SuspenseListComponent: {
const didSuspendBefore =
(current.effectTag & DidCapture) !== NoEffect;
@@ -3010,16 +3100,6 @@ function beginWork(
renderExpirationTime,
);
}
- case DehydratedSuspenseComponent: {
- if (enableSuspenseServerRenderer) {
- return updateDehydratedSuspenseComponent(
- current,
- workInProgress,
- renderExpirationTime,
- );
- }
- break;
- }
case SuspenseListComponent: {
return updateSuspenseListComponent(
current,
diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js
index e75ed5488e33b..3a6ff1f5aca05 100644
--- a/packages/react-reconciler/src/ReactFiberCommitWork.js
+++ b/packages/react-reconciler/src/ReactFiberCommitWork.js
@@ -43,7 +43,7 @@ import {
HostPortal,
Profiler,
SuspenseComponent,
- DehydratedSuspenseComponent,
+ DehydratedFragment,
IncompleteClassComponent,
MemoComponent,
SimpleMemoComponent,
@@ -967,7 +967,7 @@ function getHostSibling(fiber: Fiber): ?Instance {
while (
node.tag !== HostComponent &&
node.tag !== HostText &&
- node.tag !== DehydratedSuspenseComponent
+ node.tag !== DehydratedFragment
) {
// If it is not host node and, we might have a host node inside it.
// Try to search down until we find one.
@@ -1162,7 +1162,7 @@ function unmountHostComponents(current, renderPriorityLevel): void {
}
} else if (
enableSuspenseServerRenderer &&
- node.tag === DehydratedSuspenseComponent
+ node.tag === DehydratedFragment
) {
// Delete the dehydrated suspense boundary and all of its content.
if (currentParentIsContainer) {
diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js
index 4ca858158b4d8..6433a193ea69b 100644
--- a/packages/react-reconciler/src/ReactFiberCompleteWork.js
+++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js
@@ -48,7 +48,6 @@ import {
Profiler,
SuspenseComponent,
SuspenseListComponent,
- DehydratedSuspenseComponent,
MemoComponent,
SimpleMemoComponent,
LazyComponent,
@@ -115,7 +114,6 @@ import {popProvider} from './ReactFiberNewContext';
import {
prepareToHydrateHostInstance,
prepareToHydrateHostTextInstance,
- skipPastDehydratedSuspenseInstance,
popHydrationState,
resetHydrationState,
} from './ReactFiberHydrationContext';
@@ -838,6 +836,38 @@ function completeWork(
case SuspenseComponent: {
popSuspenseContext(workInProgress);
const nextState: null | SuspenseState = workInProgress.memoizedState;
+
+ if (enableSuspenseServerRenderer) {
+ if (nextState !== null && nextState.dehydrated !== null) {
+ if (current === null) {
+ let wasHydrated = popHydrationState(workInProgress);
+ invariant(
+ wasHydrated,
+ 'A dehydrated suspense component was completed without a hydrated node. ' +
+ 'This is probably a bug in React.',
+ );
+ if (enableSchedulerTracing) {
+ markSpawnedWork(Never);
+ }
+ return null;
+ } else {
+ // We should never have been in a hydration state if we didn't have a current.
+ // However, in some of those paths, we might have reentered a hydration state
+ // and then we might be inside a hydration state. In that case, we'll need to
+ // exit out of it.
+ resetHydrationState();
+ if ((workInProgress.effectTag & DidCapture) === NoEffect) {
+ // This boundary did not suspend so it's now hydrated and unsuspended.
+ workInProgress.memoizedState = null;
+ } else {
+ // Something suspended. Schedule an effect to attach retry listeners.
+ workInProgress.effectTag |= Update;
+ }
+ return null;
+ }
+ }
+ }
+
if ((workInProgress.effectTag & DidCapture) !== NoEffect) {
// Something suspended. Re-render with the fallback children.
workInProgress.expirationTime = renderExpirationTime;
@@ -849,8 +879,8 @@ function completeWork(
let prevDidTimeout = false;
if (current === null) {
// In cases where we didn't find a suitable hydration boundary we never
- // downgraded this to a DehydratedSuspenseComponent, but we still need to
- // pop the hydration state since we might be inside the insertion tree.
+ // put this in dehydrated mode, but we still need to pop the hydration
+ // state since we might be inside the insertion tree.
popHydrationState(workInProgress);
} else {
const prevState: null | SuspenseState = current.memoizedState;
@@ -969,37 +999,6 @@ function completeWork(
}
break;
}
- case DehydratedSuspenseComponent: {
- if (enableSuspenseServerRenderer) {
- popSuspenseContext(workInProgress);
- if (current === null) {
- let wasHydrated = popHydrationState(workInProgress);
- invariant(
- wasHydrated,
- 'A dehydrated suspense component was completed without a hydrated node. ' +
- 'This is probably a bug in React.',
- );
- skipPastDehydratedSuspenseInstance(workInProgress);
- } else {
- // We should never have been in a hydration state if we didn't have a current.
- // However, in some of those paths, we might have reentered a hydration state
- // and then we might be inside a hydration state. In that case, we'll need to
- // exit out of it.
- resetHydrationState();
- if ((workInProgress.effectTag & DidCapture) === NoEffect) {
- // This boundary did not suspend so it's now hydrated.
- // To handle any future suspense cases, we're going to now upgrade it
- // to a Suspense component. We detach it from the existing current fiber.
- current.alternate = null;
- workInProgress.alternate = null;
- workInProgress.tag = SuspenseComponent;
- workInProgress.memoizedState = null;
- workInProgress.stateNode = null;
- }
- }
- }
- break;
- }
case SuspenseListComponent: {
popSuspenseContext(workInProgress);
diff --git a/packages/react-reconciler/src/ReactFiberHydrationContext.js b/packages/react-reconciler/src/ReactFiberHydrationContext.js
index 153503f0441c0..4b9cdf73d56b1 100644
--- a/packages/react-reconciler/src/ReactFiberHydrationContext.js
+++ b/packages/react-reconciler/src/ReactFiberHydrationContext.js
@@ -16,18 +16,21 @@ import type {
Container,
HostContext,
} from './ReactFiberHostConfig';
+import type {SuspenseState} from './ReactFiberSuspenseComponent';
import {
HostComponent,
HostText,
HostRoot,
SuspenseComponent,
- DehydratedSuspenseComponent,
} from 'shared/ReactWorkTags';
import {Deletion, Placement} from 'shared/ReactSideEffectTags';
import invariant from 'shared/invariant';
-import {createFiberFromHostInstanceForDeletion} from './ReactFiber';
+import {
+ createFiberFromHostInstanceForDeletion,
+ createFiberFromDehydratedFragment,
+} from './ReactFiber';
import {
shouldSetTextContent,
supportsHydration,
@@ -82,12 +85,11 @@ function enterHydrationState(fiber: Fiber): boolean {
function reenterHydrationStateFromDehydratedSuspenseInstance(
fiber: Fiber,
+ suspenseInstance: SuspenseInstance,
): boolean {
if (!supportsHydration) {
return false;
}
-
- const suspenseInstance = fiber.stateNode;
nextHydratableInstance = getNextHydratableSibling(suspenseInstance);
popToNextHostParent(fiber);
isHydrating = true;
@@ -221,11 +223,23 @@ function tryHydrate(fiber, nextInstance) {
}
case SuspenseComponent: {
if (enableSuspenseServerRenderer) {
- const suspenseInstance = canHydrateSuspenseInstance(nextInstance);
+ const suspenseInstance: null | SuspenseInstance = canHydrateSuspenseInstance(
+ nextInstance,
+ );
if (suspenseInstance !== null) {
- // Downgrade the tag to a dehydrated component until we've hydrated it.
- fiber.tag = DehydratedSuspenseComponent;
- fiber.stateNode = (suspenseInstance: SuspenseInstance);
+ const suspenseState: SuspenseState = {
+ dehydrated: suspenseInstance,
+ };
+ fiber.memoizedState = suspenseState;
+ // Store the dehydrated fragment as a child fiber.
+ // This simplifies the code for getHostSibling and deleting nodes,
+ // since it doesn't have to consider all Suspense boundaries and
+ // check if they're dehydrated ones or not.
+ const dehydratedFragment = createFiberFromDehydratedFragment(
+ suspenseInstance,
+ );
+ dehydratedFragment.return = fiber;
+ fiber.child = dehydratedFragment;
return true;
}
}
@@ -354,7 +368,9 @@ function prepareToHydrateHostTextInstance(fiber: Fiber): boolean {
return shouldUpdate;
}
-function skipPastDehydratedSuspenseInstance(fiber: Fiber): void {
+function skipPastDehydratedSuspenseInstance(
+ fiber: Fiber,
+): null | HydratableInstance {
if (!supportsHydration) {
invariant(
false,
@@ -362,15 +378,15 @@ function skipPastDehydratedSuspenseInstance(fiber: Fiber): void {
'This error is likely caused by a bug in React. Please file an issue.',
);
}
- let suspenseInstance = fiber.stateNode;
+ let suspenseState: null | SuspenseState = fiber.memoizedState;
+ let suspenseInstance: null | SuspenseInstance =
+ suspenseState !== null ? suspenseState.dehydrated : null;
invariant(
suspenseInstance,
'Expected to have a hydrated suspense instance. ' +
'This error is likely caused by a bug in React. Please file an issue.',
);
- nextHydratableInstance = getNextHydratableInstanceAfterSuspenseInstance(
- suspenseInstance,
- );
+ return getNextHydratableInstanceAfterSuspenseInstance(suspenseInstance);
}
function popToNextHostParent(fiber: Fiber): void {
@@ -379,7 +395,7 @@ function popToNextHostParent(fiber: Fiber): void {
parent !== null &&
parent.tag !== HostComponent &&
parent.tag !== HostRoot &&
- parent.tag !== DehydratedSuspenseComponent
+ parent.tag !== SuspenseComponent
) {
parent = parent.return;
}
@@ -425,9 +441,13 @@ function popHydrationState(fiber: Fiber): boolean {
}
popToNextHostParent(fiber);
- nextHydratableInstance = hydrationParentFiber
- ? getNextHydratableSibling(fiber.stateNode)
- : null;
+ if (fiber.tag === SuspenseComponent) {
+ nextHydratableInstance = skipPastDehydratedSuspenseInstance(fiber);
+ } else {
+ nextHydratableInstance = hydrationParentFiber
+ ? getNextHydratableSibling(fiber.stateNode)
+ : null;
+ }
return true;
}
@@ -449,6 +469,5 @@ export {
tryToClaimNextHydratableInstance,
prepareToHydrateHostInstance,
prepareToHydrateHostTextInstance,
- skipPastDehydratedSuspenseInstance,
popHydrationState,
};
diff --git a/packages/react-reconciler/src/ReactFiberNewContext.js b/packages/react-reconciler/src/ReactFiberNewContext.js
index 35540c2ca8ca3..1839283ba6d55 100644
--- a/packages/react-reconciler/src/ReactFiberNewContext.js
+++ b/packages/react-reconciler/src/ReactFiberNewContext.js
@@ -25,7 +25,7 @@ import MAX_SIGNED_31_BIT_INT from './maxSigned31BitInt';
import {
ContextProvider,
ClassComponent,
- DehydratedSuspenseComponent,
+ DehydratedFragment,
} from 'shared/ReactWorkTags';
import invariant from 'shared/invariant';
@@ -249,15 +249,20 @@ export function propagateContextChange(
nextFiber = fiber.type === workInProgress.type ? null : fiber.child;
} else if (
enableSuspenseServerRenderer &&
- fiber.tag === DehydratedSuspenseComponent
+ fiber.tag === DehydratedFragment
) {
- // If a dehydrated suspense component is in this subtree, we don't know
+ // If a dehydrated suspense bounudary is in this subtree, we don't know
// if it will have any context consumers in it. The best we can do is
- // mark it as having updates on its children.
- if (fiber.expirationTime < renderExpirationTime) {
- fiber.expirationTime = renderExpirationTime;
+ // mark it as having updates.
+ let parentSuspense = fiber.return;
+ invariant(
+ parentSuspense !== null,
+ 'We just came from a parent so we must have had a parent. This is a bug in React.',
+ );
+ if (parentSuspense.expirationTime < renderExpirationTime) {
+ parentSuspense.expirationTime = renderExpirationTime;
}
- let alternate = fiber.alternate;
+ let alternate = parentSuspense.alternate;
if (
alternate !== null &&
alternate.expirationTime < renderExpirationTime
@@ -268,7 +273,7 @@ export function propagateContextChange(
// because we want to schedule this fiber as having work
// on its children. We'll use the childExpirationTime on
// this fiber to indicate that a context has changed.
- scheduleWorkOnParentPath(fiber, renderExpirationTime);
+ scheduleWorkOnParentPath(parentSuspense, renderExpirationTime);
nextFiber = fiber.sibling;
} else {
// Traverse down.
diff --git a/packages/react-reconciler/src/ReactFiberSuspenseComponent.js b/packages/react-reconciler/src/ReactFiberSuspenseComponent.js
index 2440337b2e001..06f5aebc1b8de 100644
--- a/packages/react-reconciler/src/ReactFiberSuspenseComponent.js
+++ b/packages/react-reconciler/src/ReactFiberSuspenseComponent.js
@@ -8,12 +8,23 @@
*/
import type {Fiber} from './ReactFiber';
+import type {SuspenseInstance} from './ReactFiberHostConfig';
import {SuspenseComponent, SuspenseListComponent} from 'shared/ReactWorkTags';
import {NoEffect, DidCapture} from 'shared/ReactSideEffectTags';
-// TODO: This is now an empty object. Should we switch this to a boolean?
-// Alternatively we can make this use an effect tag similar to SuspenseList.
-export type SuspenseState = {||};
+// A null SuspenseState represents an unsuspended normal Suspense boundary.
+// A non-null SuspenseState means that it is blocked for one reason or another.
+// - A non-null dehydrated field means it's blocked pending hydration.
+// - A non-null dehydrated field can use isSuspenseInstancePending or
+// isSuspenseInstanceFallback to query the reason for being dehydrated.
+// - A null dehydrated field means it's blocked by something suspending and
+// we're currently showing a fallback instead.
+export type SuspenseState = {|
+ // If this boundary is still dehydrated, we store the SuspenseInstance
+ // here to indicate that it is dehydrated (flag) and for quick access
+ // to check things like isSuspenseInstancePending.
+ dehydrated: null | SuspenseInstance,
+|};
export type SuspenseListTailMode = 'collapsed' | 'hidden' | void;
@@ -42,6 +53,10 @@ export function shouldCaptureSuspense(
// fallback. Otherwise, don't capture and bubble to the next boundary.
const nextState: SuspenseState | null = workInProgress.memoizedState;
if (nextState !== null) {
+ if (nextState.dehydrated !== null) {
+ // A dehydrated boundary always captures.
+ return true;
+ }
return false;
}
const props = workInProgress.memoizedProps;
diff --git a/packages/react-reconciler/src/ReactFiberThrow.js b/packages/react-reconciler/src/ReactFiberThrow.js
index 6f70e4e532d29..0e2d869b729a6 100644
--- a/packages/react-reconciler/src/ReactFiberThrow.js
+++ b/packages/react-reconciler/src/ReactFiberThrow.js
@@ -22,7 +22,6 @@ import {
ClassComponent,
HostRoot,
SuspenseComponent,
- DehydratedSuspenseComponent,
IncompleteClassComponent,
} from 'shared/ReactWorkTags';
import {
@@ -32,10 +31,7 @@ import {
ShouldCapture,
LifecycleEffectMask,
} from 'shared/ReactSideEffectTags';
-import {
- enableSchedulerTracing,
- enableSuspenseServerRenderer,
-} from 'shared/ReactFeatureFlags';
+import {enableSchedulerTracing} from 'shared/ReactFeatureFlags';
import {NoMode, BatchedMode} from './ReactTypeOfMode';
import {shouldCaptureSuspense} from './ReactFiberSuspenseComponent';
@@ -61,15 +57,11 @@ import {
markLegacyErrorBoundaryAsFailed,
isAlreadyFailedLegacyErrorBoundary,
pingSuspendedRoot,
- resolveRetryThenable,
checkForWrongSuspensePriorityInDEV,
} from './ReactFiberWorkLoop';
-import invariant from 'shared/invariant';
-
import {Sync} from './ReactFiberExpirationTime';
-const PossiblyWeakSet = typeof WeakSet === 'function' ? WeakSet : Set;
const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
function createRootErrorUpdate(
@@ -323,36 +315,6 @@ function throwException(
workInProgress.effectTag |= ShouldCapture;
workInProgress.expirationTime = renderExpirationTime;
- return;
- } else if (
- enableSuspenseServerRenderer &&
- workInProgress.tag === DehydratedSuspenseComponent
- ) {
- attachPingListener(root, renderExpirationTime, thenable);
-
- // Since we already have a current fiber, we can eagerly add a retry listener.
- let retryCache = workInProgress.memoizedState;
- if (retryCache === null) {
- retryCache = workInProgress.memoizedState = new PossiblyWeakSet();
- const current = workInProgress.alternate;
- invariant(
- current,
- 'A dehydrated suspense boundary must commit before trying to render. ' +
- 'This is probably a bug in React.',
- );
- current.memoizedState = retryCache;
- }
- // Memoize using the boundary fiber to prevent redundant listeners.
- if (!retryCache.has(thenable)) {
- retryCache.add(thenable);
- let retry = resolveRetryThenable.bind(null, workInProgress, thenable);
- if (enableSchedulerTracing) {
- retry = Schedule_tracing_wrap(retry);
- }
- thenable.then(retry, retry);
- }
- workInProgress.effectTag |= ShouldCapture;
- workInProgress.expirationTime = renderExpirationTime;
return;
}
// This boundary already captured during this render. Continue to the next
diff --git a/packages/react-reconciler/src/ReactFiberUnwindWork.js b/packages/react-reconciler/src/ReactFiberUnwindWork.js
index 425eb7d00ce3b..8ce3870468bed 100644
--- a/packages/react-reconciler/src/ReactFiberUnwindWork.js
+++ b/packages/react-reconciler/src/ReactFiberUnwindWork.js
@@ -9,6 +9,7 @@
import type {Fiber} from './ReactFiber';
import type {ExpirationTime} from './ReactFiberExpirationTime';
+import type {SuspenseState} from './ReactFiberSuspenseComponent';
import {
ClassComponent,
@@ -18,7 +19,6 @@ import {
ContextProvider,
SuspenseComponent,
SuspenseListComponent,
- DehydratedSuspenseComponent,
} from 'shared/ReactWorkTags';
import {DidCapture, NoEffect, ShouldCapture} from 'shared/ReactSideEffectTags';
import {enableSuspenseServerRenderer} from 'shared/ReactFeatureFlags';
@@ -71,6 +71,18 @@ function unwindWork(
}
case SuspenseComponent: {
popSuspenseContext(workInProgress);
+ if (enableSuspenseServerRenderer) {
+ const suspenseState: null | SuspenseState =
+ workInProgress.memoizedState;
+ if (suspenseState !== null && suspenseState.dehydrated !== null) {
+ invariant(
+ workInProgress.alternate !== null,
+ 'Threw in newly mounted dehydrated component. This is likely a bug in ' +
+ 'React. Please file an issue.',
+ );
+ resetHydrationState();
+ }
+ }
const effectTag = workInProgress.effectTag;
if (effectTag & ShouldCapture) {
workInProgress.effectTag = (effectTag & ~ShouldCapture) | DidCapture;
@@ -79,23 +91,6 @@ function unwindWork(
}
return null;
}
- case DehydratedSuspenseComponent: {
- if (enableSuspenseServerRenderer) {
- popSuspenseContext(workInProgress);
- if (workInProgress.alternate === null) {
- // TODO: popHydrationState
- } else {
- resetHydrationState();
- }
- const effectTag = workInProgress.effectTag;
- if (effectTag & ShouldCapture) {
- workInProgress.effectTag = (effectTag & ~ShouldCapture) | DidCapture;
- // Captured a suspense effect. Re-render the boundary.
- return workInProgress;
- }
- }
- return null;
- }
case SuspenseListComponent: {
popSuspenseContext(workInProgress);
// SuspenseList doesn't actually catch anything. It should've been
@@ -137,11 +132,6 @@ function unwindInterruptedWork(interruptedWork: Fiber) {
case SuspenseComponent:
popSuspenseContext(interruptedWork);
break;
- case DehydratedSuspenseComponent:
- if (enableSuspenseServerRenderer) {
- popSuspenseContext(interruptedWork);
- }
- break;
case SuspenseListComponent:
popSuspenseContext(interruptedWork);
break;
diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js
index e67df799a20e3..a5a91b99dfce1 100644
--- a/packages/react-reconciler/src/ReactFiberWorkLoop.js
+++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js
@@ -76,7 +76,6 @@ import {
HostRoot,
ClassComponent,
SuspenseComponent,
- DehydratedSuspenseComponent,
FunctionComponent,
ForwardRef,
MemoComponent,
@@ -2208,9 +2207,6 @@ export function resolveRetryThenable(boundaryFiber: Fiber, thenable: Thenable) {
case SuspenseComponent:
retryCache = boundaryFiber.stateNode;
break;
- case DehydratedSuspenseComponent:
- retryCache = boundaryFiber.memoizedState;
- break;
default:
invariant(
false,
diff --git a/packages/shared/ReactWorkTags.js b/packages/shared/ReactWorkTags.js
index 5411e5b17be74..5c36488a09222 100644
--- a/packages/shared/ReactWorkTags.js
+++ b/packages/shared/ReactWorkTags.js
@@ -48,6 +48,6 @@ export const MemoComponent = 14;
export const SimpleMemoComponent = 15;
export const LazyComponent = 16;
export const IncompleteClassComponent = 17;
-export const DehydratedSuspenseComponent = 18;
+export const DehydratedFragment = 18;
export const SuspenseListComponent = 19;
export const FundamentalComponent = 20;
diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json
index 604436b246022..392d643e4ef52 100644
--- a/scripts/error-codes/codes.json
+++ b/scripts/error-codes/codes.json
@@ -337,5 +337,7 @@
"336": "The \"%s\" event responder cannot be used via the \"useEvent\" hook.",
"337": "An invalid event responder was provided to host component",
"338": "ReactDOMServer does not yet support the fundamental API.",
- "339": "An invalid value was used as an event listener. Expect one or many event listeners created via React.unstable_useResponder()."
+ "339": "An invalid value was used as an event listener. Expect one or many event listeners created via React.unstable_useResponder().",
+ "340": "Threw in newly mounted dehydrated component. This is likely a bug in React. Please file an issue.",
+ "341": "We just came from a parent so we must have had a parent. This is a bug in React."
}