Skip to content

Commit ace8497

Browse files
committed
Reduce occurence of "A component was suspended by an uncached promise"
1 parent 5cf167b commit ace8497

File tree

2 files changed

+28
-6
lines changed

2 files changed

+28
-6
lines changed

packages/stack-shared/src/utils/promises.tsx

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { StackAssertionError, captureError } from "./errors";
2+
import { DependenciesMap } from "./maps";
23
import { Result } from "./results";
34
import { generateUuid } from "./uuids";
45

@@ -38,28 +39,45 @@ export function createPromise<T>(callback: (resolve: Resolve<T>, reject: Reject)
3839
} as any);
3940
}
4041

42+
const resolvedCache = new DependenciesMap<[unknown], ReactPromise<unknown>>();
4143
/**
42-
* Like Promise.resolve(...), but also adds the status and value properties for use with React's `use` hook.
44+
* Like Promise.resolve(...), but also adds the status and value properties for use with React's `use` hook, and caches
45+
* the value so that invoking `resolved` twice returns the same promise.
4346
*/
4447
export function resolved<T>(value: T): ReactPromise<T> {
45-
return Object.assign(Promise.resolve(value), {
48+
if (resolvedCache.has([value])) {
49+
return resolvedCache.get([value]) as ReactPromise<T>;
50+
}
51+
52+
const res = Object.assign(Promise.resolve(value), {
4653
status: "fulfilled",
4754
value,
4855
} as const);
56+
resolvedCache.set([value], res);
57+
return res;
4958
}
5059

60+
const rejectedCache = new DependenciesMap<[unknown], ReactPromise<unknown>>();
5161
/**
52-
* Like Promise.resolve(...), but also adds the status and value properties for use with React's `use` hook.
62+
* Like Promise.reject(...), but also adds the status and value properties for use with React's `use` hook, and caches
63+
* the value so that invoking `rejected` twice returns the same promise.
5364
*/
5465
export function rejected<T>(reason: unknown): ReactPromise<T> {
55-
return Object.assign(Promise.reject(reason), {
66+
if (rejectedCache.has([reason])) {
67+
return rejectedCache.get([reason]) as ReactPromise<T>;
68+
}
69+
70+
const res = Object.assign(Promise.reject(reason), {
5671
status: "rejected",
5772
reason: reason,
5873
} as const);
74+
rejectedCache.set([reason], res);
75+
return res;
5976
}
6077

78+
const neverResolvePromise = pending(new Promise<never>(() => {}));
6179
export function neverResolve(): ReactPromise<never> {
62-
return pending(new Promise<never>(() => {}));
80+
return neverResolvePromise;
6381
}
6482

6583
export function pending<T>(promise: Promise<T>, options: { disableErrorWrapping?: boolean } = {}): ReactPromise<T> {

packages/stack/src/lib/stack-app.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,11 @@ function useAsyncCache<D extends any[], T>(cache: AsyncCache<D, T>, dependencies
198198

199199
// note: we must use React.useSyncExternalStore instead of importing the function directly, as it will otherwise
200200
// throw an error ("can't import useSyncExternalStore from the server")
201-
const value = React.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
201+
const value = React.useSyncExternalStore(
202+
subscribe,
203+
getSnapshot,
204+
() => throwErr(new Error("getServerSnapshot should never be called in useAsyncCache because we restrict to CSR earlier"))
205+
);
202206

203207
if (value === loadingSentinel) {
204208
return use(cache.getOrWait(dependencies, "read-write"));

0 commit comments

Comments
 (0)