A React 19 component that enhances React's Suspense with optional features like promise resolution handling, error handling, caching, and more.
npm install react-enhanced-suspenseSuspense with optional features like promise resolved values handling (onSuccess), error handling (onError), retry functionality of failing promises (retry), caching (cache), and timeout fallbacks (timeouts).
Suspense (only fallback and/or children props)
children in React's Suspense
onSuccess
Context Example
onError (\*RCC Only)
retry (\*RCC Only)
cache and resourceId
timeouts And timeoutFallbacks
children, The Resource Prop
resourceId Prop
cacheAPI
bash
npm i react-enhanced-suspense
`
Usage
You can import the component in two ways:
- Named import: Use this if you prefer explicit names:
`typescript
import { EnhancedSuspense } from "react-enhanced-suspense";
`
- Default import: Use this for a shorter, more familiar syntax (like React’s Suspense):
`typescript
import Suspense from "react-enhanced-suspense";
`
Both are the same component under the hood. Pick the one that suits your project!
This component can be used as a substitute for React's Suspense.
Key Features
- Promise Resolution Handling: Use onSuccess to transform resolved promise or React Context values.
- (\*)Error Handling: Use onError to wrap React's Suspense in an ErrorBoundary for custom error rendering (RCC only).
- (\*)Retry Functionality: Automatically retry failed promises with retry, configurable with retryCount, retryDelay, retryBackoff, and onRetryFallback (RCC only).
- Caching: Store promise results in memory or localStorage (or any custom storage) with cache, resourceId, cacheTTL, cacheVersion, and cachePersist.
- Timeout Fallbacks: Update the fallback UI dynamically with timeouts and timeoutFallbacks for long-running operations.
- React's Suspense: This component is React's Suspense when only fallback or children props are used.
- TypeScript Support: Fixes TypeScript errors when using React Context with Suspense, unlike React’s native Suspense.
(\*): These props can only be used in React Client Components.
React's
Suspense (only fallback and/or children props)
`typescript
import Suspense from "react-enhanced-suspense";
export default function Component({ promise }: { promise: Promise }) {
return (
<>
Hey
{promise}
>
);
}
`
Can be used either in RCC or RSC.
$3
EnhancedSuspense fixes a Typescript error that appears when using React Context as children in React's Suspense:
`typescript
"use client";
import Suspense from "react-enhanced-suspense";
import { createContext, Suspense as ReactSuspense } from "react";
const MyContext = createContext("Default value");
export default function ShowContext() {
return (
<>
{MyContext} // <-- No Typescript error
{MyContext} // <-- Typescript error: Type 'Context' is not assignable to type 'ReactNode'.
>
);
}
`
Promise Resolution Handling With
onSuccess
`typescript
import Suspense from "react-enhanced-suspense";
export default function Component({ promise }: { promise: Promise }) {
return (
<>
Hey
fallback="Loading..."
onSuccess={(data) => data.map((item) => {item})}
>
{promise}
>
);
}
`
Can be used either in RCC or RSC.
$3
EnhancedSuspense also supports React Context as a Usable resource (children prop):
`typescript
"use client";
import Suspense from "react-enhanced-suspense";
import { createContext } from "react";
const MyContext = createContext("Default value");
export default function Context() {
return (
Context value: {value}}>
{MyContext}
);
}
`
This renders Context value: Default value.
Error Handling With
onError (\*RCC Only)
`typescript
"use client";
import Suspense from "react-enhanced-suspense";
export default function Component() {
return (
{error.message}}>
{
new Promise((resolve, reject) =>
setTimeout(() => reject("Failed"), 1000)
)
}
);
}
`
The onError prop wraps React's Suspense in an ErrorBoundary component for custom error rendering. It always expects an Error object, no matter what the rejected value of the promise is.
Can only be used in RCC.
Retry Functionality With
retry (\*RCC Only)
With EnhancedSuspense you can automatically retry failed promises with configurable options:
`typescript
"use client";
import Suspense from "react-enhanced-suspense";
export default function Component() {
return (
<>
Hey
retry
retryCount={10} // number of retries; default: 1
retryDelay={500} // delay between retries in ms; default: 0
retryBackoff="linear" // backoff for retries; default: undefined
onRetryFallback={(attempt) => {Retrying ${attempt}...}} // fallback to show in each retry attempt; default: undefined
fallback="Loading..." // fallback to show on first attempt
>
{() =>
new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() > 0.7) {
resolve(["Roger", "Alex"]);
} else {
reject("Fail on data fetching");
}
}, 1000);
})
}
>
);
}
`
When using retry, the resource (the children prop) should be a function returning a promise, rather than a promise itself. If not, the retries will be on the same initially rejected promise and would have no effect.
retryBackoff admits the following values: linear, exponential, or a function with signature (attemptIndex:number, retryDelay:number)=>number.
Works only in RCC.
Caching With
cache and resourceId
Cache promise results with cache and resourceId props:
`typescript
import Suspense from "react-enhanced-suspense";
export default function Component() {
return (
<>
Hey
cache
cacheTTL={2000} // expiration time in ms for the cache; default: undefined
cacheVersion={1} // number, when this changes invalidates cache; default: undefined
cachePersist // whether to persist or not cache in localStorage; default: false
resourceId="my-promise" // the id of the resource; without it the cache takes no effect
>
{
new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() > 0.2) {
resolve(["Roger", "Alex"]);
} else {
reject("Fail on data fetching");
}
}, 1000);
})
}
>
);
}
`
Can be used either in RCC or RSC.
Timeout Fallbacks With
timeouts And timeoutFallbacks
Update fallback UI for long-running operations:
`typescript
import Suspense from "react-enhanced-suspense";
const VERY_LONG_TIME = 15000;
export default function Component() {
return (
fallback="Loading..."
timeouts={[3000, 6000, 10000]}
timeoutFallbacks={[
"Still working...",
"Taking longer...",
Almost there...,
]}
>
{
new Promise((resolve) =>
setTimeout(() => resolve("Done"), VERY_LONG_TIME)
)
}
);
}
`
Can be used either in the RCC or RSC.
All Props Can Be Used Together In RCC (React Client Components)
`typescript
"use client";
import Suspense from "react-enhanced-suspense";
export default function Component() {
return (
retry
retryCount={15}
onRetryFallback={(attempt) => {Retry ${attempt}...}}
cache
cacheTTL={2000}
resourceId="my-promise"
onError={(error) => {error.message}}
onSuccess={(value) => value.map((item) => {item})}
fallback="Loading..."
timeouts={[1000, 2000, 3000]}
timeoutFallbacks={["Three...", "Two...", "One..."]}
>
{() =>
new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() > 0.9) {
resolve(["Roger", "Alex"]);
} else {
reject("Fail on data fetching");
}
}, 4000);
})
}
);
}
`
onRetryFallback takes precedence over timeouts and timeoutFallbacks. In the component above, from 0 to 1s, Loading... will render. From 1s to 2s, Three...; from 2s to 3s, Two...; from 3s to 4s, One.... Then, if promise is rejected, Retry 1... will render for 4s, then, if rejected again, Retry 2..., and so on, until promise is resolved or the number of retries are 15.
As always, when retry is used, children, the resource, should be a function returning a promise.
Invalid Combination Of Props In RSC (React Server Components)
When used in a RSC, there are certain combination of props which are not allowed. This happens when the component itself (EnhancedSuspense) is a RCC and has props which are functions. For example, resourceId turns EnhancedSuspense into a Client Component. If we use then onSuccess, this is not allowed in a RSC, because functions are not serializable.
`typescript
// In a RSC
{
/ resourceId + onSuccess: ❌ BAD (EnhancedSuspense is a RCC + Function) /
}
{data}}>
{promise}
;
`
When this happens, that EnhancedSuspense is a RCC and is used in a RSC passing some prop as a function, the app will crash.
Other props that turns the EnhancedSuspense into a RCC, apart from resourceId, are timeouts, onError, retry, cache, and when children (the resource) is a function. So when using onError, or children as a function, the EnhancedSuspense cannot be used in a RSC (because it's a RCC with props as functions).
But a RCC can be used in a RSC when no props are functions. For example, this is allowed in a RSC:
`typescript
// In a RSC
{
/ timeouts: ✅ EnhancedSuspense is a RCC, no props as functions /
}
fallback="Loading..."
timeouts={[3000, 6000, 10000]}
timeoutFallbacks={[
Timeout 1,
Timeout 2,
Timeout 3,
]}
>
{promise}
;
`
onSuccess doesn't turn EnhancedSuspense into a RCC. So, despite it is a function, it can be used in a RSC:
`typescript
// In a RSC
{
/ onSuccess: ✅ EnhancedSuspense is a RSC /
}
{data}}>
{promise}
;
`
cache can also be used in a RSC if children is not a function (remember that cache only works with resourceId):
`typescript
// In a RSC
{
/ cache + resourceId: ✅ EnhancedSuspense is a RCC, no props as functions /
}
{promise}
;
`
Because retry, in order to work properly, requires children to be a function, cannot be used in a RSC, and only in a RCC.
$3
| Props | EnhancedSuspense is a RCC | Allowed in a RSC |
| ------------------------- | ------------------------- | ----------------------------------------------------- |
| children as a function | ✅ | ❌ |
| onError | ✅ | ❌ |
| timeouts | ✅ | ✅ if no props as function (e.g. ❌ with onSuccess) |
| resourceId | ✅ | ✅ if no props as function (e.g. ❌ with onSuccess) |
| retry | ✅ | ❌ because used with children as a function |
| cache | ✅ | ✅ if no props as function (e.g. ❌ with onSuccess) |
| onSuccess | ❌ | ✅ |
| fallback | ❌ | ✅ |
| children not a function | ❌ | ✅ |
In a RCC all props are allowed.
children, The Resource Prop
children is the resource. It can be either a ReactNode, a Usable (which is a promise or a React Context), or a function returning a promise. When used with onSuccess, it can be a Usable or a function returning a promise. When used without onSuccess, it can be a ReactNode, a Usable that resolves to a ReactNode, or a function returning a promise that resolves to a ReactNode.
The
resourceId Prop
As has been stated, resourceId turns EnhancedSuspense into a RCC, and it's necessary for cache to take effect (it's the cache key for storing the value into memory or localStorage or custom storage). resourceId it's a string. Apart from serving as a cache key when cache is used, it is also useful when the resource, the children prop, changes dynamically. When resourceId changes, the evaluation of the resource is cancelled and a new evaluation starts. That is, a change in resourceId triggers a new evaluation of the resource, cancelling any pending evaluations.
React's Suspense evaluates the resource in every render if the resource is not memoized and it is not a state variable, that is, if it changes in every render. In EnhancedSuspense you can use resourceId prop to memoize the resource, so it is stable between rerenders. Therefore, if you want to reevalute a resource, you must supply a resourceId and change it.
`typescript
"use client";
import Suspense from "react-enhanced-suspense";
import { useState, useEffect } from "react";
export default function Component() {
const [resource, setResource] = useState(
() =>
new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() > 0.002) {
resolve(["Roger", "Alex"]);
} else {
reject("Fail on data fetching");
}
}, 2000);
})
);
const [resourceId, setResourceId] = useState("first");
const [isMounted, setIsMounted] = useState(false);
useEffect(() => {
setIsMounted(true);
}, []);
useEffect(() => {
if (!isMounted) return;
setResourceId("second");
}, [resource]);
return (
<>
onClick={() =>
setResource(
new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() > 0.002) {
resolve(["Alex", "Roger"]);
} else {
reject("Fail on data fetching");
}
}, 2000);
})
)
}
>
change resource
fallback="Loading..."
onSuccess={(values) =>
values.map((item) => {item})
}
// Reevaluates the resource when pressing the button (no resourceId prop)
>
{resource}
fallback="Loading..."
resourceId="fixed"
onSuccess={(values) =>
values.map((item) => {item})
}
// DON'T Reevaluate the resource when pressing button (resourceId constant)
>
{resource}
fallback="Loading..."
resourceId={resourceId}
onSuccess={(values) =>
values.map((item) => {item})
}
// Reevaluate the resource when pressing button (resourceId changes)
>
{resource}
>
);
}
`
The correct way to implement it would be (a more realistic scenario):
`typescript
"use client";
import Suspense from "react-enhanced-suspense";
import { useState, useEffect } from "react";
export default function Component() {
const [prop, setProp] = useState(true);
const resource = prop
? new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() > 0.002) {
resolve(["Roger", "Alex"]);
} else {
reject("Fail on data fetching");
}
}, 2000);
})
: new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() > 0.002) {
resolve(["Alex", "Roger"]);
} else {
reject("Fail on data fetching");
}
}, 2000);
});
const [resourceId, setResourceId] = useState( my-promise-${prop});
useEffect(() => {
setResourceId(my-promise-${prop});
}, [prop]);
return (
<>
fallback="Loading..."
// Reevaluates the resource on EVERY RENDER
>
{resource}
fallback="Loading..."
resourceId="fixed"
// DON'T Reevaluate the resource on every render, NEITHER when the button is pressed.
>
{resource}
fallback="Loading..."
resourceId={resourceId}
// DON'T Reevaluate the resource on every render, only WHEN the button is pressed.
>
{resource}
>
);
}
`
The
cacheAPI
Together with the component EnhancedSuspense, a cacheAPI is also exported:
`typescript
const cacheAPI: Readonly<{
setCacheSizeLimit(bytes: number | null, entries?: number | null): void;
setCustomStorage(storage: CustomCacheStorage | null): void;
cleanupCache(): void;
startAutomaticCacheCleanup(intervalMs?: number): void;
stopAutomaticCacheCleanup(): void;
getCacheStatus(): {
isCustomStorage: boolean;
entryCount: number | undefined;
persistentCount: number | undefined;
expirationCount: number | undefined;
isCleanupActive: boolean;
};
clearCache(options?: { clearLocalStorage?: boolean }): void;
}>;
`
cleanupCache() removes from cache all expired entries. clearCache() clears all the cache. setCustomStorage sets a custom storage for the cache, where:
`typescript
type CustomCacheStorage = {
get(key: string): CacheEntry | null;
set(key: string, value: any, ttl?: number, persist?: boolean): void;
delete(key: string): void;
cleanup?(): void;
clear?(): void;
status?():
| {
entryCount?: number;
persistentCount?: number;
expirationCount?: number;
}
| undefined;
};
`
and:
`typescript
type CacheEntry = {
value: any;
expiry?: number | undefined;
};
`
These two types, CustomCacheStorage and CacheEntry are also exported by the package.
Props Reference
All props are optional. All props can be used together in a RCC.
| Prop | Description | Example | EnhancedSuspense is a RCC | Allowed in a RSC |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ | ----------------------------------------------------- |
| onSuccess | A function that takes the resolved value of a resource (promise or React Context) and returns a ReactNode. When used, children must be a Usable (promise or React Context) or a function returning a promise. | | ❌ | ✅ |
| onError | A function that takes an Error and returns a ReactNode | | ✅ | ❌ |
| children | A function that returns a promise, a ReactNode, or a Usable (promise or React Context). Specifies the resource or content that suspenses | | ✅ when is a function ❌ rest of cases | ❌ when is a function ✅ rest of cases |
| fallback | A ReactNode | | ❌ | ✅ |
| retry | A boolean that indicates whether to retry failing promises or not (default false). When used, the resource should be a function returning a promise. | | ✅ | ❌ because of used with children as a function |
| retryCount | The number of retries to try when using retry (defaults to 1). Only applies when retry is used | | ✅ because of used with retry | ❌ because of used with children as a function |
| retryDelay | The number of ms to apply (wait) between each retry (defaults to 0). Only applies when retry is used | | ✅ because of used with retry | ❌ because of used with children as a function |
| retryBackoff | Specifies the backoff strategy to apply for retries (defaults undefined). Can be "linear", "exponential", or a function with signature (attemptIndex: number, retryDelay: number) => number. Only applies when retry is used. | | ✅ because of used with retry | ❌ because of used with children as a function |
| onRetryFallback | A function (with signature (retryAttempt: number) => ReactNode) that specifies the fallback UI to be shown on each retry attempt. Only applies when retry is used. | | ✅ because of used with retry | ❌ because of used with children as a function |
| resourceId | A string that identifies the resource being used. When used with cache serves as the cache key. Stabilizes the resource between rerenders. | | ✅ | ✅ if no props as function (e.g. ❌ with onSuccess) |
| cache | A boolean that indicates whether to use the cache functionality or not. In order to work needs to be accompanied by resourceId, that serves as the cache key. | | ✅ | ✅ if no props as function (e.g. ❌ with onSuccess) |
| cacheTTL | A number that sets an expiration time in milliseconds for the cached value. Only applies when cache and resourceId are used. | | ✅ because of used with cache and resourceId | ✅ if no props as function (e.g. ❌ with onSuccess) |
| cacheVersion | A number. Invalidates previous cached value when it's increased or decreased (changed). Only applies when cache and resourceId are used. | | ✅ because of used with cache and resourceId | ✅ if no props as function (e.g. ❌ with onSuccess) |
| cachePersist | A boolean (defaults to false) that indicates whether to persist the cached value into localStorage (or custom storage) or not. Only applies when cache and resourceId are used. | | ✅ because of used with cache and resourceId | ✅ if no props as function (e.g. ❌ with onSuccess) |
| timeouts | A number[]. Timeouts in milliseconds to update fallback UI shown (onRetryFallback takes precedence). Only makes sense if timeoutFallbacks is used. | | ✅ | ✅ if no props as function (e.g. ❌ with onSuccess) |
| timeoutFallbacks | A ReactNode[]. Fallback UIs to show after each timeout specified in timeouts. Only makes sense if timeouts is not an empty array. | | ✅ Because used with timeouts | ✅ if no props as function (e.g. ❌ with onSuccess`) |