diff --git a/errors/no-on-app-updated-hook.md b/errors/no-on-app-updated-hook.md new file mode 100644 index 00000000..ec0e0524 --- /dev/null +++ b/errors/no-on-app-updated-hook.md @@ -0,0 +1,25 @@ +# Router.onAppUpdated is removed + +Due to [this bug fix](https://github.com/zeit/next.js/pull/3849), we had to remove the `Router.onAppUpdated` hook. But the default functionality of this feature is still in effect. + +We use this hook to detect a new app deployment when switching pages and act accordingly. Although there are many things you can do in this hook, it's often used to navigate the page via the server as shown below: + +```js +Router.onAppUpdated = function(nextRoute) { + location.href = nextRoute +} +``` + +In this hook, you can't wait for a network request or a promise to get resolved. And you can't block the page navigation. So, the things you can do is limited. + +One real use of this hook is to persist your application state to local-storage before the page navigation. For that, you can use the [`window.onbeforeunload`](https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload) hook instead. + +This is code for that: + +```js +window.onbeforeunload = function(e) { + // Get the application state (usually from a store like Redux) + const appState = {} + localStorage.setItem('app-state', JSON.stringify(appState)); +}; +``` diff --git a/lib/page-loader.js b/lib/page-loader.js index 27cc783d..40d13449 100644 --- a/lib/page-loader.js +++ b/lib/page-loader.js @@ -76,6 +76,7 @@ export default class PageLoader { script.src = url script.onerror = () => { const error = new Error(`Error when loading route: ${route}`) + error.code = 'PAGE_LOAD_ERROR' this.pageRegisterEvents.emit(route, { error }) } diff --git a/lib/router/index.js b/lib/router/index.js index 30168ec4..665cde6d 100644 --- a/lib/router/index.js +++ b/lib/router/index.js @@ -1,5 +1,6 @@ /* global window */ import _Router from './router' +import { execOnce } from '../utils' const SingletonRouter = { router: null, // holds the actual router instance @@ -53,6 +54,18 @@ routerEvents.forEach((event) => { }) }) +const warnAboutRouterOnAppUpdated = execOnce(() => { + console.warn(`Router.onAppUpdated is removed - visit https://err.sh/next.js/no-on-app-updated-hook for more information.`) +}) + +Object.defineProperty(SingletonRouter, 'onAppUpdated', { + get () { return null }, + set () { + warnAboutRouterOnAppUpdated() + return null + } +}) + function throwIfNoRouter () { if (!SingletonRouter.router) { const message = 'No router instance found.\n' + @@ -85,15 +98,6 @@ export const createRouter = function (...args) { // Export the actual Router class, which is usually used inside the server export const Router = _Router -export function _notifyBuildIdMismatch (nextRoute) { - if (SingletonRouter.onAppUpdated) { - SingletonRouter.onAppUpdated(nextRoute) - } else { - console.warn(`An app update detected. Loading the SSR version of "${nextRoute}"`) - window.location.href = nextRoute - } -} - export function _rewriteUrlForNextExport (url) { const [, hash] = url.split('#') url = url.replace(/#.*/, '') diff --git a/lib/router/router.js b/lib/router/router.js index 8eedd6ad..f77c8154 100644 --- a/lib/router/router.js +++ b/lib/router/router.js @@ -5,7 +5,7 @@ import EventEmitter from '../EventEmitter' import shallowEquals from '../shallow-equals' import PQueue from '../p-queue' import { loadGetInitialProps, getURL, warn, execOnce } from '../utils' -import { _notifyBuildIdMismatch, _rewriteUrlForNextExport } from './' +import { _rewriteUrlForNextExport } from './' export default class Router { constructor (pathname, query, as, { pageLoader, Component, ErrorComponent, err } = {}) { @@ -212,20 +212,12 @@ export default class Router { this.components[route] = routeInfo } catch (err) { - if (err.buildIdMismatched) { - // Now we need to reload the page or do the action asked by the user - _notifyBuildIdMismatch(as) - // We also need to cancel this current route change. - // We do it like this. - err.cancelled = true - return { error: err } - } + if (err.code === 'PAGE_LOAD_ERROR') { + // If we can't load the page it could be one of following reasons + // 1. Page doesn't exists + // 2. Page does exist in a different zone + // 3. Internal error while loading the page - if (err.statusCode === 404) { - // If there's 404 error for the page, it could be due to two reasons. - // 1. Page is not exists - // 2. Page is exists in a different zone - // We are not sure whether this is actual 404 or exists in a different zone. // So, doing a hard reload is the proper way to deal with this. window.location.href = as diff --git a/package.json b/package.json index f639ecb0..a338b61f 100644 --- a/package.json +++ b/package.json @@ -106,8 +106,7 @@ "webpack-dev-middleware": "1.12.0", "webpack-hot-middleware": "2.21.0", "webpack-sources": "1.1.0", - "write-file-webpack-plugin": "4.2.0", - "xss-filters": "1.2.7" + "write-file-webpack-plugin": "4.2.0" }, "devDependencies": { "@taskr/babel": "1.1.0", diff --git a/readme.md b/readme.md index bd057318..8f30b508 100644 --- a/readme.md +++ b/readme.md @@ -491,7 +491,6 @@ Here's a list of supported events: - `onRouteChangeComplete(url)` - Fires when a route changed completely - `onRouteChangeError(err, url)` - Fires when there's an error when changing routes - `onBeforeHistoryChange(url)` - Fires just before changing the browser's history -- `onAppUpdated(nextRoute)` - Fires when switching pages and there's a new version of the app > Here `url` is the URL shown in the browser. If you call `Router.push(url, as)` (or similar), then the value of `url` will be `as`. @@ -519,17 +518,6 @@ Router.onRouteChangeError = (err, url) => { } ``` -If you change a route while in between a new deployment, we can't navigate the app via client side. We need to do a full browser navigation. We do it automatically for you. - -But you can customize that via `Route.onAppUpdated` event like this: - -```js -Router.onAppUpdated = nextUrl => { - // persist the local state - location.href = nextUrl -} -``` - ##### Shallow Routing

diff --git a/server/index.js b/server/index.js index 1b1e546b..a25f3b1b 100644 --- a/server/index.js +++ b/server/index.js @@ -196,9 +196,7 @@ export default class Server { '/_next/:buildId/page/_error.js': async (req, res, params) => { if (!this.handleBuildId(params.buildId, res)) { const error = new Error('INVALID_BUILD_ID') - const customFields = { buildIdMismatched: true } - - return await renderScriptError(req, res, '/_error', error, customFields, this.renderOpts) + return await renderScriptError(req, res, '/_error', error) } const p = join(this.dir, `${this.dist}/bundles/pages/_error.js`) @@ -211,22 +209,19 @@ export default class Server { if (!this.handleBuildId(params.buildId, res)) { const error = new Error('INVALID_BUILD_ID') - const customFields = { buildIdMismatched: true } - - return await renderScriptError(req, res, page, error, customFields, this.renderOpts) + return await renderScriptError(req, res, page, error) } if (this.dev) { try { await this.hotReloader.ensurePage(page) } catch (error) { - return await renderScriptError(req, res, page, error, {}, this.renderOpts) + return await renderScriptError(req, res, page, error) } const compilationErr = await this.getCompilationError() if (compilationErr) { - const customFields = { statusCode: 500 } - return await renderScriptError(req, res, page, compilationErr, customFields, this.renderOpts) + return await renderScriptError(req, res, page, compilationErr) } } @@ -235,7 +230,7 @@ export default class Server { // [production] If the page is not exists, we need to send a proper Next.js style 404 // Otherwise, it'll affect the multi-zones feature. if (!(await fsAsync.exists(p))) { - return await renderScriptError(req, res, page, { code: 'ENOENT' }, {}, this.renderOpts) + return await renderScriptError(req, res, page, { code: 'ENOENT' }) } await this.serveStatic(req, res, p) diff --git a/server/render.js b/server/render.js index 3d306f26..5ed575c0 100644 --- a/server/render.js +++ b/server/render.js @@ -12,7 +12,8 @@ import Head, { defaultHead } from '../lib/head' import App from '../lib/app' import ErrorDebug from '../lib/error-debug' import { flushChunks } from '../lib/dynamic' -import xssFilters from 'xss-filters' + +const logger = console export async function render (req, res, pathname, query, opts) { const html = await renderToHTML(req, res, pathname, query, opts) @@ -120,36 +121,19 @@ async function doRender (req, res, pathname, query, { return '' + renderToStaticMarkup(doc) } -export async function renderScriptError (req, res, page, error, customFields, { dev }) { +export async function renderScriptError (req, res, page, error) { // Asks CDNs and others to not to cache the errored page res.setHeader('Cache-Control', 'no-store, must-revalidate') - // prevent XSS attacks by filtering the page before printing it. - page = xssFilters.uriInSingleQuotedAttr(page) - res.setHeader('Content-Type', 'text/javascript') if (error.code === 'ENOENT') { - res.end(` - window.__NEXT_REGISTER_PAGE('${page}', function() { - var error = new Error('Page does not exist: ${page}') - error.statusCode = 404 - - return { error: error } - }) - `) + res.statusCode = 404 + res.end('404 - Not Found') return } - const errorJson = { - ...serializeError(dev, error), - ...customFields - } - - res.end(` - window.__NEXT_REGISTER_PAGE('${page}', function() { - var error = ${JSON.stringify(errorJson)} - return { error: error } - }) - `) + logger.error(error.stack) + res.statusCode = 500 + res.end('500 - Internal Error') } export function sendHTML (req, res, html, method, { dev }) { diff --git a/test/integration/production/test/index.test.js b/test/integration/production/test/index.test.js index d22c99b7..9ebb7cac 100644 --- a/test/integration/production/test/index.test.js +++ b/test/integration/production/test/index.test.js @@ -127,7 +127,7 @@ describe('Production Usage', () => { // Let the browser to prefetch the page and error it on the console. await waitFor(3000) const browserLogs = await browser.log('browser') - expect(browserLogs[0].message).toMatch(/Page does not exist: \/no-such-page/) + expect(browserLogs[0].message).toMatch(/\/no-such-page.js - Failed to load resource/) // When we go to the 404 page, it'll do a hard reload. // So, it's possible for the front proxy to load a page from another zone.