19
20
export opaque type ThenableState = Array<Thenable<any>>;
21
22
+// An error that is thrown (e.g. by `use`) to trigger Suspense. If we
23
+// detect this is caught by userspace, we'll log a warning in development.
24
+export const SuspenseException: mixed = new Error(
25
+ "Suspense Exception: This is not a real error! It's an implementation " +
26
+ 'detail of `use` to interrupt the current render. You must either ' +
27
+ 'rethrow it immediately, or move the `use` call outside of the ' +
28
+ '`try/catch` block. Capturing without rethrowing will lead to ' +
29
+ 'unexpected behavior.\n\n' +
30
+ 'To handle async errors, wrap your component in an error boundary, or ' +
31
+ "call the promise's `.catch` method and pass the result to `use`",
32
+);
33
+
34
let thenableState: ThenableState | null = null;
35
36
export function createThenableState(): ThenableState {
49
export function getThenableStateAfterSuspending(): ThenableState | null {
50
// Called by the work loop so it can stash the thenable state. It will use
51
// the state to replay the component when the promise resolves.
40
- if (
41
- thenableState !== null &&
42
- // If we only `use`-ed resolved promises, then there is no suspended state
43
- // TODO: The only reason we do this is to distinguish between throwing a
44
- // promise (old Suspense pattern) versus `use`-ing one. A better solution is
45
- // for `use` to throw a special, opaque value instead of a promise.
46
- !isThenableStateResolved(thenableState)
47
- ) {
48
- const state = thenableState;
49
- thenableState = null;
50
- return state;
51
- }
52
- return null;
52
+ const state = thenableState;
53
+ thenableState = null;
54
+ return state;
55
}
56
57
export function isThenableStateResolved(thenables: ThenableState): boolean {
131
}
132
133
// Suspend.
132
- // TODO: Throwing here is an implementation detail that allows us to
133
- // unwind the call stack. But we shouldn't allow it to leak into
134
- // userspace. Throw an opaque placeholder value instead of the
135
- // actual thenable. If it doesn't get captured by the work loop, log
136
- // a warning, because that means something in userspace must have
137
- // caught it.
138
- throw thenable;
134
+ //
135
+ // Throwing here is an implementation detail that allows us to unwind the
136
+ // call stack. But we shouldn't allow it to leak into userspace. Throw an
137
+ // opaque placeholder value instead of the actual thenable. If it doesn't
138
+ // get captured by the work loop, log a warning, because that means
139
+ // something in userspace must have caught it.
140
+ suspendedThenable = thenable;
141
+ if (__DEV__) {
142
+ needsToResetSuspendedThenableDEV = true;
143
+ }
144
+ throw SuspenseException;
145
+ }
146
+ }
147
+}
148
+
149
+// This is used to track the actual thenable that suspended so it can be
150
+// passed to the rest of the Suspense implementation — which, for historical
151
+// reasons, expects to receive a thenable.
152
+let suspendedThenable: Thenable<any> | null = null;
153
+let needsToResetSuspendedThenableDEV = false;
154
+export function getSuspendedThenable(): Thenable<mixed> {
155
+ // This is called right after `use` suspends by throwing an exception. `use`
156
+ // throws an opaque value instead of the thenable itself so that it can't be
157
+ // caught in userspace. Then the work loop accesses the actual thenable using
158
+ // this function.
159
+ if (suspendedThenable === null) {
160
+ throw new Error(
161
+ 'Expected a suspended thenable. This is a bug in React. Please file ' +
162
+ 'an issue.',
163
+ );
164
+ }
165
+ const thenable = suspendedThenable;
166
+ suspendedThenable = null;
167
+ if (__DEV__) {
168
+ needsToResetSuspendedThenableDEV = false;
169
+ }
170
+ return thenable;
171
+}
172
+
173
+export function checkIfUseWrappedInTryCatch(): boolean {
174
+ if (__DEV__) {
175
+ // This was set right before SuspenseException was thrown, and it should
176
+ // have been cleared when the exception was handled. If it wasn't,
177
+ // it must have been caught by userspace.
178
+ if (needsToResetSuspendedThenableDEV) {
179
+ needsToResetSuspendedThenableDEV = false;
180
+ return true;
181
}
182
}
183
+ return false;
184
}