mrkeyoor.com_
Wed 23 Sept 00:35 UTC
npmWeb Frontendupdated 22 Sept 2026

react-promise-suspense review

react-promise-suspense 0.3.4 exports one function named `usePromise`. During render it calls a Promise-returning loader, stores that work in a module-global array, and throws the pending Promise so React Suspense displays its fallback. A retry returns the stored value or throws the stored error. Cache lookup deep-compares only the input array, not the loader function. The current release made inputs optional and revised its typed example, but our installed package check found no TypeScript declarations. This is a small Suspense cache, not a general server-state client.

Verdict

react-promise-suspense 0.3.4 installed 2 packages in 0.6 seconds and bundled to 1.2 KB gzipped in our sandbox, but it had no types and keys its global cache without loader identity. Do not adopt it for new production data fetching; keep it only where small, client-only legacy use is already contained by tests.

We installed it

Lab card: what happened when we installed react-promise-suspenseScreenshot of react-promise-suspense documentation
Install✓ · 0.6s2 packages on disk · 1 MB
ImportESM import works · require() works · CommonJS package
Browser1.2 KBgzipped (2.7 KB minified), bundled with esbuild
Typesno TypeScript types found
Known vulns00 critical · 0 high · 0 moderate · 0 low (npm audit)

Answers from our run

Does react-promise-suspense install cleanly?

Yes. In a fresh container with an empty cache, npm install react-promise-suspense finished in 0.6s, leaving 2 packages and 1 MB on disk. npm audit reported no known vulnerabilities.

How much does react-promise-suspense add to a browser bundle?

1.2 KB gzipped (2.7 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.

Does react-promise-suspense work with both ESM and CommonJS?

Yes. Both import 'react-promise-suspense' and require('react-promise-suspense') worked in Node 22 in our run. The package is published as CommonJS.

Does react-promise-suspense include TypeScript types?

No type declarations were found in our install, so TypeScript users need their own declarations.

react-promise-suspense or @tanstack/react-query: which should you use?

@tanstack/react-query: Choose it for production server state with explicit keys, invalidation, retries, mutation, cancellation, and cache controls. react-promise-suspense 0.3.4 installed 2 packages in 0.6 seconds and bundled to 1.2 KB gzipped in our sandbox, but it had no types and keys its global cache without loader identity.

When should you not use react-promise-suspense?

You are choosing data fetching for a new production app. TanStack Query and SWR supply invalidation, retries, cancellation, mutations, and maintained React integration.

API stability2/5Version 0.3.4 has one exported call and only three arguments, which keeps the visible surface easy to learn. It remains below 1.0, and a correct fix for open issue 8 would add loader identity to cache lookup and change observable results. Requests for cache removal, size control, and custom storage also show that useful fixes would expand or alter the current contract rather than preserve it unchanged.
Docs2/5The README provides installation and one fetch example beneath Suspense. It does not explain the module-global cache, lifespan 0 retention, post-settlement eviction timing, cached errors, loader-blind collisions, SSR isolation, Error Boundaries, HTTP status checks, cancellation, linear deep comparison, or the lack of type declarations in our installed package. Safe use requires reading the 50-line source and the open issues.
Maintenance2/5npm 0.3.4 was published on February 12, 2023. GitHub reports an unarchived repository, 163 stars, 7 open issues and pull requests, and a push on May 23, 2026, but core cache-control reports have remained open since 2020. The package.json test command is `exit 0`, so a green test invocation proves only that the shell command returned successfully.
Ecosystem2/5The npm endpoint counted 3,723,006 downloads in the latest completed week, yet the package declares no React peer and offers one cache function with no adapters, provider, mutation layer, or developer tools. Current React server-state ecosystems center on TanStack Query, SWR, framework caches, and documented Suspense integrations. High transitive download volume does not supply the missing cache ownership model.

Use it if

  • You maintain existing components that already depend on `usePromise` and have tests for its global cache behavior.
  • A client-only experiment reads a small fixed set of immutable resources behind React Suspense.
  • Every loader can include its own stable resource tag in the input tuple to prevent cross-loader collisions.
  • Time-based eviction is enough and you do not need cancellation, invalidation, mutation, retry, or request scoping.
Skip it if

Setup reality

We installed react-promise-suspense 0.3.4 in a fresh Node 22 Bookworm sandbox. npm finished in 0.6 seconds and left 2 packages using 1 MB. The package was 28 KB unpacked, declared 1 direct dependency and no peers, and npm audit found 0 known vulnerabilities. It is CommonJS without an exports map; both require() and ESM import worked. Our package inspection found no TypeScript declarations.

There is no provider or config file, and React is not declared as a peer. Put every call under Suspense, and put rejected loaders under an Error Boundary. usePromise(loader, inputs, lifespan) spreads the input array into the loader. A fetch loader must check response.ok because fetch resolves for HTTP 404 and 500. The package catches a rejection into its cache, then React receives that stored error on the next render.

Cache identity is the risky part. Version 0.3.4 scans one module-global array and compares only inputs with fast-deep-equal; it never compares the loader. Put a module-level Symbol or other unique resource tag in each loader's inputs. Lifespan 0 means no eviction. A positive timer starts after settlement, and changing a refresh value creates another entry rather than clearing the previous one. Open issues request function-aware keys, manual removal, size limits, and unmount cleanup.

Our browser bundle measured 2.7 KB minified and 1.2 KB gzipped. The size is low, but the missing controls matter more than transfer cost. Deep comparison runs during render and linearly scans cached entries, so keep keys small and acyclic. Do not use the global cache for authenticated SSR. The repository's test command exits successfully without running tests, and 0.3.4 is still below 1.0.

Patterns

Read JSON below Suspense fetch-json

import {Suspense} from 'react';
import usePromise from 'react-promise-suspense';

const USER = Symbol('user');
async function loadUser(_kind, id) {
  const response = await fetch(`/api/users/${id}`);
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  return response.json();
}
function User({id}) {
  const user = usePromise(loadUser, [USER, id], 30_000);
  return <h2>{user.name}</h2>;
}
export const Screen = ({id}) => (
  <Suspense fallback={<p>Loading...</p>}><User id={id} /></Suspense>
);

The Symbol separates this loader's cache entries because version 0.3.4 does not include loader identity in its lookup.

Keep key construction in one helper wrap-resource

const PROJECT = Symbol('project');

async function fetchProject(_kind, id) {
  const response = await fetch(`/api/projects/${id}`);
  if (!response.ok) throw new Error('Project request failed');
  return response.json();
}
export function useProject(id) {
  return usePromise(fetchProject, [PROJECT, id], 60_000);
}

Define the Symbol at module scope. Creating it during render makes every input tuple unique and can suspend on every retry.

Evict a settled result after 60 seconds expire-cache-entry

const report = usePromise(
  loadReport,
  [REPORT, reportId],
  60_000,
);

The timer starts when the Promise settles. Passing 0 or omitting the lifespan schedules no eviction.

Retain one immutable public value cache-fixed-resource

const CONFIG = Symbol('public-config');
const loadConfig = async () => {
  const response = await fetch('/public-config.json');
  if (!response.ok) throw new Error('Config unavailable');
  return response.json();
};
const config = usePromise(loadConfig, [CONFIG]);

No lifespan keeps the entry for the module's lifetime. Limit this pattern to a finite set of public immutable resources.

Create a fresh key on demand refresh-with-version

function Report({id}) {
  const [revision, refresh] = useReducer(value => value + 1, 0);
  const report = usePromise(loadReport, [REPORT, id, revision], 30_000);
  return <button onClick={refresh}>Refresh {report.title}</button>;
}

A new revision adds a cache entry; it does not delete the old one. Give old entries a finite lifespan.

Group parallel requests under one cache key load-related-data

const DASHBOARD = Symbol('dashboard');
async function loadDashboard(_kind, accountId) {
  const [account, alerts] = await Promise.all([
    fetch(`/api/accounts/${accountId}`).then(checkJson),
    fetch(`/api/accounts/${accountId}/alerts`).then(checkJson),
  ]);
  return {account, alerts};
}
const data = usePromise(loadDashboard, [DASHBOARD, accountId], 15_000);

One rejected request caches an error for the combined resource. The package has no partial result or per-request retry state.

Turn bad HTTP status into an error reject-http-errors

async function checkJson(response) {
  if (!response.ok) {
    const error = new Error(`Request failed: ${response.status}`);
    error.status = response.status;
    throw error;
  }
  return response.json();
}

Fetch resolves normally for 404 and 500. Throwing sends the stored failure to the nearest Error Boundary on React's retry.

Pair Suspense with an Error Boundary catch-loader-error

class RequestBoundary extends React.Component {
  state = {error: null};
  static getDerivedStateFromError(error) { return {error}; }
  render() {
    return this.state.error ? <p>Could not load.</p> : this.props.children;
  }
}
<RequestBoundary>
  <Suspense fallback={<Spinner />}><User id={userId} /></Suspense>
</RequestBoundary>

Suspense handles the pending Promise. A rejected loader becomes a cached error and needs an Error Boundary.

Keep deep-comparison keys small use-small-inputs

const result = usePromise(
  search,
  [SEARCH, query, page, sortOrder],
  10_000,
);

Every render scans the global cache and deep-compares inputs. Avoid cyclic objects, DOM nodes, and large mutable structures.

Give two loaders separate resource tags avoid-loader-collision

const USER = Symbol('user');
const TEAM = Symbol('team');

const user = usePromise(loadUser, [USER, id], 30_000);
const team = usePromise(loadTeam, [TEAM, id], 30_000);

Without `USER` and `TEAM`, equal `[id]` arrays can return whichever loader populated the cache first.

Let page sections suspend independently split-boundaries

<main>
  <Suspense fallback={<ProfileSkeleton />}>
    <Profile id={userId} />
  </Suspense>
  <Suspense fallback={<FeedSkeleton />}>
    <Feed id={userId} />
  </Suspense>
</main>

Separate boundaries prevent one pending section from replacing the other section with the same fallback.

Keep it out of authenticated SSR isolate-server-requests

// Do not call usePromise for per-user data during shared-process SSR.
// Fetch on the server with a request-scoped cache, then pass the result
// to the client or use a framework data API with documented isolation.

The cache is a module-global array and exposes no request scope or clear method, so private results can survive beyond one server request.

Alternatives

PackageRegistryPick it when
@tanstack/react-querynpmChoose it for production server state with explicit keys, invalidation, retries, mutation, cancellation, and cache controls.
swrnpmChoose it for a smaller maintained cache with revalidation, mutation, and focus-aware refresh behavior.
use-async-resourcenpmChoose it when a Suspense resource abstraction is the goal and its cache lifecycle fits your app.
react-asyncnpmChoose it for explicit pending, fulfilled, and rejected state without relying only on thrown Promises.

More web frontend guides

postcss · react · react-dom · tailwindcss · htmlparser2 · tailwind-merge · the whole shelf →

How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.