Skip to content

Commit ec00d40

Browse files
committed
Always warn if client component suspends with an uncached promise (#28159)
Previously we only warned during a synchronous update, because we eventually want to support async client components in controlled scenarios, like during navigations. However, we're going to warn in all cases for now until we figure out how that should work. DiffTrain build for [178f435](178f435)
1 parent fa09a71 commit ec00d40

20 files changed

+702
-574
lines changed

compiled/facebook-www/REVISION

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
554fc49f41465d914b15dc8eb2ec094f37824f7e
1+
178f4351947a842ff0b56700e9115b25ae8f20d0

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ if (__DEV__) {
2424
) {
2525
__REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(new Error());
2626
}
27-
var ReactVersion = "18.3.0-www-classic-2277e96d";
27+
var ReactVersion = "18.3.0-www-classic-8a9e991a";
2828

2929
// ATTENTION
3030
// When adding new symbols to this file,

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -572,4 +572,4 @@ exports.useSyncExternalStore = function (
572572
exports.useTransition = function () {
573573
return ReactCurrentDispatcher.current.useTransition();
574574
};
575-
exports.version = "18.3.0-www-classic-4ac70da3";
575+
exports.version = "18.3.0-www-classic-d6acec23";

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

+59-43
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ if (__DEV__) {
6666
return self;
6767
}
6868

69-
var ReactVersion = "18.3.0-www-classic-09d28a0e";
69+
var ReactVersion = "18.3.0-www-classic-909c7c72";
7070

7171
var LegacyRoot = 0;
7272
var ConcurrentRoot = 1;
@@ -6101,7 +6101,14 @@ if (__DEV__) {
61016101
}
61026102
}
61036103

6104-
var ReactCurrentActQueue$2 = ReactSharedInternals.ReactCurrentActQueue; // An error that is thrown (e.g. by `use`) to trigger Suspense. If we
6104+
var ReactCurrentActQueue$2 = ReactSharedInternals.ReactCurrentActQueue;
6105+
6106+
function getThenablesFromState(state) {
6107+
{
6108+
var devState = state;
6109+
return devState.thenables;
6110+
}
6111+
} // An error that is thrown (e.g. by `use`) to trigger Suspense. If we
61056112
// detect this is caught by userspace, we'll log a warning in development.
61066113

61076114
var SuspenseException = new Error(
@@ -6134,7 +6141,12 @@ if (__DEV__) {
61346141
function createThenableState() {
61356142
// The ThenableState is created the first time a component suspends. If it
61366143
// suspends again, we'll reuse the same state.
6137-
return [];
6144+
{
6145+
return {
6146+
didWarnAboutUncachedPromise: false,
6147+
thenables: []
6148+
};
6149+
}
61386150
}
61396151
function isThenableResolved(thenable) {
61406152
var status = thenable.status;
@@ -6148,16 +6160,45 @@ if (__DEV__) {
61486160
ReactCurrentActQueue$2.didUsePromise = true;
61496161
}
61506162

6151-
var previous = thenableState[index];
6163+
var trackedThenables = getThenablesFromState(thenableState);
6164+
var previous = trackedThenables[index];
61526165

61536166
if (previous === undefined) {
6154-
thenableState.push(thenable);
6167+
trackedThenables.push(thenable);
61556168
} else {
61566169
if (previous !== thenable) {
61576170
// Reuse the previous thenable, and drop the new one. We can assume
61586171
// they represent the same value, because components are idempotent.
6159-
// Avoid an unhandled rejection errors for the Promises that we'll
6172+
{
6173+
var thenableStateDev = thenableState;
6174+
6175+
if (!thenableStateDev.didWarnAboutUncachedPromise) {
6176+
// We should only warn the first time an uncached thenable is
6177+
// discovered per component, because if there are multiple, the
6178+
// subsequent ones are likely derived from the first.
6179+
//
6180+
// We track this on the thenableState instead of deduping using the
6181+
// component name like we usually do, because in the case of a
6182+
// promise-as-React-node, the owner component is likely different from
6183+
// the parent that's currently being reconciled. We'd have to track
6184+
// the owner using state, which we're trying to move away from. Though
6185+
// since this is dev-only, maybe that'd be OK.
6186+
//
6187+
// However, another benefit of doing it this way is we might
6188+
// eventually have a thenableState per memo/Forget boundary instead
6189+
// of per component, so this would allow us to have more
6190+
// granular warnings.
6191+
thenableStateDev.didWarnAboutUncachedPromise = true; // TODO: This warning should link to a corresponding docs page.
6192+
6193+
error(
6194+
"A component was suspended by an uncached promise. Creating " +
6195+
"promises inside a Client Component or hook is not yet " +
6196+
"supported, except via a Suspense-compatible library or framework."
6197+
);
6198+
}
6199+
} // Avoid an unhandled rejection errors for the Promises that we'll
61606200
// intentionally ignore.
6201+
61616202
thenable.then(noop, noop);
61626203
thenable = previous;
61636204
}
@@ -8259,7 +8300,7 @@ if (__DEV__) {
82598300
}
82608301
}
82618302

8262-
function warnIfAsyncClientComponent(Component, componentDoesIncludeHooks) {
8303+
function warnIfAsyncClientComponent(Component) {
82638304
{
82648305
// This dev-only check only works for detecting native async functions,
82658306
// not transpiled ones. There's also a prod check that we use to prevent
@@ -8271,43 +8312,20 @@ if (__DEV__) {
82718312
"[object AsyncFunction]";
82728313

82738314
if (isAsyncFunction) {
8274-
// Encountered an async Client Component. This is not yet supported,
8275-
// except in certain constrained cases, like during a route navigation.
8315+
// Encountered an async Client Component. This is not yet supported.
82768316
var componentName = getComponentNameFromFiber(
82778317
currentlyRenderingFiber$1
82788318
);
82798319

82808320
if (!didWarnAboutAsyncClientComponent.has(componentName)) {
8281-
didWarnAboutAsyncClientComponent.add(componentName); // Check if this is a sync update. We use the "root" render lanes here
8282-
// because the "subtree" render lanes may include additional entangled
8283-
// lanes related to revealing previously hidden content.
8321+
didWarnAboutAsyncClientComponent.add(componentName);
82848322

8285-
var root = getWorkInProgressRoot();
8286-
var rootRenderLanes = getWorkInProgressRootRenderLanes();
8287-
8288-
if (root !== null && includesBlockingLane(root, rootRenderLanes)) {
8289-
error(
8290-
"async/await is not yet supported in Client Components, only " +
8291-
"Server Components. This error is often caused by accidentally " +
8292-
"adding `'use client'` to a module that was originally written " +
8293-
"for the server."
8294-
);
8295-
} else {
8296-
// This is a concurrent (Transition, Retry, etc) render. We don't
8297-
// warn in these cases.
8298-
//
8299-
// However, Async Components are forbidden to include hooks, even
8300-
// during a transition, so let's check for that here.
8301-
//
8302-
// TODO: Add a corresponding warning to Server Components runtime.
8303-
if (componentDoesIncludeHooks) {
8304-
error(
8305-
"Hooks are not supported inside an async component. This " +
8306-
"error is often caused by accidentally adding `'use client'` " +
8307-
"to a module that was originally written for the server."
8308-
);
8309-
}
8310-
}
8323+
error(
8324+
"async/await is not yet supported in Client Components, only " +
8325+
"Server Components. This error is often caused by accidentally " +
8326+
"adding `'use client'` to a module that was originally written " +
8327+
"for the server."
8328+
);
83118329
}
83128330
}
83138331
}
@@ -8390,6 +8408,7 @@ if (__DEV__) {
83908408

83918409
ignorePreviousDependencies =
83928410
current !== null && current.type !== workInProgress.type;
8411+
warnIfAsyncClientComponent(Component);
83938412
}
83948413

83958414
workInProgress.memoizedState = null;
@@ -8482,16 +8501,13 @@ if (__DEV__) {
84828501
}
84838502
}
84848503

8485-
finishRenderingHooks(current, workInProgress, Component);
8504+
finishRenderingHooks(current, workInProgress);
84868505
return children;
84878506
}
84888507

84898508
function finishRenderingHooks(current, workInProgress, Component) {
84908509
{
84918510
workInProgress._debugHookTypes = hookTypesDev;
8492-
var componentDoesIncludeHooks =
8493-
workInProgressHook !== null || thenableIndexCounter !== 0;
8494-
warnIfAsyncClientComponent(Component, componentDoesIncludeHooks);
84958511
} // We can assume the previous dispatcher is always this one, since we set it
84968512
// at the beginning of the render phase and there's no re-entrance.
84978513

@@ -8615,7 +8631,7 @@ if (__DEV__) {
86158631
props,
86168632
secondArg
86178633
);
8618-
finishRenderingHooks(current, workInProgress, Component);
8634+
finishRenderingHooks(current, workInProgress);
86198635
return children;
86208636
}
86218637

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

+59-43
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ if (__DEV__) {
6666
return self;
6767
}
6868

69-
var ReactVersion = "18.3.0-www-modern-416a65ea";
69+
var ReactVersion = "18.3.0-www-modern-4f70a13e";
7070

7171
var LegacyRoot = 0;
7272
var ConcurrentRoot = 1;
@@ -5851,7 +5851,14 @@ if (__DEV__) {
58515851
}
58525852
}
58535853

5854-
var ReactCurrentActQueue$2 = ReactSharedInternals.ReactCurrentActQueue; // An error that is thrown (e.g. by `use`) to trigger Suspense. If we
5854+
var ReactCurrentActQueue$2 = ReactSharedInternals.ReactCurrentActQueue;
5855+
5856+
function getThenablesFromState(state) {
5857+
{
5858+
var devState = state;
5859+
return devState.thenables;
5860+
}
5861+
} // An error that is thrown (e.g. by `use`) to trigger Suspense. If we
58555862
// detect this is caught by userspace, we'll log a warning in development.
58565863

58575864
var SuspenseException = new Error(
@@ -5884,7 +5891,12 @@ if (__DEV__) {
58845891
function createThenableState() {
58855892
// The ThenableState is created the first time a component suspends. If it
58865893
// suspends again, we'll reuse the same state.
5887-
return [];
5894+
{
5895+
return {
5896+
didWarnAboutUncachedPromise: false,
5897+
thenables: []
5898+
};
5899+
}
58885900
}
58895901
function isThenableResolved(thenable) {
58905902
var status = thenable.status;
@@ -5898,16 +5910,45 @@ if (__DEV__) {
58985910
ReactCurrentActQueue$2.didUsePromise = true;
58995911
}
59005912

5901-
var previous = thenableState[index];
5913+
var trackedThenables = getThenablesFromState(thenableState);
5914+
var previous = trackedThenables[index];
59025915

59035916
if (previous === undefined) {
5904-
thenableState.push(thenable);
5917+
trackedThenables.push(thenable);
59055918
} else {
59065919
if (previous !== thenable) {
59075920
// Reuse the previous thenable, and drop the new one. We can assume
59085921
// they represent the same value, because components are idempotent.
5909-
// Avoid an unhandled rejection errors for the Promises that we'll
5922+
{
5923+
var thenableStateDev = thenableState;
5924+
5925+
if (!thenableStateDev.didWarnAboutUncachedPromise) {
5926+
// We should only warn the first time an uncached thenable is
5927+
// discovered per component, because if there are multiple, the
5928+
// subsequent ones are likely derived from the first.
5929+
//
5930+
// We track this on the thenableState instead of deduping using the
5931+
// component name like we usually do, because in the case of a
5932+
// promise-as-React-node, the owner component is likely different from
5933+
// the parent that's currently being reconciled. We'd have to track
5934+
// the owner using state, which we're trying to move away from. Though
5935+
// since this is dev-only, maybe that'd be OK.
5936+
//
5937+
// However, another benefit of doing it this way is we might
5938+
// eventually have a thenableState per memo/Forget boundary instead
5939+
// of per component, so this would allow us to have more
5940+
// granular warnings.
5941+
thenableStateDev.didWarnAboutUncachedPromise = true; // TODO: This warning should link to a corresponding docs page.
5942+
5943+
error(
5944+
"A component was suspended by an uncached promise. Creating " +
5945+
"promises inside a Client Component or hook is not yet " +
5946+
"supported, except via a Suspense-compatible library or framework."
5947+
);
5948+
}
5949+
} // Avoid an unhandled rejection errors for the Promises that we'll
59105950
// intentionally ignore.
5951+
59115952
thenable.then(noop, noop);
59125953
thenable = previous;
59135954
}
@@ -8009,7 +8050,7 @@ if (__DEV__) {
80098050
}
80108051
}
80118052

8012-
function warnIfAsyncClientComponent(Component, componentDoesIncludeHooks) {
8053+
function warnIfAsyncClientComponent(Component) {
80138054
{
80148055
// This dev-only check only works for detecting native async functions,
80158056
// not transpiled ones. There's also a prod check that we use to prevent
@@ -8021,43 +8062,20 @@ if (__DEV__) {
80218062
"[object AsyncFunction]";
80228063

80238064
if (isAsyncFunction) {
8024-
// Encountered an async Client Component. This is not yet supported,
8025-
// except in certain constrained cases, like during a route navigation.
8065+
// Encountered an async Client Component. This is not yet supported.
80268066
var componentName = getComponentNameFromFiber(
80278067
currentlyRenderingFiber$1
80288068
);
80298069

80308070
if (!didWarnAboutAsyncClientComponent.has(componentName)) {
8031-
didWarnAboutAsyncClientComponent.add(componentName); // Check if this is a sync update. We use the "root" render lanes here
8032-
// because the "subtree" render lanes may include additional entangled
8033-
// lanes related to revealing previously hidden content.
8071+
didWarnAboutAsyncClientComponent.add(componentName);
80348072

8035-
var root = getWorkInProgressRoot();
8036-
var rootRenderLanes = getWorkInProgressRootRenderLanes();
8037-
8038-
if (root !== null && includesBlockingLane(root, rootRenderLanes)) {
8039-
error(
8040-
"async/await is not yet supported in Client Components, only " +
8041-
"Server Components. This error is often caused by accidentally " +
8042-
"adding `'use client'` to a module that was originally written " +
8043-
"for the server."
8044-
);
8045-
} else {
8046-
// This is a concurrent (Transition, Retry, etc) render. We don't
8047-
// warn in these cases.
8048-
//
8049-
// However, Async Components are forbidden to include hooks, even
8050-
// during a transition, so let's check for that here.
8051-
//
8052-
// TODO: Add a corresponding warning to Server Components runtime.
8053-
if (componentDoesIncludeHooks) {
8054-
error(
8055-
"Hooks are not supported inside an async component. This " +
8056-
"error is often caused by accidentally adding `'use client'` " +
8057-
"to a module that was originally written for the server."
8058-
);
8059-
}
8060-
}
8073+
error(
8074+
"async/await is not yet supported in Client Components, only " +
8075+
"Server Components. This error is often caused by accidentally " +
8076+
"adding `'use client'` to a module that was originally written " +
8077+
"for the server."
8078+
);
80618079
}
80628080
}
80638081
}
@@ -8140,6 +8158,7 @@ if (__DEV__) {
81408158

81418159
ignorePreviousDependencies =
81428160
current !== null && current.type !== workInProgress.type;
8161+
warnIfAsyncClientComponent(Component);
81438162
}
81448163

81458164
workInProgress.memoizedState = null;
@@ -8232,16 +8251,13 @@ if (__DEV__) {
82328251
}
82338252
}
82348253

8235-
finishRenderingHooks(current, workInProgress, Component);
8254+
finishRenderingHooks(current, workInProgress);
82368255
return children;
82378256
}
82388257

82398258
function finishRenderingHooks(current, workInProgress, Component) {
82408259
{
82418260
workInProgress._debugHookTypes = hookTypesDev;
8242-
var componentDoesIncludeHooks =
8243-
workInProgressHook !== null || thenableIndexCounter !== 0;
8244-
warnIfAsyncClientComponent(Component, componentDoesIncludeHooks);
82458261
} // We can assume the previous dispatcher is always this one, since we set it
82468262
// at the beginning of the render phase and there's no re-entrance.
82478263

@@ -8365,7 +8381,7 @@ if (__DEV__) {
83658381
props,
83668382
secondArg
83678383
);
8368-
finishRenderingHooks(current, workInProgress, Component);
8384+
finishRenderingHooks(current, workInProgress);
83698385
return children;
83708386
}
83718387

0 commit comments

Comments
 (0)