# Suspensive, the React Suspense toolkit that closes the gaps in the primitives

> Suspensive is a set of packages from Toss that fills the practical gaps in React's Suspense and error boundary primitives, with typed error boundaries that catch only what you name, a group boundary that resets many at once, a clientOnly boundary, a Delay component and a React Query integration that makes data fetching declarative JSX.

**toss/suspensive** — All-in-one for React Suspense — ErrorBoundary with shouldCatch, clientOnly to skip SSR for a boundary, Delay, and more

- Repository: https://github.com/toss/suspensive
- Website: https://suspensive.org
- Stars: 1,058 · Forks: 98
- Language: TypeScript
- License: MIT
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/toss-suspensive

## The gap between having Suspense and using it

The README frames the problem in one sentence. React gives you Suspense, lazy, and an interface to build Error Boundaries, but using them in real applications reveals gaps. Suspensive fills those gaps with declarative components and hooks.

That framing matters, because a library whose pitch is filling gaps has to be judged on whether those gaps are real. They are, and the example at the top of the README shows all of them at once:

```tsx
import { Delay, ErrorBoundary, ErrorBoundaryGroup, Suspense } from '@suspensive/react'

const Page = () => (
  <ErrorBoundaryGroup>
    <ErrorBoundaryGroup.Consumer>
      {({ reset }) => <button onClick={reset}>Reset All</button>}
    </ErrorBoundaryGroup.Consumer>
    <ErrorBoundary
      shouldCatch={NetworkError}
      fallback={({ error, reset }) => <ErrorUI error={error} onRetry={reset} />}
    >
      <Suspense
        clientOnly
        fallback={
          <Delay ms={200}>
            {({ isDelayed }) => <Spinner style={{ opacity: isDelayed ? 1 : 0, transition: 'opacity 200ms' }} />}
          </Delay>
        }
      >
        <Content />
      </Suspense>
    </ErrorBoundary>
  </ErrorBoundaryGroup>
)
```

Five gaps are visible in those twenty lines, and the bullet list under the example names them.

shouldCatch lets a boundary catch only specific error types and let others propagate, which is the behaviour React's own error boundary interface does not give you by default, where a boundary catches everything thrown below it.

ErrorBoundaryGroup resets multiple error boundaries at once, with no prop drilling, which is the other thing the built-in interface does not do. The Consumer component in the example is a reset button for every boundary in the group, which is the pattern you want when a page has several independent panels and one of them fails.

clientOnly skips SSR for a Suspense boundary, showing the fallback on the server and the children on the client, which is what you need for anything that touches the window.

Delay prevents a flash-of-loading-state, and it does it with render props so the fallback can fade in rather than blink.

And reset is built into the fallback props, so there is no external state to wire up for a retry button.

The licence is MIT with the copyright held by Viva Republica, Inc, which is the company behind Toss, and the project is published at suspensive.org with the documentation available in English and Korean.

## Four packages, and a codemods package that is not what you expect

The project is a monorepo of scoped packages, and the table in the README lists what is in each.

@suspensive/react is the core, with Suspense, ErrorBoundary, ErrorBoundaryGroup, Delay and ClientOnly. @suspensive/react-query is the React Query integration, with SuspenseQuery, SuspenseInfiniteQuery, Mutation and PrefetchQuery. @suspensive/jotai is the Jotai integration, with Atom, AtomValue and SetAtom. And @suspensive/codemods provides automated migration codemods.

The codemods package is the one worth a second look, because it changes how you would adopt the rest. Migrating to a library that provides its own Suspense, its own error boundary and its own React Query integration is not a matter of adding a dependency. You have to find every use of the equivalent primitives from other libraries and rewrite them, and that is a large, mechanical, easy-to-get-wrong change. A package of codemods is the answer to that, and it tells you the maintainers thought about the migration path rather than only the destination.

The key features list puts the same pieces in a different order, with a few extras. The ErrorBoundary with shouldCatch catches only the errors you want, and the README links a comparison with react-error-boundary. The Suspense with clientOnly declaratively skips SSR for a boundary when you need client-only rendering. SuspenseQuery is declarative data fetching as JSX, with no hook constraints, which is the phrase to unpack: React Query's hooks require consistent call ordering and cannot be called conditionally, and a component-based API sidesteps that.

Delay prevents flash-of-loading-state UX issues, and DefaultPropsProvider sets global default fallbacks for all components, which is a convenience for an application that wants the same fallback shape everywhere.

Installation is a single command, npm install @suspensive/react, and the full documentation is at suspensive.org.

The packages are versioned in lockstep. The recent releases are @suspensive/react@3.21.4, @suspensive/react-query@3.21.4 and @suspensive/react-query-5@3.21.4, all from 2026-09-05, and the react-query-5 variant carries a suffix marking the React Query major it targets. The last push was on 2026-09-24 and the default branch is main.

## Delay, and the flash of loading state

The Delay component deserves its own look, because it solves a problem that is easy to dismiss and annoying to live with.

The problem is the flash of loading state. When a Suspense boundary resolves quickly, the fallback appears for a frame or two and disappears, and the user sees a spinner blink. That is the kind of detail that makes an interface feel cheap, and it is the kind of detail that a design system team notices and a feature team does not.

Delay solves it by holding the fallback in place for a minimum time and telling the child when it is safe to show. The fallback is a render prop, so it receives the state of the delay:

```tsx
<Delay ms={200}>
  {({ isDelayed }) => <Spinner style={{ opacity: isDelayed ? 1 : 0, transition: 'opacity 200ms' }} />}
</Delay>
```

The spinner is invisible while the delay is running and fades in over 200 milliseconds once it ends, so a fast response never produces a blink. The render prop is the design decision here, because it gives the caller control over the transition rather than hardcoding a behaviour, and the same component can therefore produce a fade, a scale or nothing at all.

The delay is also used as the Suspense fallback in the top-level example, which shows the intended pairing. Suspense with clientOnly decides whether to render children at all on the server, and Delay decides what the user sees while it is not there. The two are separate concerns and the example keeps them separate.

The companion piece is DefaultPropsProvider, which sets global default fallbacks for all components. That is a convenience for an application that wants the same fallback shape everywhere, and it is the sort of thing that is either exactly what you want or something you should not use at all, because a global default fallback is a global decision about how your application behaves when something is loading or broken.

## The repository tells you how a large React library is put together

The repository layout is the more instructive part for anyone building a similar set of packages, because it is a real multi-package setup rather than a single library with extra exports.

There is a packages/ directory, a pnpm-workspace.yaml and a pnpm-lock.yaml, and the package manager is pinned at pnpm 10.9.0 with the preinstall script running corepack enable. The build is driven by turbo, with turbo.json at the root, and the root scripts are all turbo pipelines: build runs turbo run build, dev runs turbo watch dev, and clean runs turbo run clean. There is even a graph script that removes the graph directory, recreates it and runs the build with a graph output, which is a build-cache visualisation and a reasonable thing for a monorepo to expose.

The release process is changesets, with a.changeset/ directory and the changeset, changeset:version and changeset:publish scripts, where publishing runs the build first and then changeset publish. Changesets is the modern choice for a family of packages because each package gets its own version bump driven by which packages a changeset touches, and the fact that the packages here are versioned in lockstep is handled by a fixed or linked configuration rather than by hand.

The quality gates are visible in the ci:all script, and the list is long. It runs eslint, then sherif, then the type check, then the tests, then knip, and then the build. sherif is a package.json linter that catches things like mismatched dependency versions across a monorepo, and knip is a tool that finds unused files, dependencies and exports. There is a knip.json config, a packlint.config.mjs with a packlint sort command that keeps package.json fields in a canonical order, and an oxfmt.config.ts for formatting, with the format script running oxfmt over TypeScript, TSX and Markdown.

There is also a lint-staged configuration, a husky directory for git hooks, and a codecov.yml for coverage. The CI pipeline being this explicit is the point: a monorepo of scoped packages with different peer dependency ranges will drift, and the tools listed are the ones that catch drift.

The docs/ directory holds the documentation site, there are examples/ with three runnable projects, and there are configs/ and _artifacts/ directories for shared configuration and build output.

## Conclusion

Suspensive suits a React team that has adopted Suspense and data fetching together and wants the awkward parts, typed error boundaries, group resets, client-only rendering and query integration, to be components rather than bespoke hooks. It is the wrong choice if your project is still on React 18 without Suspense in production, since the whole package is built on the primitives React now ships, and it is the wrong choice if you want one library rather than a family of scoped packages with a codemod story. Before adopting it, read the comparison with react-error-boundary that the README links, and note that the packages are versioned together on a single number.

## FAQ

### What is Suspensive and what problem does it solve?

It is a set of packages from Toss that fills the gaps React leaves when you use Suspense and error boundaries in a real application, providing declarative components and hooks such as typed error boundaries, group resets, client-only boundaries and a delay component.

### What does shouldCatch do in Suspensive?

It lets an ErrorBoundary catch only specific error types and let others propagate, which the built-in React error boundary interface does not offer. The README links a comparison with react-error-boundary for the alternative.

### Which packages does the Suspensive project publish?

Four scoped packages. @suspensive/react has Suspense, ErrorBoundary, ErrorBoundaryGroup, Delay and ClientOnly, @suspensive/react-query has SuspenseQuery, SuspenseInfiniteQuery, Mutation and PrefetchQuery, @suspensive/jotai has Atom, AtomValue and SetAtom, and @suspensive/codemods provides automated migration codemods.

### What does clientOnly do on a Suspense boundary?

It skips server side rendering for that boundary, showing the fallback on the server and the children on the client, which is what you need for content that touches the window. It is available on the Suspense component from @suspensive/react.

### How do I install Suspensive?

Run npm install @suspensive/react, then read the full documentation at suspensive.org, which is available in English and Korean. The packages are versioned together, and the most recent releases are @suspensive/react@3.21.4 and @suspensive/react-query@3.21.4.

## Sources

- [License: MIT](https://github.com/toss/suspensive/blob/main/LICENSE)
- [Project website](https://suspensive.org)
- [README](https://github.com/toss/suspensive/blob/main/README.md)
- [Releases](https://github.com/toss/suspensive/releases)
- [toss/suspensive on GitHub](https://github.com/toss/suspensive)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/toss-suspensive
