diff --git a/packages/react-reconciler/src/ReactFiberLane.js b/packages/react-reconciler/src/ReactFiberLane.js index e90b02e441958..51126c6cdbcc4 100644 --- a/packages/react-reconciler/src/ReactFiberLane.js +++ b/packages/react-reconciler/src/ReactFiberLane.js @@ -636,6 +636,7 @@ export function markRootFinished(root: FiberRoot, remainingLanes: Lanes) { root.entangledLanes &= remainingLanes; root.errorRecoveryDisabledLanes &= remainingLanes; + root.shellSuspendCounter = 0; const entanglements = root.entanglements; const expirationTimes = root.expirationTimes; diff --git a/packages/react-reconciler/src/ReactFiberRoot.js b/packages/react-reconciler/src/ReactFiberRoot.js index 7b295bdbe80b8..e65e25b97df6b 100644 --- a/packages/react-reconciler/src/ReactFiberRoot.js +++ b/packages/react-reconciler/src/ReactFiberRoot.js @@ -74,6 +74,7 @@ function FiberRootNode( this.expiredLanes = NoLanes; this.finishedLanes = NoLanes; this.errorRecoveryDisabledLanes = NoLanes; + this.shellSuspendCounter = 0; this.entangledLanes = NoLanes; this.entanglements = createLaneMap(NoLanes); diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index f486bf2648dc3..92a18624cb42e 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -1944,6 +1944,7 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) { markRenderStarted(lanes); } + let didSuspendInShell = false; outer: do { try { if ( @@ -1969,6 +1970,42 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) { workInProgressRootExitStatus = RootDidNotComplete; break outer; } + case SuspendedOnImmediate: + case SuspendedOnData: { + if (!didSuspendInShell) { + const handler = getSuspenseHandler(); + if (handler === null) { + didSuspendInShell = true; + const counter = ++root.shellSuspendCounter; + if (counter > 100) { + // This root has suspended repeatedly in the shell without + // making any progress (i.e. committing something). This is + // highly suggestive of an infinite ping loop, often caused by + // an accidental Async Client Component. + // + // During a transition, we can suspend the work loop until the + // promise to resolve, but this is a sync render, so that's + // not an option. We also can't show a fallback, because none + // was provided. So our last resort is to throw an error. + // + // TODO: Remove this error in a future release. Other ways + // of handling this case include forcing a concurrent render, + // or putting the whole root into offscreen mode. + const asyncClientComponentError = new Error( + 'async/await is not yet supported in Client Components, ' + + 'only Server Components. This error is often caused by ' + + "accidentally adding `'use client'` to a module that " + + 'was originally written for the server.', + ); + workInProgressSuspendedReason = NotSuspended; + workInProgressThrownValue = null; + throwAndUnwindWorkLoop(unitOfWork, asyncClientComponentError); + break; + } + } + } + // Intentional fallthrough + } default: { // Unwind then continue with the normal work loop. workInProgressSuspendedReason = NotSuspended; diff --git a/packages/react-reconciler/src/ReactInternalTypes.js b/packages/react-reconciler/src/ReactInternalTypes.js index eae140ac4ed73..50a0ec85f9f25 100644 --- a/packages/react-reconciler/src/ReactInternalTypes.js +++ b/packages/react-reconciler/src/ReactInternalTypes.js @@ -248,6 +248,7 @@ type BaseFiberRootProperties = { pingedLanes: Lanes, expiredLanes: Lanes, errorRecoveryDisabledLanes: Lanes, + shellSuspendCounter: number, finishedLanes: Lanes, diff --git a/packages/react-reconciler/src/__tests__/ReactUse-test.js b/packages/react-reconciler/src/__tests__/ReactUse-test.js index ce3add950be5f..b225528a3ff57 100644 --- a/packages/react-reconciler/src/__tests__/ReactUse-test.js +++ b/packages/react-reconciler/src/__tests__/ReactUse-test.js @@ -17,6 +17,7 @@ let waitFor; let waitForPaint; let assertLog; let waitForAll; +let waitForMicrotasks; describe('ReactUse', () => { beforeEach(() => { @@ -40,6 +41,7 @@ describe('ReactUse', () => { assertLog = InternalTestUtils.assertLog; waitForPaint = InternalTestUtils.waitForPaint; waitFor = InternalTestUtils.waitFor; + waitForMicrotasks = InternalTestUtils.waitForMicrotasks; pendingTextRequests = new Map(); }); @@ -1616,4 +1618,96 @@ describe('ReactUse', () => { assertLog(['C']); expect(root).toMatchRenderedOutput('C'); }); + + // @gate !forceConcurrentByDefaultForTesting + test('an async component outside of a Suspense boundary crashes with an error (resolves in microtask)', async () => { + class ErrorBoundary extends React.Component { + state = {error: null}; + static getDerivedStateFromError(error) { + return {error}; + } + render() { + if (this.state.error) { + return ; + } + return this.props.children; + } + } + + async function AsyncClientComponent() { + return ; + } + + const root = ReactNoop.createRoot(); + await act(() => { + root.render( + + + , + ); + }); + assertLog([ + 'async/await is not yet supported in Client Components, only Server ' + + 'Components. This error is often caused by accidentally adding ' + + "`'use client'` to a module that was originally written for " + + 'the server.', + 'async/await is not yet supported in Client Components, only Server ' + + 'Components. This error is often caused by accidentally adding ' + + "`'use client'` to a module that was originally written for " + + 'the server.', + ]); + expect(root).toMatchRenderedOutput( + 'async/await is not yet supported in Client Components, only Server ' + + 'Components. This error is often caused by accidentally adding ' + + "`'use client'` to a module that was originally written for " + + 'the server.', + ); + }); + + + // @gate !forceConcurrentByDefaultForTesting + test('an async component outside of a Suspense boundary crashes with an error (resolves in macrotask)', async () => { + class ErrorBoundary extends React.Component { + state = {error: null}; + static getDerivedStateFromError(error) { + return {error}; + } + render() { + if (this.state.error) { + return ; + } + return this.props.children; + } + } + + async function AsyncClientComponent() { + await waitForMicrotasks(); + return ; + } + + const root = ReactNoop.createRoot(); + await act(() => { + root.render( + + + , + ); + }); + assertLog([ + 'async/await is not yet supported in Client Components, only Server ' + + 'Components. This error is often caused by accidentally adding ' + + "`'use client'` to a module that was originally written for " + + 'the server.', + 'async/await is not yet supported in Client Components, only Server ' + + 'Components. This error is often caused by accidentally adding ' + + "`'use client'` to a module that was originally written for " + + 'the server.', + ]); + expect(root).toMatchRenderedOutput( + 'async/await is not yet supported in Client Components, only Server ' + + 'Components. This error is often caused by accidentally adding ' + + "`'use client'` to a module that was originally written for " + + 'the server.', + ); + }); }); diff --git a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js index a1197be405837..abee206668ebb 100644 --- a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js +++ b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js @@ -13,22 +13,22 @@ // Use __VARIANT__ to simulate a GK. The tests will be run twice: once // with the __VARIANT__ set to `true`, and once set to `false`. -export const disableInputAttributeSyncing = __VARIANT__; -export const disableIEWorkarounds = __VARIANT__; -export const enableLegacyFBSupport = __VARIANT__; -export const enableUseRefAccessWarning = __VARIANT__; -export const enableProfilerNestedUpdateScheduledHook = __VARIANT__; -export const disableSchedulerTimeoutInWorkLoop = __VARIANT__; -export const enableLazyContextPropagation = __VARIANT__; -export const forceConcurrentByDefaultForTesting = __VARIANT__; -export const enableUnifiedSyncLane = __VARIANT__; -export const enableTransitionTracing = __VARIANT__; -export const enableCustomElementPropertySupport = __VARIANT__; -export const enableDeferRootSchedulingToMicrotask = __VARIANT__; -export const diffInCommitPhase = __VARIANT__; -export const enableAsyncActions = __VARIANT__; -export const alwaysThrottleRetries = __VARIANT__; -export const enableDO_NOT_USE_disableStrictPassiveEffect = __VARIANT__; +export const disableInputAttributeSyncing = !__VARIANT__; +export const disableIEWorkarounds = !__VARIANT__; +export const enableLegacyFBSupport = !__VARIANT__; +export const enableUseRefAccessWarning = !__VARIANT__; +export const enableProfilerNestedUpdateScheduledHook = !__VARIANT__; +export const disableSchedulerTimeoutInWorkLoop = !__VARIANT__; +export const enableLazyContextPropagation = !__VARIANT__; +export const forceConcurrentByDefaultForTesting = !__VARIANT__; +export const enableUnifiedSyncLane = !__VARIANT__; +export const enableTransitionTracing = !__VARIANT__; +export const enableCustomElementPropertySupport = !__VARIANT__; +export const enableDeferRootSchedulingToMicrotask = !__VARIANT__; +export const diffInCommitPhase = !__VARIANT__; +export const enableAsyncActions = !__VARIANT__; +export const alwaysThrottleRetries = !__VARIANT__; +export const enableDO_NOT_USE_disableStrictPassiveEffect = !__VARIANT__; // Enable this flag to help with concurrent mode debugging. // It logs information to the console about React scheduling, rendering, and commit phases. @@ -36,7 +36,7 @@ export const enableDO_NOT_USE_disableStrictPassiveEffect = __VARIANT__; // NOTE: This feature will only work in DEV mode; all callsites are wrapped with __DEV__. export const enableDebugTracing = __EXPERIMENTAL__; -export const enableSchedulingProfiler = __VARIANT__; +export const enableSchedulingProfiler = !__VARIANT__; // These are already tested in both modes using the build type dimension, // so we don't need to use __VARIANT__ to get extra coverage. diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index b1ae802b33b56..3423de2fa99b4 100644 --- a/scripts/error-codes/codes.json +++ b/scripts/error-codes/codes.json @@ -466,5 +466,6 @@ "478": "Thenable should have already resolved. This is a bug in React.", "479": "Cannot update optimistic state while rendering.", "480": "File/Blob fields are not yet supported in progressive forms. It probably means you are closing over binary data or FormData in a Server Action.", - "481": "Tried to encode a Server Action from a different instance than the encoder is from. This is a bug in React." + "481": "Tried to encode a Server Action from a different instance than the encoder is from. This is a bug in React.", + "482": "async/await is not yet supported in Client Components, only Server Components. This error is often caused by accidentally adding `'use client'` to a module that was originally written for the server." }