Skip to content

Commit b369dca

Browse files
authored
feat: Add strictServerPrefetchWarning (#4183)
* add missingPreloadWarning * add warning * update * update * update * lint error * lint error
1 parent e5d54c6 commit b369dca

File tree

3 files changed

+136
-39
lines changed

3 files changed

+136
-39
lines changed

src/_internal/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,12 @@ export interface PublicConfiguration<
134134
* @link https://swr.vercel.app/docs/with-nextjs
135135
*/
136136
fallbackData?: Data | Promise<Data>
137+
/**
138+
* warns when preload data is missing for a given key, this includes fallback
139+
* data, preload calls, or initial data from the cache provider
140+
* @defaultValue false
141+
*/
142+
strictServerPrefetchWarning?: boolean
137143
/**
138144
* the fetcher function
139145
*/

src/index/use-swr.ts

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ import {
2222
internalMutate,
2323
revalidateEvents,
2424
mergeObjects,
25-
isPromiseLike
25+
isPromiseLike,
26+
noop
2627
} from '../_internal'
2728
import type {
2829
State,
@@ -105,7 +106,8 @@ export const useSWRHandler = <Data = any, Error = any>(
105106
refreshInterval,
106107
refreshWhenHidden,
107108
refreshWhenOffline,
108-
keepPreviousData
109+
keepPreviousData,
110+
strictServerPrefetchWarning
109111
} = config
110112

111113
const [EVENT_REVALIDATORS, MUTATION, FETCH, PRELOAD] = SWRGlobalState.get(
@@ -286,6 +288,35 @@ export const useSWRHandler = <Data = any, Error = any>(
286288
: cachedData
287289
: data
288290

291+
const hasKeyButNoData = key && isUndefined(data)
292+
293+
// Note: the conditionally hook call is fine because the environment
294+
// `IS_SERVER` never changes.
295+
const isHydration =
296+
!IS_SERVER &&
297+
// eslint-disable-next-line react-hooks/rules-of-hooks
298+
useSyncExternalStore(
299+
() => noop,
300+
() => false,
301+
() => true
302+
)
303+
304+
// During the initial SSR render, warn if the key has no data pre-fetched via:
305+
// - fallback data
306+
// - preload calls
307+
// - initial data from the cache provider
308+
// We only warn once for each key during SSR.
309+
if (
310+
strictServerPrefetchWarning &&
311+
isHydration &&
312+
!suspense &&
313+
hasKeyButNoData
314+
) {
315+
console.warn(
316+
`Missing pre-initiated data for serialized key "${key}" during server-side rendering. Data fethcing should be initiated on the server and provided to SWR via fallback data. You can set "strictServerPrefetchWarning: false" to disable this warning.`
317+
)
318+
}
319+
289320
// - Suspense mode and there's stale data for the initial render.
290321
// - Not suspense mode and there is no fallback data and `revalidateIfStale` is enabled.
291322
// - `revalidateIfStale` is enabled but `data` is not defined.
@@ -714,7 +745,6 @@ export const useSWRHandler = <Data = any, Error = any>(
714745
// If there is no `error`, the `revalidation` promise needs to be thrown to
715746
// the suspense boundary.
716747
if (suspense) {
717-
const hasKeyButNoData = key && isUndefined(data)
718748
// SWR should throw when trying to use Suspense on the server with React 18,
719749
// without providing any fallback data. This causes hydration errors. See:
720750
// https://github.com/vercel/swr/issues/1832

test/use-swr-server.test.tsx

Lines changed: 97 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -14,50 +14,111 @@ async function withServer(runner: () => Promise<void>) {
1414
}
1515

1616
describe('useSWR - SSR', () => {
17-
beforeAll(() => {
18-
// Store the original window object
19-
// @ts-expect-error
20-
global.window.Deno = '1'
17+
describe('IS_SERVER flag', () => {
18+
beforeAll(() => {
19+
// Store the original window object
20+
// @ts-expect-error
21+
global.window.Deno = '1'
2122

22-
// Mock window to undefined
23-
// delete global.window;
24-
})
23+
// Mock window to undefined
24+
// delete global.window;
25+
})
26+
27+
afterAll(() => {
28+
// Restore window back to its original value
29+
// @ts-expect-error
30+
delete global.window.Deno
31+
})
32+
33+
it('should enable the IS_SERVER flag - suspense on server without fallback', async () => {
34+
await withServer(async () => {
35+
jest.spyOn(console, 'error').mockImplementation(() => {})
36+
const useSWR = (await import('swr')).default
2537

26-
afterAll(() => {
27-
// Restore window back to its original value
28-
// @ts-expect-error
29-
delete global.window.Deno
38+
const key = Math.random().toString()
39+
40+
const Page = () => {
41+
const { data } = useSWR(key, () => 'SWR', {
42+
suspense: true
43+
})
44+
return <div>{data || 'empty'}</div>
45+
}
46+
47+
render(
48+
<ErrorBoundary
49+
fallbackRender={({ error }) => {
50+
console.error(error)
51+
return <div>{error.message}</div>
52+
}}
53+
>
54+
<Suspense>
55+
<Page />
56+
</Suspense>
57+
</ErrorBoundary>
58+
)
59+
60+
await screen.findByText(
61+
'Fallback data is required when using Suspense in SSR.'
62+
)
63+
})
64+
})
3065
})
31-
it('should enable the IS_SERVER flag - suspense on server without fallback', async () => {
32-
await withServer(async () => {
33-
jest.spyOn(console, 'error').mockImplementation(() => {})
34-
const useSWR = (await import('swr')).default
3566

36-
const key = Math.random().toString()
67+
describe('strictServerPrefetchWarning', () => {
68+
it('should show console warning on when strictServerPrefetchWarning is enabled', async () => {
69+
await withServer(async () => {
70+
const warnings: string[] = []
3771

38-
const Page = () => {
39-
const { data } = useSWR(key, () => 'SWR', {
40-
suspense: true
72+
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(msg => {
73+
warnings.push(msg as string)
4174
})
42-
return <div>{data || 'empty'}</div>
43-
}
44-
45-
render(
46-
<ErrorBoundary
47-
fallbackRender={({ error }) => {
48-
console.error(error)
49-
return <div>{error.message}</div>
50-
}}
51-
>
52-
<Suspense>
75+
76+
const { default: useSWR, SWRConfig } = await import('swr')
77+
78+
let resolve: (() => void) | null = null
79+
const promise = new Promise<void>(r => {
80+
resolve = r
81+
})
82+
83+
const Page = () => {
84+
useSWR('ssr:1', () => 'SWR')
85+
useSWR('ssr:2', () => 'SWR')
86+
useSWR('ssr:3', () => 'SWR', { strictServerPrefetchWarning: false })
87+
useSWR('ssr:4', () => 'SWR', { fallbackData: 'SWR' })
88+
useSWR('ssr:5', () => 'SWR')
89+
90+
resolve!()
91+
92+
return null
93+
}
94+
95+
render(
96+
<SWRConfig
97+
value={{
98+
strictServerPrefetchWarning: true,
99+
fallback: {
100+
'ssr:5': 'SWR'
101+
}
102+
}}
103+
>
53104
<Page />
54-
</Suspense>
55-
</ErrorBoundary>
56-
)
105+
</SWRConfig>,
106+
{
107+
hydrate: true
108+
}
109+
)
110+
111+
await promise
112+
113+
expect(warnings).toMatchInlineSnapshot(`
114+
[
115+
"Missing pre-initiated data for serialized key "ssr:1" during server-side rendering. Data fethcing should be initiated on the server and provided to SWR via fallback data. You can set "strictServerPrefetchWarning: false" to disable this warning.",
116+
"Missing pre-initiated data for serialized key "ssr:2" during server-side rendering. Data fethcing should be initiated on the server and provided to SWR via fallback data. You can set "strictServerPrefetchWarning: false" to disable this warning.",
117+
]
118+
`)
57119

58-
await screen.findByText(
59-
'Fallback data is required when using Suspense in SSR.'
60-
)
120+
warnSpy.mockClear()
121+
})
61122
})
62123
})
63124
})

0 commit comments

Comments
 (0)