Suspense and Server Rendering Edge Cases
Purpose and Scope
This page helps diagnose SWR behavior at the boundary between React Suspense, server rendering, streaming hydration, and React Server Component preloading. In these modes, data fetching is not only a hook concern: rendering may pause behind a Suspense fallback, server output may hydrate in multiple phases, and preloaded data may need to cross from a server component into a client component. The goal is to turn the E2E scenarios into practical troubleshooting guidance for application authors and maintainers who need to recognize whether an observed fallback, warning, or hydration sequence is expected.
SWR’s public examples describe Suspense as the mode where React Suspense coordinates loading UI, and the server-render example describes using server-fetched data as fallbackData so the client can render immediately and then revalidate. The E2E suite exercises the same concepts under more exact conditions: promise fallback values, undefined keys, key changes, cached data, streaming SSR, and missing server-prefetch warnings. When a behavior seems surprising, first identify which rendering contract is in play: no key means no request, Suspense means pending promises can delay content, fallback data means the client starts with supplied data, and RSC preload means data is initiated before the client hook reads it.
Sources: e2e/test/suspense-scenarios.test.ts, e2e/test/suspense-undefined-key.test.ts, e2e/test/suspense-fallback.test.ts, e2e/test/stream-ssr.test.ts, e2e/test/server-prefetch-warning.test.ts, e2e/site/app/rsc-unstable-preload/page.tsx, e2e/site/app/rsc-unstable-preload-no-suspense/page.tsx, e2e/site/app/rsc-unstable-preload-conditional/page.tsx
Relevant Source Files
e2e/test/suspense-scenarios.test.tsgroups the broader Suspense regression cases, including promise fallback resolution, key changes, multiple resources, synchronous data, errors, cached data withrevalidateIfStale: false, and initial data.e2e/test/suspense-undefined-key.test.tsverifies that an undefined key renders non-suspense empty content first, then shows fallback and fetched data after the key becomes defined.e2e/test/suspense-fallback.test.tsisolates the promise fallback case and asserts that an async fallback value resolves before the expected text appears.e2e/test/stream-ssr.test.tsvalidates basic streaming SSR and partial hydration, including the initialundefinedstate, finalSSR Worksstate, history output, and absence of browser error logs.e2e/test/server-prefetch-warning.test.tsdefines and asserts the strict server-prefetch warning emitted when serialized server keys are not pre-initiated with fallback data.e2e/site/app/rsc-unstable-preload/page.tsxdemonstrates an App Router server component that opts into dynamic rendering, callspreload, casts the result toCacheData<string>, and passes it to a client root.e2e/site/app/rsc-unstable-preload-no-suspense/page.tsxuses the same server preload shape for a no-Suspense client path, proving that the handoff is not limited to Suspense UI.e2e/site/app/rsc-unstable-preload-conditional/page.tsxconditionally callspreloadbased onsearchParamsand documents the important non-leakage constraint between requests.
Suspense Fallbacks and Key State
A common Suspense confusion is treating every missing value as a loading state. The undefined-key scenario shows a different contract: when the key is undefined, SWR should not enter the Suspense fetch path for that resource. The page first renders empty, then the user toggles the key into a defined state, at which point the Suspense fallback appears and eventually gives way to fetched SWR content. If an app shows a fallback before a key exists, check whether the key expression is actually undefined, null, or otherwise conditional in the way the component author expects.
The fallback scenarios also distinguish asynchronous and synchronous resolution. One test navigates to a promise fallback route and waits for async promise, proving that promise-backed fallback values can be part of the rendered result once they resolve. Another scenario expects a fallback to appear before resolved data renders, while the non-promise scenario expects no fallback at all and directly observes hello. This distinction matters when replacing a mock fetcher with a real fetcher: a synchronously available cache value can bypass fallback, while a pending promise keeps React inside the Suspense boundary.
Multiple-resource Suspense boundaries add another timing edge case. The E2E suite keeps the fallback visible after an intermediate wait and only expects final data after all relevant resources resolve. In application terms, a boundary represents the readiness of everything suspended below it, not just the fastest request. If one component under the same boundary is still waiting, the fallback can remain visible even though another SWR request has already resolved. Split boundaries when independently revealing partial content is more important than a single coordinated loading state.
Sources: e2e/test/suspense-scenarios.test.ts, e2e/test/suspense-undefined-key.test.ts, e2e/test/suspense-fallback.test.ts
Key Changes, Cached Data, and Error Boundaries
Key changes are intentionally tested because they combine cache identity with Suspense timing. One scenario starts with data for an initial key, toggles to an updated key, shows fallback during the transition, and then renders the updated data. Another changes a key while the resolved data value is identical, and still expects the rendered counter to advance. These cases are useful when debugging a component that appears stuck: inspect whether the serialized key changed, whether the displayed data is coincidentally equal, and whether the component has other state proving a rerender occurred.
Cached data changes the interpretation of loading. The no-revalidate scenario expects data: cached immediately and continues to expect that same value after a delay when revalidateIfStale is false. That behavior is different from a failed fetch or a missing key; it is an explicit configuration choice saying cached data is acceptable without stale revalidation. When diagnosing why a Suspense boundary did not reappear, check whether the cache already contains data and whether revalidation flags are suppressing a new request.
Errors under Suspense should be handled by an error boundary rather than by treating the fallback as the final state. The test suite includes a case that first observes the fallback and then observes an error element. That sequence means a loading UI may be visible before the error boundary takes over. If an app never leaves fallback during an error path, verify that the fetcher rejects, that an error boundary surrounds the Suspense subtree, and that the test or user flow waits for the error UI rather than only checking the initial pending state.
Sources: e2e/test/suspense-scenarios.test.ts
Streaming SSR and Hydration Expectations
Streaming SSR introduces a different shape of evidence: the browser may first see server-rendered output with undefined data and later see hydrated content with resolved data. The basic SSR test expects result:undefined, then result:SSR Works, and finally a history of [null,"SSR Works"], while also capturing window.onerror and asserting no errors were logged. This is a useful baseline for troubleshooting: an initial undefined render is not automatically a bug if the stream hydrates cleanly and the final data state appears without client-side errors.
Partial hydration extends the same pattern to more than one client island. The E2E test expects both the first and delayed-hydration sections to begin with undefined data, then both to show SSR Works, and finally both histories to record the transition from null to the resolved value. If only the first island updates, the issue may be isolated to the delayed component boundary or its hydration timing rather than to SWR globally. When reproducing these bugs, capture console errors early, before navigation, because hydration failures often surface as browser errors rather than failed DOM expectations.
Sources: e2e/test/stream-ssr.test.ts
Server Prefetch Warnings and RSC Preload
The strict server-prefetch warning is explicit about the remediation: data fetching should be initiated on the server and provided to SWR through fallback data. The warning message includes the serialized key and notes that strictServerPrefetchWarning: false can disable the warning. The E2E test collects console warnings after hydration and expects exactly two warnings for ssr:1 and ssr:2. If this warning appears in an SSR route, treat it as a signal that the client hook is reading a key whose data was not pre-initiated for the server render.
The RSC preload pages show the server-side handoff pattern used by the App Router E2E site. Each page imports connection from next/server, opts into dynamic rendering with await connection(), calls SWR’s preload with a shared key and async getServerData, casts the result to CacheData<string>, and passes cacheData to ClientRoot. The inline comments clarify a current typing detail: runtime resolves the react-server export, while the app type checker still sees the default client preload signature. This is why the cast appears in these examples.
The conditional RSC preload page captures an especially important server-rendering edge case: global state on the server can be shared across requests, so data preloaded for one request must not leak into a later request that did not request preload. The page reads searchParams, only calls preload when the preload parameter is present, and otherwise passes an empty cache data object. When debugging intermittent RSC data leakage, verify that request-specific preload decisions produce request-specific cache data and that client roots are not reusing previous server-loaded results.
Sources: e2e/test/server-prefetch-warning.test.ts, e2e/site/app/rsc-unstable-preload/page.tsx, e2e/site/app/rsc-unstable-preload-no-suspense/page.tsx, e2e/site/app/rsc-unstable-preload-conditional/page.tsx
Troubleshooting Checklist
Use the rendering mode to choose the next diagnostic step. For Suspense routes, check the key first, then the cache, then the fetcher promise state, then the placement of Suspense and error boundaries. For SSR routes, check whether initial undefined output is followed by the resolved value and whether the browser logs hydration errors. For strict server-prefetch warnings, identify the serialized keys in the warning and ensure the matching server path provides pre-initiated data or fallback data. For RSC preload routes, check whether preload is called on the server for the current request and whether the resulting CacheData is passed to the client root.
A minimal reproduction should preserve the same observable contract as the E2E tests: navigate with a committed document, assert the initial fallback or undefined state, then assert the final data, warning, or error-boundary state. Avoid asserting only the first visible loading UI, because several correct paths intentionally pass through fallback before data or errors appear. Next, read the Suspense, server-render, preload, and configuration API pages to connect these edge cases to the public options that control them, especially suspense, fallbackData, revalidateIfStale, preload, and strictServerPrefetchWarning.