Suspensive Documentation

repository·main·Indexed 21 days ago

https://github.com/toss/suspensive

An all-in-one toolkit for React Suspense providing declarative components and hooks to manage loading states and error handling. The ecosystem includes @suspensive/react for core components like ErrorBoundary and Delay, @suspensive/react-query for TanStack Query integration, @suspensive/jotai for state management, and @suspensive/codemods for automated API migrations.

Tokens
93.2K
Snippets
281
Records
371
Agent score
69%

What's inside Suspensive

  1. Overview of @suspensive/jotai

    main
    @suspensive/jotai is an extension of the Jotai state management library designed to work seamlessly with React Suspense. It provides utilities for managing state using atom that are optimized for Suspense-based workflows, allowing for smoother integration of asynchronous state updates within React's concurrent features.
  2. What is Suspensive?

    main

    Suspensive is a library designed to fill the functional gaps in React's core async rendering primitives (Suspense, Error Boundary, and lazy). It provides declarative components and hooks to handle real-world application requirements such as skipping SSR for specific boundaries, preventing loading spinner flashes, managing hydration mismatches, and coordinating error handling across multiple boundaries.

    Beyond React, Suspensive provides specialized integrations for:

    • TanStack Query (@suspensive/react-query): Simplifies Suspense integration, SSR prefetching, and error handling during hydration.
    • Jotai (@suspensive/jotai): Makes atom-based state management compatible with Suspense and other data-fetching layers.
    • React Core (@suspensive/react): Enhances Suspense, ErrorBoundary, and lazy with features like clientOnly boundaries, error filtering, and preloading.
  3. Overview of Suspensive core concepts

    main

    Suspensive is a collection of declarative React components and hooks designed to bridge the practical gaps in React's native features like Suspense, Error Boundaries, and lazy.

    Core Philosophy: Declare boundaries (error handling, loading states) and data-fetching (queries, mutations) as JSX at the same depth using render props. This allows your functional components to focus exclusively on the "success case," as the surrounding Suspensive components handle the loading and error states.

  4. Overview of @suspensive/react

    main

    @suspensive/react provides declarative components and hooks to enhance React's built-in Suspense, error boundary, and lazy primitives. It is designed for React 18+ and focuses on improving loading UX, error handling, SSR/client-only rendering, and code-splitting.

    Core Runtime Exports:

    • Boundaries: Suspense, ErrorBoundary, ErrorBoundaryGroup
    • UX/SSR: Delay, ClientOnly, useIsClient
    • Error Handling: useErrorBoundary, useErrorBoundaryFallbackProps
    • Code Splitting: lazy, createLazy, reloadOnError
    • Configuration: DefaultPropsProvider, DefaultProps
    • Pattern: All boundary components expose a .with(props, Component) HOC pattern.
  5. Overview of Suspensive features

    main

    Suspensive is a declarative toolkit for React Suspense, providing integrated solutions for error handling, loading states, and data fetching. Key features include:

    • <ErrorBoundary />: Declarative error handling with support for fallback, resetKeys, onError, and shouldCatch for selective error catching.
    • <ErrorBoundaryGroup />: Allows resetting multiple ErrorBoundary components simultaneously without prop drilling.
    • <Suspense clientOnly />: Skips server rendering for a specific boundary. It renders the fallback on the server and the children on the client, avoiding the need for dynamic() or useEffect guards.
    • <Delay ms={number} />: Prevents the 'flash of loading state' by only showing spinners if the loading process exceeds a specified duration. Supports render props for fade-in effects.
    • <DefaultPropsProvider />: Sets global default fallbacks for Suspense and Delay components, which can be overridden locally.
    • <SuspenseQuery />: Enables declarative data fetching as JSX. It works with TanStack Query and eliminates the need for hooks or wrapper components.
    • <ClientOnly />: Controls server/client rendering boundaries by rendering components only on the client side.
  6. How prefetching works in Suspensive

    main

    Prefetch APIs (usePrefetchQuery, usePrefetchInfiniteQuery, PrefetchQuery, and PrefetchInfiniteQuery) fire a fetch during the render phase—before a Suspense boundary below them suspends. This warms the cache so that subsequent suspense queries find the data immediately instead of starting a new request.

    Key characteristics:

    • They return nothing.
    • They never suspend.
    • They never throw errors (errors surface later through the suspense query that reads the cache).
    • You must use the exact same queryOptions (including queryKey) for both the prefetch and the suspense query to ensure a cache hit.
  7. Use QueriesHydration for per-boundary streaming

    main

    The QueriesHydration component is an async React Server Component (RSC) that prefetches an array of queries on the server and hydrates them into client components. This replaces the manual prefetchQuery + dehydrate + Hydrate boilerplate.

    To enable independent HTML streaming, wrap each section of your UI in its own Suspense boundary and QueriesHydration component. This allows each section to stream to the client as its specific queries resolve.

    Key details:

    • queries accepts an array of queryOptions or infiniteQueryOptions results.
    • Every entry must have a queryKey.
    • All queries in the array are fetched in parallel using Promise.all.
    • Render QueriesHydration only in Server Components.
    // app/posts/page.tsx — Server Component (no 'use client')
    import { Suspense } from '@suspensive/react'
    import { QueriesHydration } from '@suspensive/react-query-4'
    import { postsQueryOptions, userQueryOptions } from './queries'
    import { PostList, UserProfile } from './_components'
    
    export default function PostsPage({ userId }: { userId: number }) {
      return (
        <>
          <Suspense fallback={<div>Loading user...</div>}>
            <QueriesHydration queries={[userQueryOptions(userId)]}>
              <UserProfile userId={userId} />
            </QueriesHydration>
          </Suspense>
          <Suspense fallback={<div>Loading posts...</div>}>
            <QueriesHydration queries={[postsQueryOptions(userId)]}>
              <PostList userId={userId} />
            </QueriesHydration>
          </Suspense>
        </>
      )
    }
  8. How @suspensive/jotai components work

    main

    @suspensive/jotai provides three render-prop components that wrap Jotai hooks. This allows you to read or write atoms inline within JSX (e.g., inside loops or conditionals) without needing to extract a new component just to use a hook.

    Key Characteristics:

    • Client-only: All components are 'use client' and cannot cross the React Server Components (RSC) serialization boundary.
    • Suspense Integration: Async atoms (or writing a Promise via SetAtom) will trigger the nearest parent Suspense boundary. A Suspense boundary must exist above these components to prevent blanking the UI.
    • Props: Each component accepts an atom prop and an optional options prop (which is forwarded to the underlying Jotai hook, such as { store }).
  9. Using @suspensive/jotai with Jotai extensions

    main

    The components provided by @suspensive/jotai (specifically <Atom/>, <AtomValue/>, and <SetAtom/>) are compatible with the existing Jotai extension ecosystem. This means atoms created via extensions like tRPC, Query (e.g., jotai-tanstack-query), or Cache work out of the box without requiring additional wrappers.

    import { AtomValue } from '@suspensive/jotai'
    import { Suspense, ErrorBoundary } from '@suspensive/react'
    import { userQueryAtom } from '~/queries' // Example: atom from jotai-tanstack-query
    
    const MyPage = () => (
      <ErrorBoundary fallback={({ error }) => <>{error.message}</>}>
        <Suspense fallback={'pending...'}>
          <AtomValue atom={userQueryAtom}>
            {({ data: user }) => <UserProfile key={user.id} {...user} />}
          </AtomValue>
        </Suspense>
      </ErrorBoundary>
    )