Skip to content
Merged
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
30 changes: 30 additions & 0 deletions examples/with-shallow-routing/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@

# Shallow Routing Example

## How to use

Download the example [or clone the repo](https://github.com/zeit/next.js):

```bash
curl https://codeload.github.com/zeit/next.js/tar.gz/master | tar -xz --strip=2 next.js-master/examples/hello-world
cd hello-world
```

Install it and run:

```bash
npm install
npm run dev
```

Deploy it to the cloud with [now](https://zeit.co/now) ([download](https://zeit.co/download))

```bash
now
```

## The idea behind the example

With shallow routing, we could change the URL without actually running the `getInitialProps` every time you change the URL.

We do this passing the `shallow: true` option to `Router.push` or `Router.replace`.
16 changes: 16 additions & 0 deletions examples/with-shallow-routing/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "with-shallow-routing",
"version": "1.0.0",
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
},
"dependencies": {
"next": "next@beta",
"react": "^15.4.2",
"react-dom": "^15.4.2"
},
"author": "",
"license": "ISC"
}
3 changes: 3 additions & 0 deletions examples/with-shallow-routing/pages/about.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default () => (
<div>About us</div>
)
46 changes: 46 additions & 0 deletions examples/with-shallow-routing/pages/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import React from 'react'
import Link from 'next/link'
import Router from 'next/router'
import { format } from 'url'

let counter = 1

export default class Index extends React.Component {
static getInitialProps ({ res }) {
if (res) {
return { initialPropsCounter: 1 }
}

counter++
return {
initialPropsCounter: counter
}
}

reload () {
const { pathname, query } = Router
Router.push(format({ pathname, query }))
}

incrementStateCounter () {
const { url } = this.props
const currentCounter = url.query.counter ? parseInt(url.query.counter) : 0
const href = `/?counter=${currentCounter + 1}`
Router.push(href, href, { shallow: true })
}

render () {
const { initialPropsCounter, url } = this.props

return (
<div>
<h2>This is the Home Page</h2>
<Link href='/about'><a>About</a></Link>
<button onClick={() => this.reload()}>Reload</button>
<button onClick={() => this.incrementStateCounter()}>Change State Counter</button>
<p>"getInitialProps" ran for "{initialPropsCounter}" times.</p>
<p>Counter: "{url.query.counter || 0}".</p>
</div>
)
}
}
26 changes: 4 additions & 22 deletions lib/app.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React, { Component, PropTypes } from 'react'
import { AppContainer } from 'react-hot-loader'
import shallowEquals from './shallow-equals'
import { warn } from './utils'

const ErrorDebug = process.env.NODE_ENV === 'production'
? null : require('./error-debug').default
Expand All @@ -18,7 +17,8 @@ export default class App extends Component {

render () {
const { Component, props, hash, err, router } = this.props
const containerProps = { Component, props, hash, router }
const url = createUrl(router)
const containerProps = { Component, props, hash, router, url }

return <div>
<Container {...containerProps} />
Expand Down Expand Up @@ -52,8 +52,7 @@ class Container extends Component {
}

render () {
const { Component, props, router } = this.props
const url = createUrl(router)
const { Component, props, url } = this.props

// includes AppContainer which bypasses shouldComponentUpdate method
// https://github.com/gaearon/react-hot-loader/issues/442
Expand All @@ -66,23 +65,6 @@ class Container extends Component {
function createUrl (router) {
return {
query: router.query,
pathname: router.pathname,
back: () => router.back(),
push: (url, as) => router.push(url, as),
pushTo: (href, as) => {
warn(`Warning: 'url.pushTo()' is deprecated. Please use 'url.push()' instead.`)
const pushRoute = as ? href : null
const pushUrl = as || href

return router.push(pushRoute, pushUrl)
},
replace: (url, as) => router.replace(url, as),
replaceTo: (href, as) => {
warn(`Warning: 'url.replaceTo()' is deprecated. Please use 'url.replace()' instead.`)
const replaceRoute = as ? href : null
const replaceUrl = as || href

return router.replace(replaceRoute, replaceUrl)
}
pathname: router.pathname
}
}
84 changes: 51 additions & 33 deletions lib/router/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,16 @@ export default class Router extends EventEmitter {
return
}

const { url, as } = e.state
this.replace(url, as)
const { url, as, options } = e.state
this.replace(url, as, options)
}

update (route, Component) {
const data = this.components[route] || {}
const data = this.components[route]
if (!data) {
throw new Error(`Cannot update unavailable route: ${route}`)
}

const newData = { ...data, Component }
this.components[route] = newData

Expand All @@ -95,17 +99,14 @@ export default class Router extends EventEmitter {
const { pathname, query } = parse(url, true)

this.emit('routeChangeStart', url)
const {
data,
props,
error
} = await this.getRouteInfo(route, pathname, query, url)
const routeInfo = await this.getRouteInfo(route, pathname, query, url)
const { error } = routeInfo

if (error && error.cancelled) {
return
}

this.notify({ ...data, props })
this.notify(routeInfo)

if (error) {
this.emit('routeChangeError', error, url)
Expand All @@ -119,15 +120,15 @@ export default class Router extends EventEmitter {
window.history.back()
}

push (url, as = url) {
return this.change('pushState', url, as)
push (url, as = url, options = {}) {
return this.change('pushState', url, as, options)
}

replace (url, as = url) {
return this.change('replaceState', url, as)
replace (url, as = url, options = {}) {
return this.change('replaceState', url, as, options)
}

async change (method, url, as) {
async change (method, url, as, options) {
this.abortComponentLoad(as)
const { pathname, query } = parse(url, true)

Expand All @@ -147,21 +148,30 @@ export default class Router extends EventEmitter {
}

const route = toRoute(pathname)
const { shallow = false } = options
let routeInfo = null

this.emit('routeChangeStart', as)
const {
data, props, error
} = await this.getRouteInfo(route, pathname, query, as)

// If shallow === false and other conditions met, we reuse the
// existing routeInfo for this route.
// Because of this, getInitialProps would not run.
if (shallow && this.isShallowRoutingPossible(route)) {
routeInfo = this.components[route]
} else {
routeInfo = await this.getRouteInfo(route, pathname, query, as)
}

const { error } = routeInfo

if (error && error.cancelled) {
return false
}

this.changeState(method, url, as)
this.changeState(method, url, as, options)
const hash = window.location.hash.substring(1)

this.route = route
this.set(pathname, query, as, { ...data, props, hash })
this.set(route, pathname, query, as, { ...routeInfo, hash })

if (error) {
this.emit('routeChangeError', error, as)
Expand All @@ -172,31 +182,33 @@ export default class Router extends EventEmitter {
return true
}

changeState (method, url, as) {
changeState (method, url, as, options = {}) {
if (method !== 'pushState' || getURL() !== as) {
window.history[method]({ url, as }, null, as)
window.history[method]({ url, as, options }, null, as)
}
}

async getRouteInfo (route, pathname, query, as) {
const routeInfo = {}
let routeInfo = null

try {
routeInfo.data = await this.fetchComponent(route, as)
if (!routeInfo.data) {
return null
routeInfo = this.components[route]
if (!routeInfo) {
routeInfo = await this.fetchComponent(route, as)
}

const { Component, err, jsonPageRes } = routeInfo.data
const { Component, err, jsonPageRes } = routeInfo
const ctx = { err, pathname, query, jsonPageRes }
routeInfo.props = await this.getInitialProps(Component, ctx)

this.components[route] = routeInfo
} catch (err) {
if (err.cancelled) {
return { error: err }
}

const Component = this.ErrorComponent
routeInfo.data = { Component, err }
routeInfo = { Component, err }
const ctx = { err, pathname, query }
routeInfo.props = await this.getInitialProps(Component, ctx)

Expand All @@ -207,7 +219,8 @@ export default class Router extends EventEmitter {
return routeInfo
}

set (pathname, query, as, data) {
set (route, pathname, query, as, data) {
this.route = route
this.pathname = pathname
this.query = query
this.as = as
Expand Down Expand Up @@ -238,6 +251,15 @@ export default class Router extends EventEmitter {
return this.pathname !== pathname || !shallowEquals(query, this.query)
}

isShallowRoutingPossible (route) {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are the conditions before deciding to use shallow routing.

return (
// If there's cached routeInfo for the route.
Boolean(this.components[route]) &&
// If the route is already rendered on the screen.
this.route === route
)
}

async prefetch (url) {
// We don't add support for prefetch in the development mode.
// If we do that, our on-demand-entries optimization won't performs better
Expand All @@ -249,9 +271,6 @@ export default class Router extends EventEmitter {
}

async fetchComponent (route, as) {
let data = this.components[route]
if (data) return data

let cancelled = false
const cancel = this.componentLoadCancel = function () {
cancelled = true
Expand Down Expand Up @@ -283,7 +302,6 @@ export default class Router extends EventEmitter {
this.componentLoadCancel = null
}

this.components[route] = newData
return newData
}

Expand Down
45 changes: 45 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)
- [Shallow Routing](#shallow-routing)
- [Prefetching Pages](#prefetching-pages)
- [With `<Link>`](#with-link-1)
- [Imperatively](#imperatively-1)
Expand Down Expand Up @@ -349,6 +350,50 @@ Router.onAppUpdated = (nextUrl) => {
}
```

##### Shallow Routing

<p><details>
<summary><b>Examples</b></summary>
<ul>
<li><a href="./examples/with-shallow-routing">Shallow Routing</a></li>
</ul>
</details></p>

With shallow routing you could chnage the URL without running `getInitialProps` of the page. You'll receive the updated "pathname" and the "query" via the `url` prop of the page.

You can do this by invoking the eith `Router.push` or `Router.replace` with `shallow: true` option. Here's an example:

```js
// Current URL is "/"
const href = '/?counter=10'
const as = href
Router.push(href, as, { shallow: true })
```

Now, the URL is updated to "/?counter=10" and page is re-rendered.
You can see the updated URL with `this.props.url` inside the Component.

You can also watch for URL changes via [`componentWillReceiveProps`](https://facebook.github.io/react/docs/react-component.html#componentwillreceiveprops) hook as shown below:

```
componentWillReceiveProps(nextProps) {
const { pathname, query } = nextProps.url
// fetch data based on the new query
}
```

> NOTES:
>
> Shallow routing works **only** for same page URL changes.
>
> For an example, let's assume we've another page called "about".
> Now you are changing a URL like this:
> ```js
> Router.push('/about?counter=10', '/about?counter=10', { shallow: true })
> ```
> Since that's a new page, it'll run "getInitialProps" of the "about" page even we asked to do shallow routing.


### Prefetching Pages

(This is a production only feature)
Expand Down
Loading