From ace8497ca6777fb41c64d68e7366f16fcbdb1342 Mon Sep 17 00:00:00 2001 From: Stan Wohlwend Date: Sun, 16 Jun 2024 15:55:37 +0200 Subject: [PATCH] Reduce occurence of "A component was suspended by an uncached promise" --- packages/stack-shared/src/utils/promises.tsx | 28 ++++++++++++++++---- packages/stack/src/lib/stack-app.ts | 6 ++++- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/packages/stack-shared/src/utils/promises.tsx b/packages/stack-shared/src/utils/promises.tsx index 71f61d50f..6bc595246 100644 --- a/packages/stack-shared/src/utils/promises.tsx +++ b/packages/stack-shared/src/utils/promises.tsx @@ -1,4 +1,5 @@ import { StackAssertionError, captureError } from "./errors"; +import { DependenciesMap } from "./maps"; import { Result } from "./results"; import { generateUuid } from "./uuids"; @@ -38,28 +39,45 @@ export function createPromise(callback: (resolve: Resolve, reject: Reject) } as any); } +const resolvedCache = new DependenciesMap<[unknown], ReactPromise>(); /** - * Like Promise.resolve(...), but also adds the status and value properties for use with React's `use` hook. + * Like Promise.resolve(...), but also adds the status and value properties for use with React's `use` hook, and caches + * the value so that invoking `resolved` twice returns the same promise. */ export function resolved(value: T): ReactPromise { - return Object.assign(Promise.resolve(value), { + if (resolvedCache.has([value])) { + return resolvedCache.get([value]) as ReactPromise; + } + + const res = Object.assign(Promise.resolve(value), { status: "fulfilled", value, } as const); + resolvedCache.set([value], res); + return res; } +const rejectedCache = new DependenciesMap<[unknown], ReactPromise>(); /** - * Like Promise.resolve(...), but also adds the status and value properties for use with React's `use` hook. + * Like Promise.reject(...), but also adds the status and value properties for use with React's `use` hook, and caches + * the value so that invoking `rejected` twice returns the same promise. */ export function rejected(reason: unknown): ReactPromise { - return Object.assign(Promise.reject(reason), { + if (rejectedCache.has([reason])) { + return rejectedCache.get([reason]) as ReactPromise; + } + + const res = Object.assign(Promise.reject(reason), { status: "rejected", reason: reason, } as const); + rejectedCache.set([reason], res); + return res; } +const neverResolvePromise = pending(new Promise(() => {})); export function neverResolve(): ReactPromise { - return pending(new Promise(() => {})); + return neverResolvePromise; } export function pending(promise: Promise, options: { disableErrorWrapping?: boolean } = {}): ReactPromise { diff --git a/packages/stack/src/lib/stack-app.ts b/packages/stack/src/lib/stack-app.ts index 006390a7c..fa7ef2d7c 100644 --- a/packages/stack/src/lib/stack-app.ts +++ b/packages/stack/src/lib/stack-app.ts @@ -198,7 +198,11 @@ function useAsyncCache(cache: AsyncCache, dependencies // note: we must use React.useSyncExternalStore instead of importing the function directly, as it will otherwise // throw an error ("can't import useSyncExternalStore from the server") - const value = React.useSyncExternalStore(subscribe, getSnapshot, getSnapshot); + const value = React.useSyncExternalStore( + subscribe, + getSnapshot, + () => throwErr(new Error("getServerSnapshot should never be called in useAsyncCache because we restrict to CSR earlier")) + ); if (value === loadingSentinel) { return use(cache.getOrWait(dependencies, "read-write"));