diff --git a/e2e/site/app/suspense-undefined-key/page.tsx b/e2e/site/app/suspense-undefined-key/page.tsx new file mode 100644 index 000000000..5c9eed084 --- /dev/null +++ b/e2e/site/app/suspense-undefined-key/page.tsx @@ -0,0 +1,30 @@ +'use client' + +import { Suspense, useReducer } from 'react' +import useSWR from 'swr' + +const fetcher = async (key: string) => { + // Add a small delay to simulate network request + await new Promise(resolve => setTimeout(resolve, 100)) + return 'SWR' +} + +const Section = ({ trigger }: { trigger: boolean }) => { + const { data } = useSWR(trigger ? 'test-key' : undefined, fetcher, { + suspense: true + }) + return
{data || 'empty'}
+} + +export default function Page() { + const [trigger, toggle] = useReducer(x => !x, false) + + return ( +
+ + fallback
}> +
+ + + ) +} diff --git a/e2e/test/suspense-undefined-key.test.ts b/e2e/test/suspense-undefined-key.test.ts new file mode 100644 index 000000000..c618a942c --- /dev/null +++ b/e2e/test/suspense-undefined-key.test.ts @@ -0,0 +1,20 @@ +/* eslint-disable testing-library/prefer-screen-queries */ +import { test, expect } from '@playwright/test' + +test.describe('suspense with undefined key', () => { + test('should render correctly when key is undefined', async ({ page }) => { + await page.goto('./suspense-undefined-key', { waitUntil: 'commit' }) + + // Should show content for undefined key (not suspense) + await expect(page.getByText('empty')).toBeVisible() + + // Click toggle to enable key + await page.getByRole('button', { name: 'toggle' }).click() + + // Should show loading fallback when key becomes defined + await expect(page.getByText('fallback')).toBeVisible() + + // Should eventually show the fetched data + await expect(page.getByText('SWR')).toBeVisible() + }) +}) diff --git a/src/index/use-swr.ts b/src/index/use-swr.ts index eb6a2f480..fe535e794 100644 --- a/src/index/use-swr.ts +++ b/src/index/use-swr.ts @@ -714,14 +714,14 @@ export const useSWRHandler = ( // If there is no `error`, the `revalidation` promise needs to be thrown to // the suspense boundary. if (suspense) { + const hasKeyButNoData = key && isUndefined(data) // SWR should throw when trying to use Suspense on the server with React 18, // without providing any fallback data. This causes hydration errors. See: // https://github.com/vercel/swr/issues/1832 - if (!IS_REACT_LEGACY && IS_SERVER) { + if (!IS_REACT_LEGACY && IS_SERVER && hasKeyButNoData) { throw new Error('Fallback data is required when using Suspense in SSR.') } - const hasKeyButNoData = key && isUndefined(data) // Always update fetcher and config refs even with the Suspense mode. if (hasKeyButNoData) { fetcherRef.current = fetcher