From 6d5c48742b83a7b77b6a98023f7dc549c8f6595d Mon Sep 17 00:00:00 2001 From: Joe Haddad Date: Sat, 15 Feb 2020 13:01:10 -0500 Subject: [PATCH] Implement `isFallback` Router Property (#10539) This adds a `isFallback` property to detect if the page is being rendered in "fallback" mode or normal mode. Accessed via the `useRouter()` hook. --- Closes #10527 --- packages/next/client/index.js | 1 + packages/next/client/router.ts | 9 ++++++++- packages/next/next-server/lib/router/router.ts | 8 ++++++++ packages/next/next-server/server/render.tsx | 14 ++++++++++++-- .../prerender/pages/catchall/[...slug].js | 14 +++++++++++++- test/integration/prerender/test/index.test.js | 17 ++++++++++++++--- 6 files changed, 56 insertions(+), 7 deletions(-) diff --git a/packages/next/client/index.js b/packages/next/client/index.js index ffe66c971534055..c69fedcff8c8403 100644 --- a/packages/next/client/index.js +++ b/packages/next/client/index.js @@ -206,6 +206,7 @@ export default async ({ webpackHMR: passedWebpackHMR } = {}) => { Component, wrapApp, err: initialErr, + isFallback, subscription: ({ Component, props, err }, App) => { render({ App, Component, props, err }) }, diff --git a/packages/next/client/router.ts b/packages/next/client/router.ts index 41d4dad46af9fc4..6de604eada93afc 100644 --- a/packages/next/client/router.ts +++ b/packages/next/client/router.ts @@ -29,7 +29,14 @@ const singletonRouter: SingletonRouterBase = { } // Create public properties and methods of the router in the singletonRouter -const urlPropertyFields = ['pathname', 'route', 'query', 'asPath', 'components'] +const urlPropertyFields = [ + 'pathname', + 'route', + 'query', + 'asPath', + 'components', + 'isFallback', +] const routerEvents = [ 'routeChangeStart', 'beforeHistoryChange', diff --git a/packages/next/next-server/lib/router/router.ts b/packages/next/next-server/lib/router/router.ts index 51090ffd9b90578..6bd26cddd8c1c6e 100644 --- a/packages/next/next-server/lib/router/router.ts +++ b/packages/next/next-server/lib/router/router.ts @@ -49,6 +49,7 @@ export type NextRouter = BaseRouter & | 'prefetch' | 'beforePopState' | 'events' + | 'isFallback' > type RouteInfo = { @@ -122,6 +123,7 @@ export default class Router implements BaseRouter { events: MittEmitter _wrapApp: (App: ComponentType) => any isSsr: boolean + isFallback: boolean static events: MittEmitter = mitt() @@ -137,6 +139,7 @@ export default class Router implements BaseRouter { Component, err, subscription, + isFallback, }: { subscription: Subscription initialProps: any @@ -145,6 +148,7 @@ export default class Router implements BaseRouter { App: ComponentType wrapApp: (App: ComponentType) => any err?: Error + isFallback: boolean } ) { // represents the current component key @@ -180,6 +184,8 @@ export default class Router implements BaseRouter { // back from external site this.isSsr = true + this.isFallback = isFallback + if (typeof window !== 'undefined') { // in order for `e.state` to work on the `onpopstate` event // we have to register the initial route upon initialization @@ -580,6 +586,8 @@ export default class Router implements BaseRouter { as: string, data: RouteInfo ): void { + this.isFallback = false + this.route = route this.pathname = pathname this.query = query diff --git a/packages/next/next-server/server/render.tsx b/packages/next/next-server/server/render.tsx index afaeff62cf4de8e..946d6c0a5a0de97 100644 --- a/packages/next/next-server/server/render.tsx +++ b/packages/next/next-server/server/render.tsx @@ -46,14 +46,22 @@ class ServerRouter implements NextRouter { query: ParsedUrlQuery asPath: string events: any + isFallback: boolean // TODO: Remove in the next major version, as this would mean the user is adding event listeners in server-side `render` method static events: MittEmitter = mitt() - constructor(pathname: string, query: ParsedUrlQuery, as: string) { + constructor( + pathname: string, + query: ParsedUrlQuery, + as: string, + { isFallback }: { isFallback: boolean } + ) { this.route = pathname.replace(/\/$/, '') || '/' this.pathname = pathname this.query = query this.asPath = as + + this.isFallback = isFallback } push(): any { noRouter() @@ -394,7 +402,9 @@ export async function renderToHTML( // @ts-ignore url will always be set const asPath: string = req.url - const router = new ServerRouter(pathname, query, asPath) + const router = new ServerRouter(pathname, query, asPath, { + isFallback: isFallback, + }) const ctx = { err, req: isAutoExport ? undefined : req, diff --git a/test/integration/prerender/pages/catchall/[...slug].js b/test/integration/prerender/pages/catchall/[...slug].js index f1dd8038a6b93f0..c2ff67739caf080 100644 --- a/test/integration/prerender/pages/catchall/[...slug].js +++ b/test/integration/prerender/pages/catchall/[...slug].js @@ -1,4 +1,10 @@ +import { useRouter } from 'next/router' + export async function unstable_getStaticProps({ params: { slug } }) { + if (slug[0] === 'delayby3s') { + await new Promise(resolve => setTimeout(resolve, 3000)) + } + return { props: { slug, @@ -18,4 +24,10 @@ export async function unstable_getStaticPaths() { } } -export default ({ slug }) =>

Hi {slug?.join('/')}

+export default ({ slug }) => { + const { isFallback } = useRouter() + if (isFallback) { + return

fallback

+ } + return

Hi {slug.join('/')}

+} diff --git a/test/integration/prerender/test/index.test.js b/test/integration/prerender/test/index.test.js index 04e7df065665ba2..15cc1629d39d243 100644 --- a/test/integration/prerender/test/index.test.js +++ b/test/integration/prerender/test/index.test.js @@ -386,9 +386,20 @@ const runTests = (dev = false) => { } // Production will render fallback for a "lazy" route else { - const browser = await webdriver(appPort, '/catchall/notreturnedinpaths') - const text = await browser.elementByCss('#catchall').text() - expect(text).toMatch(/Hi.*?notreturnedinpaths/) + const html = await renderViaHTTP(appPort, '/catchall/notreturnedinpaths') + const $ = cheerio.load(html) + expect($('#catchall').text()).toBe('fallback') + + // hydration + const browser = await webdriver(appPort, '/catchall/delayby3s') + + const text1 = await browser.elementByCss('#catchall').text() + expect(text1).toBe('fallback') + + await new Promise(resolve => setTimeout(resolve, 4000)) + + const text2 = await browser.elementByCss('#catchall').text() + expect(text2).toMatch(/Hi.*?delayby3s/) } })