mrkeyoor.com_
Sat 08 Aug 22:01 UTC
npmWeb Frontendupdated 08 Aug 2026

react-promise-suspense

react-promise-suspense exports one function, usePromise, that calls a Promise-returning function during React render and throws the pending Promise so the nearest Suspense boundary shows its fallback. When the work settles, React retries and the function returns the cached value or throws the cached error. Inputs are compared with fast-deep-equal and an optional lifespan removes a cache entry after settlement. Despite the hook-like name, the implementation uses no React hooks and has no React dependency or peer declaration.

Verdict

Do not install react-promise-suspense for new production data fetching. Its tiny API is appealing, but function-blind cache keys, permanent default retention, global SSR state, and a no-op test suite make the simplicity unsafe beyond controlled legacy code.

API stability2/5There is only one exported function and version 0.3.4's generic tuple typing gives callers decent result inference, so the visible call shape is easy to learn. The package remains below 1.0, however, and fixing the open cache-identity bug by adding Promise function identity would change observable behavior. Missing invalidation, sizing, and cleanup controls also leave little room to evolve without expanding the contract.
Docs2/5The README shows installation and one fetch example under Suspense, which is enough for a demo. It does not document global cache scope, permanent zero-lifespan retention, when the lifespan timer starts, cached errors, function-blind key collisions, SSR isolation, HTTP status handling, Error Boundaries, cancellation, TypeScript import style, or the linear deep-equality lookup. The source is required reading for safe use.
Maintenance2/5npm 0.3.4 and the latest visible source commit date to February 2023. The repository is not archived, but the package test command exits successfully without running tests, and open cache correctness and memory-control requests date to 2020. A repository push timestamp in 2026 does not outweigh the absence of a newer published fix for those core design reports.
Ecosystem2/5The package recorded 3,475,178 downloads in the measured week and ships TypeScript declarations, but its direct community surface is tiny: one API, one runtime dependency, seven combined open issues and pull requests, no adapters, no provider, and no devtools. Most modern React server-state guidance and integrations center on TanStack Query, SWR, framework caches, or React's current data APIs instead.

Use it if

  • You maintain an existing component that already uses this exact function and its global cache behavior is understood
  • You need a tiny client-only Suspense proof of concept for a finite, low-cardinality set of immutable resources
  • You can give every resource type a unique stable tag in its input array to avoid cross-loader cache collisions
  • You accept a basic time-based cache with no invalidation, cancellation, mutation, retry, or request-scoping API
Skip it if

Setup reality

npm install react-promise-suspense adds fast-deep-equal and no declared React peer dependency. The package is CommonJS with an export-equals TypeScript declaration; depending on compiler settings, TypeScript uses import usePromise = require('react-promise-suspense') or a default import with esModuleInterop. There is no provider or config file, but every caller must be below React Suspense and rejected work needs an Error Boundary. The loader must return a Promise and the input array is spread into it as arguments. The important setup is cache discipline. Version 0.3.4 searches one module-global array using only deep equality of inputs. It does not include the loader function in the key, even though an open issue identifies that bug. Give each resource family a module-level Symbol or other unique tag as its first input and make the loader accept that unused argument. Avoid cyclic input objects because the equality library is not a general cyclic-graph keyer, and prefer small primitive keys because every lookup scans the array. lifespan defaults to 0, which means no scheduled eviction; both responses and errors remain cached. A positive lifespan starts its timer only after the Promise settles. There is no manual invalidation, cache bound, focus refetch, stale-while-revalidate behavior, retry, cancellation, AbortController ownership, unmount cleanup, or mutation coordination. Changing an input forces a different entry but does not remove the old one until its own lifespan expires. fetch also resolves on HTTP 404 and 500, so loaders must check response.ok if those should reach an Error Boundary. Server rendering is especially risky because cached values are shared across requests and users. The package has no request-scoped cache or reset API, so do not use it for authenticated SSR data.

Patterns

Read JSON under a Suspense boundaryfetch-json

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

const USER_RESOURCE = Symbol('user');
const loadUser = async (_resource, 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_RESOURCE, id], 30_000);
  return <h2>{user.name}</h2>;
}

export function Screen({ id }) {
  return <Suspense fallback={<p>Loading...</p>}><User id={id} /></Suspense>;
}

The Symbol distinguishes this loader from every other loader because version 0.3.4 does not include function identity in its cache key.

Hide cache-key discipline in a resource hookdefine-resource-hook

const PROJECT_RESOURCE = Symbol('project');

async function fetchProject(_resource, projectId) {
  const response = await fetch(`/api/projects/${projectId}`);
  if (!response.ok) throw new Error('Project request failed');
  return response.json();
}

export function useProject(projectId) {
  return usePromise(fetchProject, [PROJECT_RESOURCE, projectId], 60_000);
}

Keep the tag at module scope. Creating Symbol('project') inside the hook would produce a new cache key on every render and suspend forever.

Expire a settled entry after a minuteset-cache-lifespan

const result = usePromise(
  loadReport,
  [REPORT_RESOURCE, reportId],
  60_000
);

The eviction timer begins after the Promise settles, not when the request starts. A lifespan of 0 schedules no eviction.

Keep a finite immutable resource cachedcache-static-resource

const CONFIG_RESOURCE = Symbol('public-config');
const loadConfig = (_resource) =>
  fetch('/public-config.json').then((response) => {
    if (!response.ok) throw new Error('Config unavailable');
    return response.json();
  });

const config = usePromise(loadConfig, [CONFIG_RESOURCE]);

Omitting lifespan retains this entry for the life of the module. Reserve that for a small fixed set of public immutable values.

Force a new request by changing an inputforce-refresh

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

This creates a new cache entry; it does not delete the old one. Use a finite lifespan to prevent refresh keys accumulating permanently.

Resolve related requests in parallelload-in-parallel

const DASHBOARD_RESOURCE = Symbol('dashboard');

async function loadDashboard(_resource, accountId) {
  const [account, alerts] = await Promise.all([
    fetch(`/api/accounts/${accountId}`).then(checkJson),
    fetch(`/api/accounts/${accountId}/alerts`).then(checkJson),
  ]);
  return { account, alerts };
}

const dashboard = usePromise(
  loadDashboard,
  [DASHBOARD_RESOURCE, accountId],
  15_000
);

One rejection caches the combined error until eviction. The library supplies no per-request retry or partial-result state.

Turn HTTP failures into rejected Promisescheck-http-status

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 responses. Throw explicitly so the cached error reaches an Error Boundary.

Catch rejected work with an Error Boundarycatch-render-errors

class RequestErrorBoundary extends React.Component {
  state = { error: null };
  static getDerivedStateFromError(error) { return { error }; }
  render() {
    if (this.state.error) return <p>Could not load this section.</p>;
    return this.props.children;
  }
}

<RequestErrorBoundary>
  <Suspense fallback={<Spinner />}>
    <User id={userId} />
  </Suspense>
</RequestErrorBoundary>

Suspense handles pending Promises, not rejected results. The error remains cached for that input until its lifespan expires.

Build cache inputs from small primitive valuesuse-primitive-inputs

const searchResult = usePromise(
  search,
  [SEARCH_RESOURCE, query, page, sortOrder],
  10_000
);

Inputs are deep-compared by a linear scan of the global cache. Avoid cyclic objects, large structures, DOM nodes, and freshly changing values.

Infer a typed result from a typed loadertype-result

type User = { id: string; name: string };
const USER_RESOURCE = Symbol('user');

async function loadUser(_resource: symbol, id: string): Promise<User> {
  return checkJson(await fetch(`/api/users/${id}`));
}

const user = usePromise(loadUser, [USER_RESOURCE, userId], 30_000);
user.name;

The 0.3.4 declaration uses tuple generics, so argument and result types flow from the loader. The package uses a CommonJS export-equals declaration.

Import without esModuleInteropimport-with-typescript

import usePromise = require('react-promise-suspense');

With esModuleInterop enabled, a default import normally works. The published declaration uses export = usePromise.

Keep independent sections from blocking togethersplit-suspense-boundaries

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

Separate boundaries let one cached or slow resource resolve without forcing the other section to share its fallback.

Alternatives

PackageRegistryPick it when
@tanstack/react-querynpmUse it for production server-state fetching with explicit keys, retries, invalidation, mutations, cancellation, and cache controls.
swrnpmUse it for a smaller stale-while-revalidate model with focus revalidation, mutation, and a maintained React cache API.
use-async-resourcenpmUse it when you specifically want a Suspense resource abstraction and are prepared to evaluate its cache model.