Suspense, SSR, and React Server Components
Purpose and Scope
This page explains how SWR participates in React rendering modes where data may be missing at first render: Suspense, concurrent rendering, server-side rendering, streaming hydration, and React Server Components. In normal client rendering, useSWR can return undefined data while a fetcher is pending. With Suspense enabled, SWR instead integrates with React by letting a surrounding <Suspense> boundary show its fallback until the data path can continue. The tests make this behavior concrete by rendering fallback UI first, then asserting that resolved data replaces it once the SWR fetcher completes.
The important mental model is that Suspense changes when the component is allowed to render, but it does not remove SWR's cache-first, revalidation-oriented behavior. Cached values can still be used immediately, errors still flow to an error boundary when a Suspense tree cannot render successfully, and per-hook options such as revalidateIfStale still affect whether cached data triggers a background fetch. The same hook remains the developer-facing primitive; the rendering environment determines whether the first missing value becomes loading state, a thrown thenable, or a server restriction.
Sources: test/use-swr-suspense.test.tsx, e2e/test/suspense-scenarios.test.ts
Relevant Source Files
test/use-swr-suspense.test.tsx- exercises the unit-level Suspense contract for fallback rendering, multiple suspended resources, synchronous fetchers, errors, cached data with errors, cached data without stale revalidation, and key changes.test/use-swr-concurrent-rendering.test.tsx- verifies that SWR still fetches and commits data in a concurrent rendering setup, and records an additional skipped transition-oriented scenario for Suspense interaction.test/use-swr-server.test.tsx- covers server-like environments, including server-sidepreload, the SSR Suspense requirement for fallback data, and fulfilled promise fallback data.test/use-swr-streaming-ssr.test.tsx- covers hydration matching for SSR output and documents the partially hydrated streaming case that requires browser-level validation.e2e/test/suspense-scenarios.test.ts- validates Suspense behavior in Playwright against real application routes, including fallback timing, key changes, initial data, and error boundaries.e2e/test/stream-ssr.test.ts- validates streaming SSR pages in a browser and checks that hydration produces expected history without console errors.package.json- declares the public package entrypoints, includingreact-serverexport conditions for the root package,swr/infinite, andswr/_internal.
Sources: test/use-swr-suspense.test.tsx, test/use-swr-concurrent-rendering.test.tsx, test/use-swr-server.test.tsx, test/use-swr-streaming-ssr.test.tsx, e2e/test/suspense-scenarios.test.ts, e2e/test/stream-ssr.test.ts, package.json
Suspense Behavior
To enable Suspense for a hook, pass suspense: true in the SWR options and render the component below a React <Suspense> boundary. The unit tests show a component using useSWR(key, fetcher, { suspense: true }) and a fallback of <div>fallback</div>. The fallback is visible during hydration or pending data resolution, and the final resolved value appears after the fetcher returns. This establishes the user-facing contract: Suspense mode lets React own the loading placeholder while SWR owns cache lookup, fetching, deduplication, and eventual data publication.
SWR's Suspense path is not limited to promise-returning fetchers. A test uses a fetcher that returns the string hello synchronously while Suspense is enabled, and the component renders the value without needing a visible fallback period. This matters when a project has mixed data sources: some fetchers may return already-computed values, cached values, or mock values in tests, while others return network promises. Suspense does not require every fetcher to be asynchronous; it requires SWR to decide whether rendering must pause for unresolved data.
Multiple SWR hooks inside one Suspense boundary are coordinated by React's normal Suspense semantics. The tests render a section that reads two SWR resources, each delayed, then assert that the fallback remains visible until both values have resolved and the computed sum can render. The Playwright scenarios repeat this at route level by checking that the fallback is still visible after the first wait interval and only the final data appears later. When designing a page, place the boundary at the level where it is acceptable to wait for all enclosed SWR resources.
Errors in Suspense mode follow React's Suspense and error boundary model. The unit tests wrap a Suspense boundary inside an ErrorBoundary; a fetcher that resolves to an error first shows the fallback and then renders the error boundary. That distinction is useful for UI design: the Suspense fallback is for pending data, while the error boundary is for failed rendering caused by a rejected or error-producing data path. If the app needs local retry controls, put them in the error boundary fallback or use non-Suspense loading and error rendering for that part of the interface.
Sources: test/use-swr-suspense.test.tsx, e2e/test/suspense-scenarios.test.ts
Cache, Key Changes, and Previous Values under Suspense
Suspense does not mean SWR ignores cache state. The test suite mutates a key to cached data before rendering a Suspense hook, then verifies the component can render cached data immediately while a later failed revalidation surfaces an error next to that cached value. Another route-level scenario is skipped in Playwright, but it documents the same desired shape: no fallback when cached data is already available, followed by an error update. The practical takeaway is that Suspense is most disruptive only when SWR has no usable value for the key being rendered.
The revalidateIfStale option remains important with Suspense. A Playwright scenario visits a route that starts with cached data and revalidateIfStale disabled, asserts data: cached, waits, and asserts the value does not change. This is the same option developers use outside Suspense, but the visible outcome is especially important in Suspense trees: disabling stale revalidation can prevent an otherwise cached screen from entering a loading boundary because SWR has been told not to refresh that stale value on mount.
Key changes are also covered as first-class Suspense behavior. E2E tests navigate to routes where a key changes, a fallback becomes visible again, and the final data updates to the new key's result. Another scenario verifies that rendering remains correct even when the data value is identical across a key change but an adjacent counter changes. These tests protect against an easy mistake in Suspense UIs: assuming identical returned data means the key did not matter. In SWR, the key identifies the request and cache entry, so a new key can still drive a new Suspense cycle.
Sources: test/use-swr-suspense.test.tsx, e2e/test/suspense-scenarios.test.ts
Server-Side Rendering and Fallback Data
SWR supports server rendering patterns, but Suspense on the server has a stricter requirement in the tested behavior. In a server-like environment, a unit test imports SWR inside an isolated module registry so the environment is observed at import time, renders a Suspense-enabled hook without fallback data, and expects the error message Fallback data is required when using Suspense in SSR. The meaning is direct: if a Suspense-enabled SWR hook is rendered during SSR, provide data that lets the server output a deterministic result instead of trying to suspend for a client-side fetch.
The official server-render example describes the intended application pattern: fetch data in Next.js server-side data loading, pass it into the component as props, and provide it to SWR through fallbackData. Once the browser takes over, SWR can revalidate against the API and update the DOM if fresher data is available. The tests back the restriction that makes this pattern necessary. Server output must be renderable before client fetches occur, and fallbackData is the hook-level mechanism for giving SWR that first value.
The server test suite also covers preload in a server-like environment. When preload('test-key', fetcher) is imported from swr under the server condition simulated by the test, it returns undefined and does not call the fetcher. That prevents a preload call from becoming an accidental server fetch side effect in environments where SWR should not execute client data loading. Treat preload as a client-side render optimization unless the relevant runtime and entrypoint explicitly support a different behavior.
A subtle SSR Suspense case involves promise-shaped fallback data. The test creates a resolved promise, marks it with React's fulfilled thenable shape, and passes it as fallbackData with suspense: true and revalidateIfStale: false. The assertion is that fulfilled fallback data should not cause SWR to suspend. For application developers, this means that server-provided async values that React has already fulfilled can act as usable fallback data, rather than causing a loading boundary to appear during hydration.
Sources: test/use-swr-server.test.tsx
Streaming SSR and Hydration
Streaming SSR adds a timing problem: different parts of the page may hydrate at different moments, while SWR's client cache can update as soon as one hydrated block fetches data. The unit streaming test renders server markup containing undefined, hydrates a SWR block against that markup, and mocks hydration errors to ensure the SSR result matches during hydration. The goal is not to suppress data updates forever; it is to avoid a hydration mismatch where the client reads newer cache data before React has matched the server's HTML for that part of the tree.
The streaming unit file also documents a partially hydrated scenario as a failing JSDOM test because the behavior needs a real browser. In that scenario, block a hydrates first and updates the client cache, while block b is still being streamed and later hydrates under Suspense. The expected first hydration read for both blocks is still undefined, matching the server result. This comment explains the design constraint: streaming correctness is about preserving each boundary's server snapshot during its own hydration phase, even if other client work has already completed.
The Playwright streaming SSR test provides the browser-level signal for this area. It visits a basic-ssr route and expects the page to show result:undefined, then result:SSR Works, and a history of [null,"SSR Works"] without recorded console errors. It also visits a partially-hydrate route where first and delayed second regions both initially show undefined, later both show SSR Works, and both histories preserve the same two-step transition. This confirms that streaming hydration should be quiet, deterministic, and eventually fresh.
Sources: test/use-swr-streaming-ssr.test.tsx, e2e/test/stream-ssr.test.ts
Concurrent Rendering and React Server Component Entrypoints
SWR is tested against React concurrent rendering with a basic scenario that renders a page, starts with data: while the delayed fetcher is pending, and later renders data:0. The test sets dedupingInterval: 0, making the fetch timing explicit rather than relying on deduped reuse. This is a small but important compatibility signal: concurrent React can start and complete rendering work while SWR still publishes the resolved value through its normal hook state path.
There is also a skipped concurrent transition scenario in the source. It combines a counter updated on an interval, a first SWR fetch that gates a second Suspense-enabled SWR hook, and executeWithoutBatching. Because it is skipped, it should be read as a documented edge scenario rather than a guaranteed behavior claim. It still identifies the kind of integration SWR maintainers are watching: Suspense, transitions, background updates, and revalidation options interacting under concurrent scheduling.
React Server Components are represented in the package metadata through conditional exports rather than the browser Suspense tests. The root swr export includes a react-server condition with ./dist/index/react-server.mjs and matching React Server type declarations. The swr/infinite entrypoint also declares a react-server export, and swr/_internal exposes an internal React Server build. Other public subpackages such as swr/immutable, swr/subscription, and swr/mutation are exported for import and require formats but do not show a react-server condition in the provided package metadata.
For consumers, the practical rule is to import from the public package path and let the framework resolver choose the correct condition. In a Next.js App Router or another RSC-aware bundler, importing from swr can resolve to the React Server build where appropriate. Client components that call hooks such as useSWR still need to run in a client-rendered React environment, while server-compatible entrypoints and helpers are selected by the package export map. Do not hard-code dist paths; rely on swr, swr/infinite, or other public subpaths.
Sources: test/use-swr-concurrent-rendering.test.tsx, package.json
Compact Reference
| Area | Public surface or option | Behavior shown by source evidence |
|---|---|---|
| Suspense hook rendering | useSWR(key, fetcher, { suspense: true }) | Shows a React fallback while unresolved data is pending and renders resolved data afterward. |
| Synchronous fetcher | Fetcher returning a non-promise value | Renders resolved data without requiring a visible fallback. |
| Error handling | <ErrorBoundary> around Suspense tree | Pending UI appears first, then an error boundary handles failed data. |
| Cached data | mutate(key, value) before rendering | Cached data can render immediately while later error state is surfaced. |
| Stale revalidation | revalidateIfStale: false | Cached data can remain stable without an automatic refresh. |
| SSR Suspense | fallbackData with suspense: true | Required for SSR Suspense rendering; missing fallback data produces a specific error. |
| Server preload | preload(key, fetcher) | No-op under the tested server-like environment and does not call the fetcher. |
| Streaming SSR | Hydration against server undefined output | Hydrates without mismatch, then updates to fresh client data. |
| RSC package resolution | react-server export condition | Provided for swr, swr/infinite, and swr/_internal in package metadata. |
Practical Guidance
Use Suspense when a whole region of UI can wait behind a boundary and when a loading fallback is clearer than per-component isLoading checks. Provide error boundaries near Suspense boundaries so data failures have an intentional rendering path. For SSR, do not enable Suspense without initial data; pass server-fetched values through fallbackData or use a non-Suspense rendering path. For streaming SSR, expect the first hydrated read to match server output, then let SWR revalidate and update the UI after hydration.
When working in React Server Component environments, import from public SWR entrypoints and let package conditions resolve the correct build. Keep actual hook usage in client components unless the selected API is explicitly designed for server execution. If you are debugging behavior, reproduce it first with the smallest relevant dimension: Suspense fallback timing, SSR fallback data, streaming hydration, or concurrent scheduling. Then compare it to the tests on this page, because they encode the compatibility contracts maintainers protect.
Related pages: api-use-swr, api-preload, api-swr-config, troubleshooting-suspense-and-server-rendering, example-suspense, example-server-render