Skip to content

Commit d8c7f59

Browse files
committed
Detect and warn about native async function components in development (#27031)
Adds a development warning to complement the error introduced by #27019. We can detect and warn about async client components by checking the prototype of the function. This won't work for environments where async functions are transpiled, but for native async functions, it allows us to log an earlier warning during development, including in cases that don't trigger the infinite loop guard added in #27019. It does not supersede the infinite loop guard, though, because that mechanism also prevents the app from crashing. I also added a warning for calling a hook inside an async function. This one fires even during a transition. We could add a corresponding warning to Flight, since hooks are not allowed in async Server Components, either. (Though in both environments, this is better handled by a lint rule.) DiffTrain build for [5c8dabf](5c8dabf)
1 parent 023fffc commit d8c7f59

26 files changed

+2715
-1904
lines changed

compiled/facebook-www/JSXDEVRuntime-dev.classic.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -466,7 +466,14 @@ function describeNativeComponentFrame(fn, construct) {
466466
// tries to access React/ReactDOM/props. We should probably make this throw
467467
// in simple components too
468468

469-
fn();
469+
var maybePromise = fn(); // If the function component returns a promise, it's likely an async
470+
// component, which we don't yet support. Attach a noop catch handler to
471+
// silence the error.
472+
// TODO: Implement component stacks for async client components?
473+
474+
if (maybePromise && typeof maybePromise.catch === "function") {
475+
maybePromise.catch(function () {});
476+
}
470477
}
471478
} catch (sample) {
472479
// This is inlined manually because closure doesn't do it for us.

compiled/facebook-www/JSXDEVRuntime-dev.modern.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -466,7 +466,14 @@ function describeNativeComponentFrame(fn, construct) {
466466
// tries to access React/ReactDOM/props. We should probably make this throw
467467
// in simple components too
468468

469-
fn();
469+
var maybePromise = fn(); // If the function component returns a promise, it's likely an async
470+
// component, which we don't yet support. Attach a noop catch handler to
471+
// silence the error.
472+
// TODO: Implement component stacks for async client components?
473+
474+
if (maybePromise && typeof maybePromise.catch === "function") {
475+
maybePromise.catch(function () {});
476+
}
470477
}
471478
} catch (sample) {
472479
// This is inlined manually because closure doesn't do it for us.

compiled/facebook-www/REVISION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
7f362de1588d98438787d652941533e21f2f332d
1+
5c8dabf8864e1d826c831d6096b2dfa66309961a

compiled/facebook-www/React-dev.classic.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ if (
2727
}
2828
"use strict";
2929

30-
var ReactVersion = "18.3.0-www-classic-60aa7d44";
30+
var ReactVersion = "18.3.0-www-classic-cc85cd34";
3131

3232
// ATTENTION
3333
// When adding new symbols to this file,
@@ -2293,7 +2293,14 @@ function describeNativeComponentFrame(fn, construct) {
22932293
// tries to access React/ReactDOM/props. We should probably make this throw
22942294
// in simple components too
22952295

2296-
fn();
2296+
var maybePromise = fn(); // If the function component returns a promise, it's likely an async
2297+
// component, which we don't yet support. Attach a noop catch handler to
2298+
// silence the error.
2299+
// TODO: Implement component stacks for async client components?
2300+
2301+
if (maybePromise && typeof maybePromise.catch === "function") {
2302+
maybePromise.catch(function () {});
2303+
}
22972304
}
22982305
} catch (sample) {
22992306
// This is inlined manually because closure doesn't do it for us.

compiled/facebook-www/React-dev.modern.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ if (
2727
}
2828
"use strict";
2929

30-
var ReactVersion = "18.3.0-www-modern-fec65964";
30+
var ReactVersion = "18.3.0-www-modern-01767f27";
3131

3232
// ATTENTION
3333
// When adding new symbols to this file,
@@ -2293,7 +2293,14 @@ function describeNativeComponentFrame(fn, construct) {
22932293
// tries to access React/ReactDOM/props. We should probably make this throw
22942294
// in simple components too
22952295

2296-
fn();
2296+
var maybePromise = fn(); // If the function component returns a promise, it's likely an async
2297+
// component, which we don't yet support. Attach a noop catch handler to
2298+
// silence the error.
2299+
// TODO: Implement component stacks for async client components?
2300+
2301+
if (maybePromise && typeof maybePromise.catch === "function") {
2302+
maybePromise.catch(function () {});
2303+
}
22972304
}
22982305
} catch (sample) {
22992306
// This is inlined manually because closure doesn't do it for us.

compiled/facebook-www/React-prod.modern.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -622,4 +622,4 @@ exports.useSyncExternalStore = function (
622622
);
623623
};
624624
exports.useTransition = useTransition;
625-
exports.version = "18.3.0-www-modern-e397df4a";
625+
exports.version = "18.3.0-www-modern-4c04bf5d";

compiled/facebook-www/ReactART-dev.classic.js

Lines changed: 107 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ function _assertThisInitialized(self) {
6969
return self;
7070
}
7171

72-
var ReactVersion = "18.3.0-www-classic-9da53968";
72+
var ReactVersion = "18.3.0-www-classic-4bc5632c";
7373

7474
var LegacyRoot = 0;
7575
var ConcurrentRoot = 1;
@@ -3013,7 +3013,14 @@ function describeNativeComponentFrame(fn, construct) {
30133013
// tries to access React/ReactDOM/props. We should probably make this throw
30143014
// in simple components too
30153015

3016-
fn();
3016+
var maybePromise = fn(); // If the function component returns a promise, it's likely an async
3017+
// component, which we don't yet support. Attach a noop catch handler to
3018+
// silence the error.
3019+
// TODO: Implement component stacks for async client components?
3020+
3021+
if (maybePromise && typeof maybePromise.catch === "function") {
3022+
maybePromise.catch(function () {});
3023+
}
30173024
}
30183025
} catch (sample) {
30193026
// This is inlined manually because closure doesn't do it for us.
@@ -5401,6 +5408,7 @@ function trackUsedThenable(thenableState, thenable, index) {
54015408

54025409
case "rejected": {
54035410
var rejectedError = thenable.reason;
5411+
checkIfUseWrappedInAsyncCatch(rejectedError);
54045412
throw rejectedError;
54055413
}
54065414

@@ -5456,18 +5464,20 @@ function trackUsedThenable(thenableState, thenable, index) {
54565464
rejectedThenable.reason = error;
54575465
}
54585466
}
5459-
);
5460-
} // Check one more time in case the thenable resolved synchronously.
5467+
); // Check one more time in case the thenable resolved synchronously.
54615468

5462-
switch (thenable.status) {
5463-
case "fulfilled": {
5464-
var fulfilledThenable = thenable;
5465-
return fulfilledThenable.value;
5466-
}
5469+
switch (thenable.status) {
5470+
case "fulfilled": {
5471+
var fulfilledThenable = thenable;
5472+
return fulfilledThenable.value;
5473+
}
54675474

5468-
case "rejected": {
5469-
var rejectedThenable = thenable;
5470-
throw rejectedThenable.reason;
5475+
case "rejected": {
5476+
var rejectedThenable = thenable;
5477+
var _rejectedError = rejectedThenable.reason;
5478+
checkIfUseWrappedInAsyncCatch(_rejectedError);
5479+
throw _rejectedError;
5480+
}
54715481
}
54725482
} // Suspend.
54735483
//
@@ -5526,6 +5536,22 @@ function checkIfUseWrappedInTryCatch() {
55265536

55275537
return false;
55285538
}
5539+
function checkIfUseWrappedInAsyncCatch(rejectedReason) {
5540+
// This check runs in prod, too, because it prevents a more confusing
5541+
// downstream error, where SuspenseException is caught by a promise and
5542+
// thrown asynchronously.
5543+
// TODO: Another way to prevent SuspenseException from leaking into an async
5544+
// execution context is to check the dispatcher every time `use` is called,
5545+
// or some equivalent. That might be preferable for other reasons, too, since
5546+
// it matches how we prevent similar mistakes for other hooks.
5547+
if (rejectedReason === SuspenseException) {
5548+
throw new Error(
5549+
"Hooks are not supported inside an async component. This " +
5550+
"error is often caused by accidentally adding `'use client'` " +
5551+
"to a module that was originally written for the server."
5552+
);
5553+
}
5554+
}
55295555

55305556
var thenableState$1 = null;
55315557
var thenableIndexCounter$1 = 0;
@@ -7843,10 +7869,12 @@ var ReactCurrentDispatcher$1 = ReactSharedInternals.ReactCurrentDispatcher,
78437869
var didWarnAboutMismatchedHooksForComponent;
78447870
var didWarnUncachedGetSnapshot;
78457871
var didWarnAboutUseWrappedInTryCatch;
7872+
var didWarnAboutAsyncClientComponent;
78467873

78477874
{
78487875
didWarnAboutMismatchedHooksForComponent = new Set();
78497876
didWarnAboutUseWrappedInTryCatch = new Set();
7877+
didWarnAboutAsyncClientComponent = new Set();
78507878
} // The effect "instance" is a shared object that remains the same for the entire
78517879
// lifetime of an effect. In Rust terms, a RefCell. We use it to store the
78527880
// "destroy" function that is returned from an effect, because that is stateful.
@@ -7987,6 +8015,57 @@ function warnOnHookMismatchInDev(currentHookName) {
79878015
}
79888016
}
79898017

8018+
function warnIfAsyncClientComponent(Component, componentDoesIncludeHooks) {
8019+
{
8020+
// This dev-only check only works for detecting native async functions,
8021+
// not transpiled ones. There's also a prod check that we use to prevent
8022+
// async client components from crashing the app; the prod one works even
8023+
// for transpiled async functions. Neither mechanism is completely
8024+
// bulletproof but together they cover the most common cases.
8025+
var isAsyncFunction = // $FlowIgnore[method-unbinding]
8026+
Object.prototype.toString.call(Component) === "[object AsyncFunction]";
8027+
8028+
if (isAsyncFunction) {
8029+
// Encountered an async Client Component. This is not yet supported,
8030+
// except in certain constrained cases, like during a route navigation.
8031+
var componentName = getComponentNameFromFiber(currentlyRenderingFiber$1);
8032+
8033+
if (!didWarnAboutAsyncClientComponent.has(componentName)) {
8034+
didWarnAboutAsyncClientComponent.add(componentName); // Check if this is a sync update. We use the "root" render lanes here
8035+
// because the "subtree" render lanes may include additional entangled
8036+
// lanes related to revealing previously hidden content.
8037+
8038+
var root = getWorkInProgressRoot();
8039+
var rootRenderLanes = getWorkInProgressRootRenderLanes();
8040+
8041+
if (root !== null && includesBlockingLane(root, rootRenderLanes)) {
8042+
error(
8043+
"async/await is not yet supported in Client Components, only " +
8044+
"Server Components. This error is often caused by accidentally " +
8045+
"adding `'use client'` to a module that was originally written " +
8046+
"for the server."
8047+
);
8048+
} else {
8049+
// This is a concurrent (Transition, Retry, etc) render. We don't
8050+
// warn in these cases.
8051+
//
8052+
// However, Async Components are forbidden to include hooks, even
8053+
// during a transition, so let's check for that here.
8054+
//
8055+
// TODO: Add a corresponding warning to Server Components runtime.
8056+
if (componentDoesIncludeHooks) {
8057+
error(
8058+
"Hooks are not supported inside an async component. This " +
8059+
"error is often caused by accidentally adding `'use client'` " +
8060+
"to a module that was originally written for the server."
8061+
);
8062+
}
8063+
}
8064+
}
8065+
}
8066+
}
8067+
}
8068+
79908069
function throwInvalidHookError() {
79918070
throw new Error(
79928071
"Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for" +
@@ -8156,18 +8235,20 @@ function renderWithHooks(
81568235
}
81578236
}
81588237

8159-
finishRenderingHooks(current, workInProgress);
8238+
finishRenderingHooks(current, workInProgress, Component);
81608239
return children;
81618240
}
81628241

8163-
function finishRenderingHooks(current, workInProgress) {
8164-
// We can assume the previous dispatcher is always this one, since we set it
8165-
// at the beginning of the render phase and there's no re-entrance.
8166-
ReactCurrentDispatcher$1.current = ContextOnlyDispatcher;
8167-
8242+
function finishRenderingHooks(current, workInProgress, Component) {
81688243
{
81698244
workInProgress._debugHookTypes = hookTypesDev;
8170-
} // This check uses currentHook so that it works the same in DEV and prod bundles.
8245+
var componentDoesIncludeHooks =
8246+
workInProgressHook !== null || thenableIndexCounter !== 0;
8247+
warnIfAsyncClientComponent(Component, componentDoesIncludeHooks);
8248+
} // We can assume the previous dispatcher is always this one, since we set it
8249+
// at the beginning of the render phase and there's no re-entrance.
8250+
8251+
ReactCurrentDispatcher$1.current = ContextOnlyDispatcher; // This check uses currentHook so that it works the same in DEV and prod bundles.
81718252
// hookTypesDev could catch more cases (e.g. context) but only in DEV bundles.
81728253

81738254
var didRenderTooFewHooks = currentHook !== null && currentHook.next !== null;
@@ -8240,7 +8321,12 @@ function finishRenderingHooks(current, workInProgress) {
82408321
var componentName =
82418322
getComponentNameFromFiber(workInProgress) || "Unknown";
82428323

8243-
if (!didWarnAboutUseWrappedInTryCatch.has(componentName)) {
8324+
if (
8325+
!didWarnAboutUseWrappedInTryCatch.has(componentName) && // This warning also fires if you suspend with `use` inside an
8326+
// async component. Since we warn for that above, we'll silence this
8327+
// second warning by checking here.
8328+
!didWarnAboutAsyncClientComponent.has(componentName)
8329+
) {
82448330
didWarnAboutUseWrappedInTryCatch.add(componentName);
82458331

82468332
error(
@@ -8280,7 +8366,7 @@ function replaySuspendedComponentWithHooks(
82808366
props,
82818367
secondArg
82828368
);
8283-
finishRenderingHooks(current, workInProgress);
8369+
finishRenderingHooks(current, workInProgress, Component);
82848370
return children;
82858371
}
82868372

0 commit comments

Comments
 (0)