Skip to content

Commit e38e3dd

Browse files
authored
Adjust SSG Loading Behavior (vercel#10510)
* Adjust SSG Loading Behavior * Update expected preview behavior * Rename two corrections * Only use skeleton in production for now * Fix "should SSR SPR page correctly" test * fix tests * fix trailing comment letter * disable test for now
1 parent 5f04144 commit e38e3dd

File tree

7 files changed

+139
-157
lines changed

7 files changed

+139
-157
lines changed

packages/next/build/index.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -657,15 +657,18 @@ export default async function build(dir: string, conf = null): Promise<void> {
657657
// n.b. we cannot handle this above in combinedPages because the dynamic
658658
// page must be in the `pages` array, but not in the mapping.
659659
exportPathMap: (defaultMap: any) => {
660-
// Generate fallback for dynamically routed pages to use as
661-
// the loading state for pages while the data is being populated
660+
// Dynamically routed pages should be prerendered to be used as
661+
// a client-side skeleton (fallback) while data is being fetched.
662+
// This ensures the end-user never sees a 500 or slow response from the
663+
// server.
662664
//
663665
// Note: prerendering disables automatic static optimization.
664666
ssgPages.forEach(page => {
665667
if (isDynamicRoute(page)) {
666668
tbdPrerenderRoutes.push(page)
667-
// set __nextFallback query so render doesn't call
668-
// getStaticProps/getServerProps
669+
670+
// Override the rendering for the dynamic page to be treated as a
671+
// fallback render.
669672
defaultMap[page] = { page, query: { __nextFallback: true } }
670673
}
671674
})

packages/next/client/index.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,9 +97,10 @@ class Container extends React.Component {
9797
})
9898
}
9999

100-
// If page was exported and has a querystring
101-
// If it's a dynamic route or has a querystring
102-
// if it's a fallback page
100+
// We need to replace the router state if:
101+
// - the page was (auto) exported and has a query string or search (hash)
102+
// - it was auto exported and is a dynamic route (to provide params)
103+
// - if it is a client-side skeleton (fallback render)
103104
if (
104105
router.isSsr &&
105106
(isFallback ||
@@ -121,6 +122,10 @@ class Container extends React.Component {
121122
// client-side hydration. Your app should _never_ use this property.
122123
// It may change at any time without notice.
123124
_h: 1,
125+
// Fallback pages must trigger the data fetch, so the transition is
126+
// not shallow.
127+
// Other pages (strictly updating query) happens shallowly, as data
128+
// requirements would already be present.
124129
shallow: !isFallback,
125130
}
126131
)

packages/next/next-server/lib/router/router.ts

Lines changed: 54 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,12 @@ type BeforePopStateCallback = (state: any) => boolean
6464

6565
type ComponentLoadCancel = (() => void) | null
6666

67-
const fetchNextData = (
67+
function fetchNextData(
6868
pathname: string,
6969
query: ParsedUrlQuery | null,
70+
isServerRender: boolean,
7071
cb?: (...args: any) => any
71-
) => {
72+
) {
7273
return fetch(
7374
formatWithValidation({
7475
// @ts-ignore __NEXT_DATA__
@@ -78,15 +79,22 @@ const fetchNextData = (
7879
)
7980
.then(res => {
8081
if (!res.ok) {
81-
const error = new Error(`Failed to load static props`)
82-
;(error as any).statusCode = res.status
83-
throw error
82+
throw new Error(`Failed to load static props`)
8483
}
8584
return res.json()
8685
})
8786
.then(data => {
8887
return cb ? cb(data) : data
8988
})
89+
.catch((err: Error) => {
90+
// We should only trigger a server-side transition if this was caused
91+
// on a client-side transition. Otherwise, we'd get into an infinite
92+
// loop.
93+
if (!isServerRender) {
94+
;(err as any).code = 'PAGE_LOAD_ERROR'
95+
}
96+
throw err
97+
})
9098
}
9199

92100
export default class Router implements BaseRouter {
@@ -391,65 +399,31 @@ export default class Router implements BaseRouter {
391399

392400
// If shallow is true and the route exists in the router cache we reuse the previous result
393401
this.getRouteInfo(route, pathname, query, as, shallow).then(routeInfo => {
394-
let emitHistory = false
395-
396-
const doRouteChange = (routeInfo: RouteInfo, complete: boolean) => {
397-
const { error } = routeInfo
398-
399-
if (error && error.cancelled) {
400-
return resolve(false)
401-
}
402+
const { error } = routeInfo
402403

403-
if (!emitHistory) {
404-
emitHistory = true
405-
Router.events.emit('beforeHistoryChange', as)
406-
this.changeState(method, url, addBasePath(as), options)
407-
}
404+
if (error && error.cancelled) {
405+
return resolve(false)
406+
}
408407

409-
if (process.env.NODE_ENV !== 'production') {
410-
const appComp: any = this.components['/_app'].Component
411-
;(window as any).next.isPrerendered =
412-
appComp.getInitialProps === appComp.origGetInitialProps &&
413-
!(routeInfo.Component as any).getInitialProps
414-
}
408+
Router.events.emit('beforeHistoryChange', as)
409+
this.changeState(method, url, addBasePath(as), options)
415410

416-
this.set(route, pathname, query, as, routeInfo)
411+
if (process.env.NODE_ENV !== 'production') {
412+
const appComp: any = this.components['/_app'].Component
413+
;(window as any).next.isPrerendered =
414+
appComp.getInitialProps === appComp.origGetInitialProps &&
415+
!(routeInfo.Component as any).getInitialProps
416+
}
417417

418-
if (complete) {
419-
if (error) {
420-
Router.events.emit('routeChangeError', error, as)
421-
throw error
422-
}
418+
this.set(route, pathname, query, as, routeInfo)
423419

424-
Router.events.emit('routeChangeComplete', as)
425-
resolve(true)
426-
}
420+
if (error) {
421+
Router.events.emit('routeChangeError', error, as)
422+
throw error
427423
}
428424

429-
if ((routeInfo as any).dataRes) {
430-
const dataRes = (routeInfo as any).dataRes as Promise<RouteInfo>
431-
432-
// to prevent a flash of the fallback page we delay showing it for
433-
// 110ms and race the timeout with the data response. If the data
434-
// beats the timeout we skip showing the fallback
435-
Promise.race([
436-
new Promise(resolve => setTimeout(() => resolve(false), 110)),
437-
dataRes,
438-
])
439-
.then((data: any) => {
440-
if (!data) {
441-
// data didn't win the race, show fallback
442-
doRouteChange(routeInfo, false)
443-
}
444-
return dataRes
445-
})
446-
.then(finalData => {
447-
// render with the data and complete route change
448-
doRouteChange(finalData as RouteInfo, true)
449-
}, reject)
450-
} else {
451-
doRouteChange(routeInfo, true)
452-
}
425+
Router.events.emit('routeChangeComplete', as)
426+
return resolve(true)
453427
}, reject)
454428
})
455429
}
@@ -518,51 +492,25 @@ export default class Router implements BaseRouter {
518492
}
519493
}
520494

521-
const isSSG = (Component as any).__N_SSG
522-
const isSSP = (Component as any).__N_SSP
523-
524-
const handleData = (props: any) => {
495+
return this._getData<RouteInfo>(() =>
496+
(Component as any).__N_SSG
497+
? this._getStaticData(as)
498+
: (Component as any).__N_SSP
499+
? this._getServerData(as)
500+
: this.getInitialProps(
501+
Component,
502+
// we provide AppTree later so this needs to be `any`
503+
{
504+
pathname,
505+
query,
506+
asPath: as,
507+
} as any
508+
)
509+
).then(props => {
525510
routeInfo.props = props
526511
this.components[route] = routeInfo
527512
return routeInfo
528-
}
529-
530-
// resolve with fallback routeInfo and promise for data
531-
if (isSSG || isSSP) {
532-
const dataMethod = () =>
533-
isSSG ? this._getStaticData(as) : this._getServerData(as)
534-
535-
const retry = (error: Error & { statusCode: number }) => {
536-
if (error.statusCode === 404) {
537-
throw error
538-
}
539-
return dataMethod()
540-
}
541-
542-
return Promise.resolve({
543-
...routeInfo,
544-
props: {},
545-
dataRes: this._getData(() =>
546-
dataMethod()
547-
// we retry for data twice unless we get a 404
548-
.catch(retry)
549-
.catch(retry)
550-
.then((props: any) => handleData(props))
551-
),
552-
})
553-
}
554-
555-
return this._getData<RouteInfo>(() =>
556-
this.getInitialProps(
557-
Component,
558-
// we provide AppTree later so this needs to be `any`
559-
{
560-
pathname,
561-
query,
562-
asPath: as,
563-
} as any
564-
)
565-
).then(props => handleData(props))
513+
})
566514
})
567515
.catch(err => {
568516
return new Promise(resolve => {
@@ -765,13 +713,18 @@ export default class Router implements BaseRouter {
765713

766714
return process.env.NODE_ENV === 'production' && this.sdc[pathname]
767715
? Promise.resolve(this.sdc[pathname])
768-
: fetchNextData(pathname, null, data => (this.sdc[pathname] = data))
716+
: fetchNextData(
717+
pathname,
718+
null,
719+
this.isSsr,
720+
data => (this.sdc[pathname] = data)
721+
)
769722
}
770723

771724
_getServerData = (asPath: string): Promise<object> => {
772725
let { pathname, query } = parse(asPath, true)
773726
pathname = prepareRoute(pathname!)
774-
return fetchNextData(pathname, query)
727+
return fetchNextData(pathname, query, this.isSsr)
775728
}
776729

777730
getInitialProps(

packages/next/next-server/server/next-server.ts

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -992,22 +992,42 @@ export default class Server {
992992
return { html, pageData, sprRevalidate }
993993
})
994994

995-
// render fallback if for a preview path or a non-seeded dynamic path
995+
const isProduction = !this.renderOpts.dev
996996
const isDynamicPathname = isDynamicRoute(pathname)
997+
const didRespond = isResSent(res)
998+
// const isForcedBlocking =
999+
// req.headers['X-Prerender-Bypass-Mode'] !== 'Blocking'
1000+
1001+
// When we did not respond from cache, we need to choose to block on
1002+
// rendering or return a skeleton.
1003+
//
1004+
// * Data requests always block.
1005+
//
1006+
// * Preview mode toggles all pages to be resolved in a blocking manner.
1007+
//
1008+
// * Non-dynamic pages should block (though this is an be an impossible
1009+
// case in production).
1010+
//
1011+
// * Dynamic pages should return their skeleton, then finish the data
1012+
// request on the client-side.
1013+
//
9971014
if (
998-
!isResSent(res) &&
1015+
!didRespond &&
9991016
!isDataReq &&
1000-
((isPreviewMode &&
1001-
// A header can opt into the blocking behavior.
1002-
req.headers['X-Prerender-Bypass-Mode'] !== 'Blocking') ||
1003-
isDynamicPathname)
1017+
!isPreviewMode &&
1018+
isDynamicPathname &&
1019+
// TODO: development should trigger fallback when the path is not in
1020+
// `getStaticPaths`, for now, let's assume it is.
1021+
isProduction
10041022
) {
1005-
let html = ''
1023+
let html: string
10061024

1007-
const isProduction = !this.renderOpts.dev
1008-
if (isProduction && (isDynamicPathname || !isPreviewMode)) {
1025+
// Production already emitted the fallback as static HTML.
1026+
if (isProduction) {
10091027
html = await getFallback(pathname)
1010-
} else {
1028+
}
1029+
// We need to generate the fallback on-demand for development.
1030+
else {
10111031
query.__nextFallback = 'true'
10121032
if (isLikeServerless) {
10131033
this.prepareServerlessUrl(req, query)

test/integration/dynamic-routing/test/index.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -275,15 +275,15 @@ function runTests(dev) {
275275
})
276276
})
277277

278-
it('[predefined ssg: prerendered catch all] should pass param in getInitialProps during SSR', async () => {
278+
it('[predefined ssg: prerendered catch all] should pass param in getStaticProps during SSR', async () => {
279279
const data = await renderViaHTTP(
280280
appPort,
281281
`/_next/data/${buildId}/p1/p2/predefined-ssg/one-level.json`
282282
)
283283
expect(JSON.parse(data).pageProps.params).toEqual({ rest: ['one-level'] })
284284
})
285285

286-
it('[predefined ssg: prerendered catch all] should pass params in getInitialProps during SSR', async () => {
286+
it('[predefined ssg: prerendered catch all] should pass params in getStaticProps during SSR', async () => {
287287
const data = await renderViaHTTP(
288288
appPort,
289289
`/_next/data/${buildId}/p1/p2/predefined-ssg/1st-level/2nd-level.json`

test/integration/prerender-preview/test/index.test.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ function runTests() {
8181
cookie.serialize('__next_preview_data', cookies[1].__next_preview_data)
8282
})
8383

84-
it('should return fallback page on preview request', async () => {
84+
it('should not return fallback page on preview request', async () => {
8585
const res = await fetchViaHTTP(
8686
appPort,
8787
'/',
@@ -91,8 +91,8 @@ function runTests() {
9191
const html = await res.text()
9292

9393
const { nextData, pre } = getData(html)
94-
expect(nextData).toMatchObject({ isFallback: true })
95-
expect(pre).toBe('Has No Props')
94+
expect(nextData).toMatchObject({ isFallback: false })
95+
expect(pre).toBe('true and {"lets":"goooo"}')
9696
})
9797

9898
it('should return cookies to be expired on reset request', async () => {
@@ -136,8 +136,8 @@ function runTests() {
136136
it('should fetch preview data', async () => {
137137
await browser.get(`http://localhost:${appPort}/`)
138138
await browser.waitForElementByCss('#props-pre')
139-
expect(await browser.elementById('props-pre').text()).toBe('Has No Props')
140-
await new Promise(resolve => setTimeout(resolve, 2000))
139+
// expect(await browser.elementById('props-pre').text()).toBe('Has No Props')
140+
// await new Promise(resolve => setTimeout(resolve, 2000))
141141
expect(await browser.elementById('props-pre').text()).toBe(
142142
'true and {"client":"mode"}'
143143
)

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy