Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle buildId mismatch properly #1221

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions lib/router/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
/* global window, location */
import _Router from './router'
import url from 'url'
import UUID from 'uuid'

const SingletonRouter = {
router: null, // holds the actual router instance
Expand Down Expand Up @@ -75,3 +78,39 @@ export const createRouter = function (...args) {

// Export the actual Router class, which is usually used inside the server
export const Router = _Router

// Add a functionality to reload the current page while providing a way to
// persist the browser state.
const onBeforeReloadHooks = []
export function _forceReload () {
const parsedUrl = url.parse(location.href, true)
const reloadKey = UUID.v4()
parsedUrl.query.__reloadKey = reloadKey

onBeforeReloadHooks.forEach((hook) => hook(reloadKey))

delete parsedUrl.search
const reloadUrl = url.format(parsedUrl)
location.href = reloadUrl
}

Object.defineProperty(SingletonRouter, 'onBeforeReload', {
get () { return () => {} },
set (fn) {
if (typeof window === 'undefined') return

onBeforeReloadHooks.push(fn)
}
})

Object.defineProperty(SingletonRouter, 'onAfterReload', {
get () { return () => {} },
set (fn) {
if (typeof window === 'undefined') return

const parsedUrl = url.parse(location.href, true)
if (!parsedUrl.query.__reloadKey) return

fn(parsedUrl.query.__reloadKey)
}
})
7 changes: 7 additions & 0 deletions lib/router/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import evalScript from '../eval-script'
import shallowEquals from '../shallow-equals'
import PQueue from '../p-queue'
import { loadGetInitialProps, getLocationOrigin } from '../utils'
import { _forceReload } from './'

// Add "fetch" polyfill for older browsers
if (typeof window !== 'undefined') {
Expand Down Expand Up @@ -240,6 +241,12 @@ export default class Router extends EventEmitter {

const jsonPageRes = await this.fetchRoute(route)
const jsonData = await jsonPageRes.json()

if (jsonData.buildIdMismatch) {
_forceReload()
throw new Error('Reloading due to buildId mismatch')
}

const newData = {
...loadComponent(jsonData),
jsonPageRes
Expand Down
22 changes: 22 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ _**NOTE! the README on the `master` branch might not match that of the [latest s
- [With `<Link>`](#with-link)
- [Imperatively](#imperatively)
- [Router Events](#router-events)
- [Reload Hooks](#reload-hooks)
- [Prefetching Pages](#prefetching-pages)
- [With `<Link>`](#with-link-1)
- [Imperatively](#imperatively-1)
Expand Down Expand Up @@ -337,6 +338,27 @@ Router.onRouteChangeError = (err, url) => {
}
```

##### Reload Hooks

Between new deployments, Next.js might reload your app when you are navigating pages. With that, your app's client side state might get destroyed. In that case, you can use our reload hooks to restore that state after the reload.

Let's assume our client side state stored in a variable called `state`. Then this is how we use reload hooks to restore the state .

```js
let state = {}

Router.onBeforeReload = function (key) {
localStorage.setItem(key, JSON.stringify(state))
}

Router.onAfterReload = function (key) {
const data = (localStorage.getItem(key))
if (!data) return

const state = JSON.parse(data)
}
```

### Prefetching Pages

<p><details>
Expand Down
31 changes: 24 additions & 7 deletions server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,19 +81,32 @@ export default class Server {
},

'/_next/:buildId/main.js': async (req, res, params) => {
this.handleBuildId(params.buildId, res)
if (!this.handleBuildId(params.buildId, res)) {
reloadTheBrowser(res)
return
}

const p = join(this.dir, '.next/main.js')
await this.serveStatic(req, res, p)
},

'/_next/:buildId/commons.js': async (req, res, params) => {
this.handleBuildId(params.buildId, res)
if (!this.handleBuildId(params.buildId, res)) {
reloadTheBrowser(res)
return
}

const p = join(this.dir, '.next/commons.js')
await this.serveStatic(req, res, p)
},

'/_next/:buildId/pages/:path*': async (req, res, params) => {
this.handleBuildId(params.buildId, res)
if (!this.handleBuildId(params.buildId, res)) {
res.setHeader('Content-Type', 'application/json')
res.end(JSON.stringify({ buildIdMismatch: true }))
return
}

const paths = params.path || ['index']
const pathname = `/${paths.join('/')}`
await this.renderJSON(req, res, pathname)
Expand Down Expand Up @@ -277,14 +290,13 @@ export default class Server {
}

handleBuildId (buildId, res) {
if (this.dev) return
if (this.dev) return true
if (buildId !== this.renderOpts.buildId) {
const errorMessage = 'Build id mismatch!' +
'Seems like the server and the client version of files are not the same.'
throw new Error(errorMessage)
return false
}

res.setHeader('Cache-Control', 'max-age=365000000, immutable')
return true
}

getCompilationError (page) {
Expand All @@ -298,3 +310,8 @@ export default class Server {
if (p) return errors.get(p)[0]
}
}

function reloadTheBrowser (res) {
res.setHeader('Content-Type', 'text/javascript')
res.end('location.reload()')
}
33 changes: 33 additions & 0 deletions test/integration/basic/pages/force-reload.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/* global localStorage */
import React, { Component } from 'react'
import Router, { _forceReload } from 'next/router'

let counter = 0

export default class extends Component {

increase () {
counter++
this.forceUpdate()
}

render () {
return (
<div className='force-reload'>
<div id='counter'>
Counter: {counter}
</div>
<button id='increase' onClick={() => this.increase()}>Increase</button>
<button id='reload' onClick={() => _forceReload()}>Reload</button>
</div>
)
}
}

Router.onBeforeReload = function (key) {
localStorage.setItem(key, counter)
}

Router.onAfterReload = function (key) {
counter = parseInt(localStorage.getItem(key))
}
22 changes: 20 additions & 2 deletions test/integration/basic/test/misc.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* global describe, test, expect */
import webdriver from 'next-webdriver'

export default function ({ app }) {
export default function (context) {
describe('Misc', () => {
test('finishes response', async () => {
const res = {
Expand All @@ -9,8 +10,25 @@ export default function ({ app }) {
this.finished = true
}
}
const html = await app.renderToHTML({}, res, '/finish-response', {})
const html = await context.app.renderToHTML({}, res, '/finish-response', {})
expect(html).toBeFalsy()
})

test('should allow to persist while force reload', async () => {
const browser = await webdriver(context.appPort, '/force-reload')
const countText = await browser
.elementByCss('#increase').click()
.elementByCss('#increase').click()
.elementByCss('#counter').text()

expect(countText).toBe('Counter: 2')

await browser.elementByCss('#reload').click()

const newCountText = await browser.elementByCss('#counter').text()
expect(newCountText).toBe(countText)

await browser.close()
})
})
}