Skip to content

Commit 0a50547

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 commit 5c8dabf.
1 parent d27c5ca commit 0a50547

File tree

13 files changed

+953
-632
lines changed

13 files changed

+953
-632
lines changed

compiled-rn/facebook-fbsource/xplat/js/RKJSModules/vendor/react-test-renderer/cjs/ReactTestRenderer-dev.js

Lines changed: 100 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
* @noflow
88
* @nolint
99
* @preventMunge
10-
* @generated SignedSource<<adf921170475fb87731c699acf7c0c8f>>
10+
* @generated SignedSource<<4a19698954f795c20e290576071bf6b0>>
1111
*/
1212

1313
'use strict';
@@ -4118,6 +4118,7 @@ function trackUsedThenable(thenableState, thenable, index) {
41184118

41194119
case "rejected": {
41204120
var rejectedError = thenable.reason;
4121+
checkIfUseWrappedInAsyncCatch(rejectedError);
41214122
throw rejectedError;
41224123
}
41234124

@@ -4173,18 +4174,20 @@ function trackUsedThenable(thenableState, thenable, index) {
41734174
rejectedThenable.reason = error;
41744175
}
41754176
}
4176-
);
4177-
} // Check one more time in case the thenable resolved synchronously.
4177+
); // Check one more time in case the thenable resolved synchronously.
41784178

4179-
switch (thenable.status) {
4180-
case "fulfilled": {
4181-
var fulfilledThenable = thenable;
4182-
return fulfilledThenable.value;
4183-
}
4179+
switch (thenable.status) {
4180+
case "fulfilled": {
4181+
var fulfilledThenable = thenable;
4182+
return fulfilledThenable.value;
4183+
}
41844184

4185-
case "rejected": {
4186-
var rejectedThenable = thenable;
4187-
throw rejectedThenable.reason;
4185+
case "rejected": {
4186+
var rejectedThenable = thenable;
4187+
var _rejectedError = rejectedThenable.reason;
4188+
checkIfUseWrappedInAsyncCatch(_rejectedError);
4189+
throw _rejectedError;
4190+
}
41884191
}
41894192
} // Suspend.
41904193
//
@@ -4243,6 +4246,22 @@ function checkIfUseWrappedInTryCatch() {
42434246

42444247
return false;
42454248
}
4249+
function checkIfUseWrappedInAsyncCatch(rejectedReason) {
4250+
// This check runs in prod, too, because it prevents a more confusing
4251+
// downstream error, where SuspenseException is caught by a promise and
4252+
// thrown asynchronously.
4253+
// TODO: Another way to prevent SuspenseException from leaking into an async
4254+
// execution context is to check the dispatcher every time `use` is called,
4255+
// or some equivalent. That might be preferable for other reasons, too, since
4256+
// it matches how we prevent similar mistakes for other hooks.
4257+
if (rejectedReason === SuspenseException) {
4258+
throw new Error(
4259+
"Hooks are not supported inside an async component. This " +
4260+
"error is often caused by accidentally adding `'use client'` " +
4261+
"to a module that was originally written for the server."
4262+
);
4263+
}
4264+
}
42464265

42474266
var thenableState$1 = null;
42484267
var thenableIndexCounter$1 = 0;
@@ -6527,10 +6546,12 @@ var ReactCurrentDispatcher$1 = ReactSharedInternals.ReactCurrentDispatcher,
65276546
var didWarnAboutMismatchedHooksForComponent;
65286547
var didWarnUncachedGetSnapshot;
65296548
var didWarnAboutUseWrappedInTryCatch;
6549+
var didWarnAboutAsyncClientComponent;
65306550

65316551
{
65326552
didWarnAboutMismatchedHooksForComponent = new Set();
65336553
didWarnAboutUseWrappedInTryCatch = new Set();
6554+
didWarnAboutAsyncClientComponent = new Set();
65346555
} // The effect "instance" is a shared object that remains the same for the entire
65356556
// lifetime of an effect. In Rust terms, a RefCell. We use it to store the
65366557
// "destroy" function that is returned from an effect, because that is stateful.
@@ -6671,6 +6692,57 @@ function warnOnHookMismatchInDev(currentHookName) {
66716692
}
66726693
}
66736694

6695+
function warnIfAsyncClientComponent(Component, componentDoesIncludeHooks) {
6696+
{
6697+
// This dev-only check only works for detecting native async functions,
6698+
// not transpiled ones. There's also a prod check that we use to prevent
6699+
// async client components from crashing the app; the prod one works even
6700+
// for transpiled async functions. Neither mechanism is completely
6701+
// bulletproof but together they cover the most common cases.
6702+
var isAsyncFunction = // $FlowIgnore[method-unbinding]
6703+
Object.prototype.toString.call(Component) === "[object AsyncFunction]";
6704+
6705+
if (isAsyncFunction) {
6706+
// Encountered an async Client Component. This is not yet supported,
6707+
// except in certain constrained cases, like during a route navigation.
6708+
var componentName = getComponentNameFromFiber(currentlyRenderingFiber$1);
6709+
6710+
if (!didWarnAboutAsyncClientComponent.has(componentName)) {
6711+
didWarnAboutAsyncClientComponent.add(componentName); // Check if this is a sync update. We use the "root" render lanes here
6712+
// because the "subtree" render lanes may include additional entangled
6713+
// lanes related to revealing previously hidden content.
6714+
6715+
var root = getWorkInProgressRoot();
6716+
var rootRenderLanes = getWorkInProgressRootRenderLanes();
6717+
6718+
if (root !== null && includesBlockingLane(root, rootRenderLanes)) {
6719+
error(
6720+
"async/await is not yet supported in Client Components, only " +
6721+
"Server Components. This error is often caused by accidentally " +
6722+
"adding `'use client'` to a module that was originally written " +
6723+
"for the server."
6724+
);
6725+
} else {
6726+
// This is a concurrent (Transition, Retry, etc) render. We don't
6727+
// warn in these cases.
6728+
//
6729+
// However, Async Components are forbidden to include hooks, even
6730+
// during a transition, so let's check for that here.
6731+
//
6732+
// TODO: Add a corresponding warning to Server Components runtime.
6733+
if (componentDoesIncludeHooks) {
6734+
error(
6735+
"Hooks are not supported inside an async component. This " +
6736+
"error is often caused by accidentally adding `'use client'` " +
6737+
"to a module that was originally written for the server."
6738+
);
6739+
}
6740+
}
6741+
}
6742+
}
6743+
}
6744+
}
6745+
66746746
function throwInvalidHookError() {
66756747
throw new Error(
66766748
"Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for" +
@@ -6823,18 +6895,20 @@ function renderWithHooks(
68236895
);
68246896
}
68256897

6826-
finishRenderingHooks(current, workInProgress);
6898+
finishRenderingHooks(current, workInProgress, Component);
68276899
return children;
68286900
}
68296901

6830-
function finishRenderingHooks(current, workInProgress) {
6831-
// We can assume the previous dispatcher is always this one, since we set it
6832-
// at the beginning of the render phase and there's no re-entrance.
6833-
ReactCurrentDispatcher$1.current = ContextOnlyDispatcher;
6834-
6902+
function finishRenderingHooks(current, workInProgress, Component) {
68356903
{
68366904
workInProgress._debugHookTypes = hookTypesDev;
6837-
} // This check uses currentHook so that it works the same in DEV and prod bundles.
6905+
var componentDoesIncludeHooks =
6906+
workInProgressHook !== null || thenableIndexCounter !== 0;
6907+
warnIfAsyncClientComponent(Component, componentDoesIncludeHooks);
6908+
} // We can assume the previous dispatcher is always this one, since we set it
6909+
// at the beginning of the render phase and there's no re-entrance.
6910+
6911+
ReactCurrentDispatcher$1.current = ContextOnlyDispatcher; // This check uses currentHook so that it works the same in DEV and prod bundles.
68386912
// hookTypesDev could catch more cases (e.g. context) but only in DEV bundles.
68396913

68406914
var didRenderTooFewHooks = currentHook !== null && currentHook.next !== null;
@@ -6885,7 +6959,12 @@ function finishRenderingHooks(current, workInProgress) {
68856959
var componentName =
68866960
getComponentNameFromFiber(workInProgress) || "Unknown";
68876961

6888-
if (!didWarnAboutUseWrappedInTryCatch.has(componentName)) {
6962+
if (
6963+
!didWarnAboutUseWrappedInTryCatch.has(componentName) && // This warning also fires if you suspend with `use` inside an
6964+
// async component. Since we warn for that above, we'll silence this
6965+
// second warning by checking here.
6966+
!didWarnAboutAsyncClientComponent.has(componentName)
6967+
) {
68896968
didWarnAboutUseWrappedInTryCatch.add(componentName);
68906969

68916970
error(
@@ -6925,7 +7004,7 @@ function replaySuspendedComponentWithHooks(
69257004
props,
69267005
secondArg
69277006
);
6928-
finishRenderingHooks(current, workInProgress);
7007+
finishRenderingHooks(current, workInProgress, Component);
69297008
return children;
69307009
}
69317010

@@ -23903,7 +23982,7 @@ function createFiberRoot(
2390323982
return root;
2390423983
}
2390523984

23906-
var ReactVersion = "18.3.0-canary-47385f8fa-20230630";
23985+
var ReactVersion = "18.3.0-canary-5c8dabf88-20230701";
2390723986

2390823987
// Might add PROFILE later.
2390923988

0 commit comments

Comments
 (0)