main
js 177 lines 6.89 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @flow
8 */
9
10 // Corresponds to ReactFiberWakeable and ReactFizzWakeable modules. Generally,
11 // changes to one module should be reflected in the others.
12
13 // TODO: Rename this module and the corresponding Fiber one to "Thenable"
14 // instead of "Wakeable". Or some other more appropriate name.
15
16 import type {
17 Thenable,
18 PendingThenable,
19 FulfilledThenable,
20 RejectedThenable,
21 } from 'shared/ReactTypes';
22
23 import {enableAsyncDebugInfo} from 'shared/ReactFeatureFlags';
24
25 import noop from 'shared/noop';
26
27 export type ThenableState = Array<Thenable<any>>;
28
29 // An error that is thrown (e.g. by `use`) to trigger Suspense. If we
30 // detect this is caught by userspace, we'll log a warning in development.
31 export const SuspenseException: mixed = new Error(
32 "Suspense Exception: This is not a real error! It's an implementation " +
33 'detail of `use` to interrupt the current render. You must either ' +
34 'rethrow it immediately, or move the `use` call outside of the ' +
35 '`try/catch` block. Capturing without rethrowing will lead to ' +
36 'unexpected behavior.\n\n' +
37 'To handle async errors, wrap your component in an error boundary, or ' +
38 "call the promise's `.catch` method and pass the result to `use`.",
39 );
40
41 export function createThenableState(): ThenableState {
42 // The ThenableState is created the first time a component suspends. If it
43 // suspends again, we'll reuse the same state.
44 return [];
45 }
46
47 export function trackUsedThenable<T>(
48 thenableState: ThenableState,
49 thenable: Thenable<T>,
50 index: number,
51 ): T {
52 const previous = thenableState[index];
53 if (previous === undefined) {
54 thenableState.push(thenable);
55 if (__DEV__ && enableAsyncDebugInfo) {
56 const stacks: Array<Error> =
57 (thenableState as any)._stacks || ((thenableState as any)._stacks = []);
58 stacks.push(new Error());
59 }
60 } else {
61 if (previous !== thenable) {
62 // Reuse the previous thenable, and drop the new one. We can assume
63 // they represent the same value, because components are idempotent.
64
65 // Avoid an unhandled rejection errors for the Promises that we'll
66 // intentionally ignore.
67 thenable.then(noop, noop);
68 thenable = previous;
69 }
70 }
71
72 // We use an expando to track the status and result of a thenable so that we
73 // can synchronously unwrap the value. Think of this as an extension of the
74 // Promise API, or a custom interface that is a superset of Thenable.
75 //
76 // If the thenable doesn't have a status, set it to "pending" and attach
77 // a listener that will update its status and result when it resolves.
78 switch (thenable.status) {
79 case 'fulfilled': {
80 // This could be a bad instrumentation that doesn't set .value.
81 // We're not type-checking since this is a hot path where you can
82 // track down easily when something becomes `undefined` unexpectedly.
83 const fulfilledValue: T = thenable.value;
84 return fulfilledValue;
85 }
86 case 'rejected': {
87 const rejectedError = thenable.reason;
88
89 // Rejected Promises are rarer so we're doing an extra type-check in
90 // case of a bad instrumentation that doesn't set .reason
91 // If we end up throwing `undefined` it becomes hard to track down
92 // where that throw originated because no callstack would exist.
93 // React would still have a Component stack but that could only be used
94 // as an approximation.
95 if (rejectedError === undefined && !('reason' in thenable)) {
96 throw new Error(
97 'A rejected Promise was passed to React without a `reason` property. ' +
98 'React threw a generic error from where the Promise was used to assist in identifying the problematic Promise. ' +
99 "Make sure that instrumented Promises correctly set the `reason` property when setting `status` to `'rejected'`.",
100 );
101 }
102
103 throw rejectedError;
104 }
105 default: {
106 if (typeof thenable.status === 'string') {
107 // Only instrument the thenable if the status if not defined. If
108 // it's defined, but an unknown value, assume it's been instrumented by
109 // some custom userspace implementation. We treat it as "pending".
110 // Attach a dummy listener, to ensure that any lazy initialization can
111 // happen. Flight lazily parses JSON when the value is actually awaited.
112 thenable.then(noop, noop);
113 } else {
114 const pendingThenable: PendingThenable<T> = thenable as any;
115 pendingThenable.status = 'pending';
116 pendingThenable.then(
117 fulfilledValue => {
118 if (thenable.status === 'pending') {
119 const fulfilledThenable: FulfilledThenable<T> = thenable as any;
120 fulfilledThenable.status = 'fulfilled';
121 fulfilledThenable.value = fulfilledValue;
122 }
123 },
124 (error: mixed) => {
125 if (thenable.status === 'pending') {
126 const rejectedThenable: RejectedThenable<T> = thenable as any;
127 rejectedThenable.status = 'rejected';
128 rejectedThenable.reason = error;
129 }
130 },
131 );
132 }
133
134 // Check one more time in case the thenable resolved synchronously
135 switch ((thenable as Thenable<T>).status) {
136 case 'fulfilled': {
137 const fulfilledThenable: FulfilledThenable<T> = thenable as any;
138 return fulfilledThenable.value;
139 }
140 case 'rejected': {
141 const rejectedThenable: RejectedThenable<T> = thenable as any;
142 throw rejectedThenable.reason;
143 }
144 }
145
146 // Suspend.
147 //
148 // Throwing here is an implementation detail that allows us to unwind the
149 // call stack. But we shouldn't allow it to leak into userspace. Throw an
150 // opaque placeholder value instead of the actual thenable. If it doesn't
151 // get captured by the work loop, log a warning, because that means
152 // something in userspace must have caught it.
153 suspendedThenable = thenable;
154 throw SuspenseException;
155 }
156 }
157 }
158
159 // This is used to track the actual thenable that suspended so it can be
160 // passed to the rest of the Suspense implementation — which, for historical
161 // reasons, expects to receive a thenable.
162 let suspendedThenable: Thenable<any> | null = null;
163 export function getSuspendedThenable(): Thenable<mixed> {
164 // This is called right after `use` suspends by throwing an exception. `use`
165 // throws an opaque value instead of the thenable itself so that it can't be
166 // caught in userspace. Then the work loop accesses the actual thenable using
167 // this function.
168 if (suspendedThenable === null) {
169 throw new Error(
170 'Expected a suspended thenable. This is a bug in React. Please file ' +
171 'an issue.',
172 );
173 }
174 const thenable = suspendedThenable;
175 suspendedThenable = null;
176 return thenable;
177 }