mrkeyoor.com_
Sat 08 Aug 20:58 UTC
npmWeb Frontendupdated 08 Aug 2026

suspend-react

suspend-react is a tiny React Suspense cache for promise-returning work. Call suspend during render with an async function and a key tuple; the first call throws the pending promise to the nearest Suspense boundary, a later render returns the resolved value directly, and rejected work reaches the nearest error boundary. The same module-global cache also supports preloading, peeking, manual clearing, expiring idle entries, and custom key equality. It is a low-level primitive, not a complete server-state or fetching library.

Verdict

The implementation is admirably small, but the global cache and manual lifecycle make it best for narrow client-side resources with carefully designed keys. For application server state, TanStack Query or SWR earns its extra weight by handling invalidation, retries, cancellation, and cache boundaries.

API stability3/5The four exports, suspend, preload, peek, and clear, have a compact contract, and the implementation is only a small cache around thrown promises. However, the current version remains 0.1.3 rather than a 1.0 commitment, the README describes a future move toward React cache boundaries, and its stated React 16.6 compatibility conflicts with the published peer dependency of React >=17. Those are reasons to wrap usage rather than expose it throughout an app.
Docs3/5The README explains the Suspense mechanism, key tuples, resolved-value return, error propagation, lifespan, custom equality, preload, clear, peek, global key collisions, TypeScript, and React history with short examples. It does not document SSR isolation, cancellation, retries, the fact that cached errors remain until eviction, how loader arguments include identity suffixes, or how the cache behaves under newer React server and concurrent rendering patterns.
Maintenance2/5The package is not deprecated and the repository is not archived, but version 0.1.3 was published on June 13, 2023 and the last repository push was October 24, 2023. GitHub reports 13 open issues and pull requests. A tiny dependency-free implementation can remain functional without frequent releases, yet a React integration primitive benefits from active verification as React's cache and rendering APIs evolve.
Ecosystem3/5suspend-react recorded 4,974,625 downloads for the fetched week, has 1,411 GitHub stars, belongs to the pmndrs organization, includes TypeScript declarations, and has no runtime dependencies beyond the React peer. Its ecosystem is intentionally narrow: there are no adapters, devtools, persistence, hydration, mutation, or query-key utilities, and its global cache does not integrate with provider boundaries used by larger React data libraries.

Use it if

  • You already understand React Suspense and need a very small read-through cache around promise-returning work
  • Your data is naturally read during rendering and pending UI belongs at a parent Suspense boundary
  • You can design collision-safe cache key tuples and explicitly clear or expire entries when data changes
  • You want preload and peek helpers without adopting a full query-client configuration model
Skip it if

Setup reality

Install suspend-react alongside React. The current package metadata declares React >=17 as a peer dependency, even though the README still says the technique works from React 16.6, so dependency managers use 17 as the actual install constraint. Both ESM and CommonJS builds are published and TypeScript declarations are included. Every component that can suspend must render below a Suspense boundary, and rejected work needs an error boundary; Suspense does not catch errors. The central setup job is key design. Keys are arrays compared item by item with === by default, and the cache is one module-global array. Fresh object literals miss on every render unless you provide a custom equal function, while generic tuples such as [42] can collide across unrelated loaders because an explicitly supplied key replaces the function as identity. Append a stable string or Symbol for the resource type. The loader receives every key item as an argument, including that identity suffix, so write its parameter list accordingly or ignore the extra item. With no keys, the promise or function becomes the key. Entries live forever because lifespan defaults to 0; set a positive idle lifespan or call clear after mutations, logout, and tests. Reads refresh a positive timer. preload starts the same cache entry but returns undefined, peek returns a resolved value or undefined without suspending, and neither gives status metadata. Rejections are cached and thrown during future renders, so a retry button must clear the exact key before rendering again. There is no AbortSignal wiring, deduplication policy beyond exact key equality, server dehydration, provider-scoped cache, or automatic cleanup on unmount. In SSR, create process or request isolation outside this library or choose a query cache with explicit boundaries. The repository has not shipped since 2023, so test it against your exact React mode before committing architecture to it.

Patterns

Read async data under Suspensesuspend-data-read

import {Suspense} from 'react'
import {suspend} from 'suspend-react'

async function loadPost(id) {
  const response = await fetch(`/api/posts/${id}`)
  if (!response.ok) throw new Error(`HTTP ${response.status}`)
  return response.json()
}

function Post({id}) {
  const post = suspend(loadPost, [id, 'post'])
  return <h1>{post.title}</h1>
}

export function Page({id}) {
  return <Suspense fallback={<p>Loading...</p>}><Post id={id} /></Suspense>
}

suspend returns the resolved value, not a promise. The loader receives both id and the identity suffix; extra arguments are harmless here.

Prevent key collisions with Symbolsisolate-resource-keys

import {suspend} from 'suspend-react'

const userResource = Symbol('user-resource')
const loadUser = (id) => fetch(`/api/users/${id}`).then((r) => r.json())

function User({id}) {
  const user = suspend(loadUser, [id, userResource])
  return <span>{user.name}</span>
}

The cache is global and explicit keys do not automatically include loadUser. A stable Symbol keeps [1] for a user separate from [1] for another resource.

Start loading before renderpreload-resource

import {preload, suspend} from 'suspend-react'

const postResource = Symbol('post')
const loadPost = (id) => fetch(`/api/posts/${id}`).then((r) => r.json())

preload(loadPost, [42, postResource])

function Post() {
  const post = suspend(loadPost, [42, postResource])
  return <article>{post.title}</article>
}

preload returns undefined. The later suspend call must use an equal key tuple to reuse the pending or resolved entry.

Read a resolved entry without suspendingpeek-cached-value

import {peek} from 'suspend-react'

const cached = peek([42, postResource])
if (cached !== undefined) {
  console.log('already loaded', cached)
}

peek returns only a resolved response or undefined. It does not distinguish missing, pending, rejected, or a legitimately resolved undefined value.

Invalidate one cache key after a mutationclear-one-entry

import {clear} from 'suspend-react'

async function savePost(id, input) {
  await fetch(`/api/posts/${id}`, {
    method: 'PUT',
    headers: {'content-type': 'application/json'},
    body: JSON.stringify(input)
  })
  clear([id, postResource])
}

The key must match exactly, including the same Symbol or object references used for suspend. The next render starts a new load.

Reset the whole cacheclear-global-cache

import {clear} from 'suspend-react'

afterEach(() => {
  clear()
})

clear() removes every entry in the module-global cache. In an app, use this carefully because unrelated Suspense resources share that cache.

Expire an entry after inactivityexpire-idle-entry

const profile = suspend(loadProfile, [userId, profileResource], {
  lifespan: 60_000
})

lifespan is milliseconds and defaults to 0, which means keep forever. Each successful read refreshes a positive expiry timer.

Use a custom equality functioncompare-structured-keys

import deepEqual from 'fast-deep-equal'
import {suspend} from 'suspend-react'

const result = suspend(loadSearch, [{query, filters}, searchResource], {
  equal: deepEqual
})

fast-deep-equal is a separate dependency. Deep comparison runs while scanning cache entries, so primitive stable keys are cheaper and easier to invalidate.

Suspend directly on a stable promiseuse-promise-key

import {suspend} from 'suspend-react'

const settingsPromise = fetch('/api/settings').then((r) => r.json())

function Settings() {
  const settings = suspend(settingsPromise)
  return <pre>{JSON.stringify(settings, null, 2)}</pre>
}

With no keys, the promise itself is the cache key. Create it outside render; a new promise on every render would create endless misses.

Use nested Suspense boundariesscope-loading-ui

function Dashboard() {
  return (
    <Suspense fallback={<DashboardSkeleton />}>
      <Summary />
      <Suspense fallback={<ChartSkeleton />}>
        <Chart />
      </Suspense>
    </Suspense>
  )
}

A thrown pending promise is handled by the nearest Suspense boundary. Boundary placement determines how much already rendered UI is replaced.

Clear a failed entry before retryingretry-cached-error

import {startTransition} from 'react'
import {clear} from 'suspend-react'

function retryPost(id) {
  clear([id, postResource])
  startTransition(() => setRetryCount((count) => count + 1))
}

Rejected entries stay cached and keep throwing. An error-boundary retry must clear the exact key before triggering another render.

Preload an entry with idle expirypreload-with-lifespan

preload(loadProduct, [sku, productResource], {lifespan: 30_000})

// Later, under Suspense:
const product = suspend(loadProduct, [sku, productResource], {lifespan: 30_000})

Use the same key and lifecycle policy at preload and read sites. preload does not report pending or rejected status.

Alternatives

PackageRegistryPick it when
@tanstack/react-querynpmYou need a maintained server-state cache with retries, cancellation, invalidation, mutations, pagination, hydration, and optional Suspense integration
swrnpmYou want stale-while-revalidate fetching, focus revalidation, mutations, and a concise React hook API
react-asyncnpmYou prefer explicit pending, fulfilled, and rejected state through hooks or components instead of render-time promise throwing