3.1 KB97 lines
Blame
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
8import {isPromise} from 'shared/utils';
9
10/**
11 * Return the promise result, raise the promise error, or "suspend" React's rendering.
12 * The callsite should have `<Suspense>` and `<ErrorBoundary>` in a parent component
13 * to support suspension and error handling, or use `<SuspenseBoundary>`.
14 *
15 * Be aware that the function that produces `promise` should returning
16 * `Promise.resolve(data)`, and instead return `data` directly. This is because
17 * Javascript does not provide a way to test if a Promise is resolved without async.
18 * So the Promise will be treated as "pending" temporarily, rendering the Suspense
19 * fallback. The actual Suspense children will lose their states because the
20 * fallback replaces them. See the `maybePromise` below.
21 *
22 * Example:
23 *
24 * ```
25 * // Parent component with `<Suspense>`.
26 * function Container(props: {path: string}) {
27 * return <SuspenseBoundary><Inner /></SuspenseBoundary>;
28 * }
29 *
30 * // Child component using `usePromise`.
31 * function Inner() {
32 * const data = usePromise(maybePromise());
33 * return <Data data={data} />;
34 * }
35 *
36 * function maybePromise(): Data | Promise<Data> {
37 * if (isDataReady()) {
38 * // Do not return Promise.resolve(data). That loses <Inner /> state.
39 * return data;
40 * }
41 * ...
42 * }
43 * ```
44 *
45 * Alternatively, the promise can be passed from a more "stable" stateful
46 * parent component that keeps the promise object unchanged when <Suspense>
47 * switches between fallback to non-fallback:
48 *
49 * ```
50 * // Parent component with `<Suspense>`.
51 * function Container(props: {path: string}) {
52 * const loader = useLoader();
53 * const promise = loader.load(props.path);
54 * return <SuspenseBoundary><Inner promise={promise} /></SuspenseBoundary>;
55 * }
56 *
57 * // Child component using `usePromise`.
58 * function Inner(props: {promise: ...}) {
59 * // The promise is from the parent of <Suspense />.
60 * const data = usePromise(promise);
61 * return <Data data={data} />;
62 * }
63 * ```
64 *
65 * See also https://github.com/reactjs/react.dev/blob/3364c93feb358a7d1ac2e8d8b0468c3e32214062/src/content/reference/react/Suspense.md?plain=1#L141
66 */
67export function usePromise<T>(promise: T | PromiseExt<T>): T {
68 if (!isPromise(promise)) {
69 return promise;
70 }
71 const status = promise.usePromiseStatus;
72 if (status === undefined) {
73 promise.usePromiseStatus = 'pending';
74 promise.then(
75 resolve => {
76 promise.usePromiseStatus = ['ok', resolve];
77 },
78 error => {
79 promise.usePromiseStatus = ['error', error];
80 },
81 );
82 // This is the undocumented API to make <Suspense /> render its fallback.
83 // React might change it in the future. But it has been like this for years.
84 throw promise;
85 } else if (status === 'pending') {
86 throw promise;
87 } else if (status[0] === 'ok') {
88 return status[1];
89 } else {
90 throw status[1];
91 }
92}
93
94export interface PromiseExt<T> extends Promise<T> {
95 usePromiseStatus?: ['ok', T] | ['error', Error] | 'pending';
96}
97