addons/isl/src/usePromise.tsxblame
View source
b69ab311/**
b69ab312 * Copyright (c) Meta Platforms, Inc. and affiliates.
b69ab313 *
b69ab314 * This source code is licensed under the MIT license found in the
b69ab315 * LICENSE file in the root directory of this source tree.
b69ab316 */
b69ab317
b69ab318import {isPromise} from 'shared/utils';
b69ab319
b69ab3110/**
b69ab3111 * Return the promise result, raise the promise error, or "suspend" React's rendering.
b69ab3112 * The callsite should have `<Suspense>` and `<ErrorBoundary>` in a parent component
b69ab3113 * to support suspension and error handling, or use `<SuspenseBoundary>`.
b69ab3114 *
b69ab3115 * Be aware that the function that produces `promise` should returning
b69ab3116 * `Promise.resolve(data)`, and instead return `data` directly. This is because
b69ab3117 * Javascript does not provide a way to test if a Promise is resolved without async.
b69ab3118 * So the Promise will be treated as "pending" temporarily, rendering the Suspense
b69ab3119 * fallback. The actual Suspense children will lose their states because the
b69ab3120 * fallback replaces them. See the `maybePromise` below.
b69ab3121 *
b69ab3122 * Example:
b69ab3123 *
b69ab3124 * ```
b69ab3125 * // Parent component with `<Suspense>`.
b69ab3126 * function Container(props: {path: string}) {
b69ab3127 * return <SuspenseBoundary><Inner /></SuspenseBoundary>;
b69ab3128 * }
b69ab3129 *
b69ab3130 * // Child component using `usePromise`.
b69ab3131 * function Inner() {
b69ab3132 * const data = usePromise(maybePromise());
b69ab3133 * return <Data data={data} />;
b69ab3134 * }
b69ab3135 *
b69ab3136 * function maybePromise(): Data | Promise<Data> {
b69ab3137 * if (isDataReady()) {
b69ab3138 * // Do not return Promise.resolve(data). That loses <Inner /> state.
b69ab3139 * return data;
b69ab3140 * }
b69ab3141 * ...
b69ab3142 * }
b69ab3143 * ```
b69ab3144 *
b69ab3145 * Alternatively, the promise can be passed from a more "stable" stateful
b69ab3146 * parent component that keeps the promise object unchanged when <Suspense>
b69ab3147 * switches between fallback to non-fallback:
b69ab3148 *
b69ab3149 * ```
b69ab3150 * // Parent component with `<Suspense>`.
b69ab3151 * function Container(props: {path: string}) {
b69ab3152 * const loader = useLoader();
b69ab3153 * const promise = loader.load(props.path);
b69ab3154 * return <SuspenseBoundary><Inner promise={promise} /></SuspenseBoundary>;
b69ab3155 * }
b69ab3156 *
b69ab3157 * // Child component using `usePromise`.
b69ab3158 * function Inner(props: {promise: ...}) {
b69ab3159 * // The promise is from the parent of <Suspense />.
b69ab3160 * const data = usePromise(promise);
b69ab3161 * return <Data data={data} />;
b69ab3162 * }
b69ab3163 * ```
b69ab3164 *
b69ab3165 * See also https://github.com/reactjs/react.dev/blob/3364c93feb358a7d1ac2e8d8b0468c3e32214062/src/content/reference/react/Suspense.md?plain=1#L141
b69ab3166 */
b69ab3167export function usePromise<T>(promise: T | PromiseExt<T>): T {
b69ab3168 if (!isPromise(promise)) {
b69ab3169 return promise;
b69ab3170 }
b69ab3171 const status = promise.usePromiseStatus;
b69ab3172 if (status === undefined) {
b69ab3173 promise.usePromiseStatus = 'pending';
b69ab3174 promise.then(
b69ab3175 resolve => {
b69ab3176 promise.usePromiseStatus = ['ok', resolve];
b69ab3177 },
b69ab3178 error => {
b69ab3179 promise.usePromiseStatus = ['error', error];
b69ab3180 },
b69ab3181 );
b69ab3182 // This is the undocumented API to make <Suspense /> render its fallback.
b69ab3183 // React might change it in the future. But it has been like this for years.
b69ab3184 throw promise;
b69ab3185 } else if (status === 'pending') {
b69ab3186 throw promise;
b69ab3187 } else if (status[0] === 'ok') {
b69ab3188 return status[1];
b69ab3189 } else {
b69ab3190 throw status[1];
b69ab3191 }
b69ab3192}
b69ab3193
b69ab3194export interface PromiseExt<T> extends Promise<T> {
b69ab3195 usePromiseStatus?: ['ok', T] | ['error', Error] | 'pending';
b69ab3196}