Suspense
Purpose and Scope
This page explains how SWR is used with React Suspense in the repository’s local example and E2E application routes. Suspense is a React rendering model where a component can pause rendering while asynchronous work is pending and let a parent <Suspense> boundary show a fallback. In SWR, that asynchronous work is usually the request associated with a cache key. The examples here focus on how useSWR participates in that boundary, how fallback data can itself be a promise, and what the test routes are asserting about render behavior.
SWR’s general data-fetching model is still the foundation: a hook is called with a key and a fetcher, the key identifies the request, and the fetcher resolves data asynchronously. The README describes SWR as a React Hooks library for data fetching, derived from stale-while-revalidate, and explicitly lists React Suspense among the supported capabilities. That means Suspense is not a separate data layer; it is another rendering mode layered on top of the same key, cache, fetcher, revalidation, and configuration primitives used by normal useSWR calls.
Sources: README.md, e2e/site/app/render-suspense-avoid-rerender/page.tsx
Relevant Source Files
README.md— Introduces SWR as a React Hooks data-fetching library, explains the key/fetcher/return-value model, and identifies React Suspense as a supported capability.e2e/site/app/render-suspense-avoid-rerender/page.tsx— Shows the clearest local Suspense pattern: a client component wraps auseSWRcall configured withsuspense: truein a React<Suspense>boundary.e2e/site/app/render-promise-suspense-resolve/page.tsx— Demonstrates a promise stored inSWRConfigfallback data that resolves before a slower fetcher returns fresh data.e2e/site/app/render-promise-suspense-error/page.tsx— Demonstrates a promise stored inSWRConfigfallback data that rejects and is handled by an error boundary around the Suspense boundary.e2e/site/app/render-promise-suspense-shared/page.tsx— Demonstrates multiple components reading the same key while sharing the same promise-backed fallback value.e2e/site/app/rsc-unstable-preload-no-suspense/client.tsx— Shows an adjacent React Server Components preload path that injects cache data throughSWRConfigwithout relying on a Suspense boundary in the client component.
Core Primitives
The main primitive is useSWR. In the standard README quick-start shape, useSWR('/api/user', fetcher) returns state such as data, error, and isLoading. In Suspense examples, the same hook is used, but the component tree includes a React <Suspense> boundary that decides what should be shown while the hook cannot synchronously provide final data. The hook’s key still names the cache entry, and the fetcher still performs asynchronous work; Suspense only changes how pending work is surfaced to React.
A second primitive is SWRConfig, the provider used to supply configuration to hooks below it. The promise-based routes use SWRConfig with a fallback object whose property name is the SWR key and whose value is a promise. This makes fallback data more than a static bootstrap value: it can represent work that is already in flight. The page components keep the promise stable by creating it with useState, then memoize the provider value with useMemo so the fallback map does not change on every render.
The third primitive is React’s own boundary structure. The E2E routes use <Suspense fallback={<div data-testid="fallback">loading</div>}> or a similar fallback, and the error case adds ErrorBoundary from react-error-boundary. This separation is important: Suspense handles pending promises, while an error boundary handles rejected work. SWR integrates with that model rather than replacing it with SWR-specific loading and error branches inside the component that reads data.
Sources: README.md, e2e/site/app/render-promise-suspense-resolve/page.tsx, e2e/site/app/render-promise-suspense-error/page.tsx
Local Suspense Flow
The most direct Suspense route is render-suspense-avoid-rerender. It is a client component and imports Suspense, useRef, useSWR, and a local sleep helper. Its fetchValue function waits briefly and returns the string SWR. Inside Section, the hook call is useSWR('render-suspense-avoid-rerender', fetchValue, { suspense: true }). The parent page wraps Section in <Suspense fallback={<div data-testid="fallback">fallback</div>}>, so React can show the fallback until SWR’s Suspense-enabled hook has data.
The route also counts renders with refs. startCountRef increments at the beginning of the component function, while dataCountRef increments only when the observed data value changes from the previous value and is not undefined. This is a testing signal, not application UI, but it teaches an important Suspense expectation: the component may start rendering while data is pending, yet the meaningful data render should occur when the fetcher result is available. The visible output includes start-count, data-count, and data test IDs so E2E tests can distinguish fallback behavior from data-render behavior.
A minimal Suspense version of this pattern looks like the tested route:
function Section() {
const { data } = useSWR('render-suspense-avoid-rerender', fetchValue, {
suspense: true
})
return <div>{data}</div>
}
export default function Page() {
return (
<Suspense fallback={<div>fallback</div>}>
<Section />
</Suspense>
)
}Sources: e2e/site/app/render-suspense-avoid-rerender/page.tsx
Promise Fallback Patterns
The promise fallback routes show a more advanced pattern: SWRConfig can provide fallback values for keys, and those fallback values can be promises. In render-promise-suspense-resolve, the fallback promise waits for fallbackDelay and resolves to initial data; the hook also has a fetcher that waits longer and returns new data. The page renders Content under Suspense, then records a debug history of data. The route demonstrates a layered progression: first the boundary waits for the fallback promise, then the component can render initial data, and later SWR revalidation can replace it with the fetcher’s fresher result.
The rejection route uses the same structure but makes the fallback promise throw new Error('error') after the delay. The page wraps the Suspense boundary with an ErrorBoundary whose fallback renderer outputs the error message. This demonstrates that rejected fallback work is not represented as a normal data value. It follows React’s boundary model: pending work reaches Suspense, rejected work reaches the error boundary. For application code, this means a Suspense-enabled SWR tree should be paired with an error boundary whenever rejected promises or fetcher failures are expected to be shown in a controlled way.
The shared route demonstrates that a single key can be consumed by more than one component inside the same boundary. PromiseConfig sets fallback: { [key]: fallback }, and two Item components both call useSWR<string>(key). Both items render the same data-testid pattern with their own IDs after the shared promise resolves. The important design point is that sharing happens through the SWR key and provider-backed cache state, not through manually passing the resolved value as props to every component.
Sources: e2e/site/app/render-promise-suspense-resolve/page.tsx, e2e/site/app/render-promise-suspense-error/page.tsx, e2e/site/app/render-promise-suspense-shared/page.tsx
Client-Only and RSC-Adjacent Considerations
Each Suspense route in this set begins with 'use client' and renders its content inside OnlyRenderInClient. That wrapper is part of the E2E app’s test harness, but the intent is clear: these examples exercise client-side React behavior. When documenting or copying the pattern into a Next.js app, treat the Suspense boundary and useSWR call as client component concerns. The boundary can appear in a larger app tree, but the hook itself runs where React client hooks are valid.
The RSC-adjacent route, rsc-unstable-preload-no-suspense/client.tsx, shows a related but different data path. ClientRoot receives cacheData and passes it into SWRConfig as value={{ cacheData }}. ClientData then calls useSWR(key, fetcher) without the explicit suspense: true option. It reads data, isLoading, isValidating, a bound mutate, and the underlying cache entry through useSWRConfig. The UI exposes these fields with test IDs and includes a revalidate button that calls mutate(). This route is useful when comparing Suspense to preload/cache hydration: cached data can be available to the client without forcing the component to use a Suspense boundary for its initial display.
This distinction helps avoid a common confusion. Suspense is one way to coordinate pending async data with React rendering. Preloaded or injected cache data is another way to make data available before a hook needs to fetch on the client. Both still use SWR’s cache and key model, but their user-visible behavior differs: a Suspense route emphasizes fallback UI while waiting, whereas the preload route emphasizes reading seeded cache data, tracking validation state, and triggering revalidation manually.
Sources: e2e/site/app/rsc-unstable-preload-no-suspense/client.tsx
Running and Adapting the Example
The official example flow for the Suspense example is to download the example folder from the repository, install dependencies, and run the development server with either Yarn or npm. In a local checkout of this repository, the same conceptual workflow is to run the example or E2E app, open the Suspense route, and observe the fallback first and the resolved data afterward. When adapting the pattern, start with the simplest version: a client component, a stable SWR key, a fetcher that returns a promise, suspense: true, and a parent <Suspense> fallback.
Use promise-backed fallback only when you specifically need to seed SWR with asynchronous fallback data through SWRConfig. In that case, keep the promise stable across renders, as the E2E routes do with useState, and keep the provider value stable with useMemo. If the promise can reject, place an error boundary near the Suspense boundary so the user sees a controlled error state rather than an uncaught render failure. If multiple children need the same result, give them the same key under the same provider instead of duplicating the asynchronous work.
A practical next step is to compare this page with the API references for useSWR and SWRConfig, then read the troubleshooting page for Suspense and server-rendering edge cases. Together, those pages explain which options belong on the hook, which values belong in the provider, and when a Next.js app should prefer fallback data, client-only Suspense, or cache preloading for a particular route.
Sources: README.md, e2e/site/app/render-suspense-avoid-rerender/page.tsx, e2e/site/app/render-promise-suspense-resolve/page.tsx