Skip to content

Commit f8f3fa7

Browse files
arunodarauchg
authored andcommitted
Introducing Shallow Routing (#1357)
* Simplify route info handling. * Add basic resolve=false support. * Make sure to render getInitialProps always if it's the first render. * Change resolve=false to shallow routing. * Add test cases for shallow routing. * Update README for shallow routing docs. * Update docs. * Update docs. * Update docs.
1 parent 76698ea commit f8f3fa7

File tree

11 files changed

+300
-57
lines changed

11 files changed

+300
-57
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
2+
# Shallow Routing Example
3+
4+
## How to use
5+
6+
Download the example [or clone the repo](https://github.com/zeit/next.js):
7+
8+
```bash
9+
curl https://codeload.github.com/zeit/next.js/tar.gz/master | tar -xz --strip=2 next.js-master/examples/hello-world
10+
cd hello-world
11+
```
12+
13+
Install it and run:
14+
15+
```bash
16+
npm install
17+
npm run dev
18+
```
19+
20+
Deploy it to the cloud with [now](https://zeit.co/now) ([download](https://zeit.co/download))
21+
22+
```bash
23+
now
24+
```
25+
26+
## The idea behind the example
27+
28+
With shallow routing, we could change the URL without actually running the `getInitialProps` every time you change the URL.
29+
30+
We do this passing the `shallow: true` option to `Router.push` or `Router.replace`.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"name": "with-shallow-routing",
3+
"version": "1.0.0",
4+
"scripts": {
5+
"dev": "next",
6+
"build": "next build",
7+
"start": "next start"
8+
},
9+
"dependencies": {
10+
"next": "next@beta",
11+
"react": "^15.4.2",
12+
"react-dom": "^15.4.2"
13+
},
14+
"author": "",
15+
"license": "ISC"
16+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default () => (
2+
<div>About us</div>
3+
)
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import React from 'react'
2+
import Link from 'next/link'
3+
import Router from 'next/router'
4+
import { format } from 'url'
5+
6+
let counter = 1
7+
8+
export default class Index extends React.Component {
9+
static getInitialProps ({ res }) {
10+
if (res) {
11+
return { initialPropsCounter: 1 }
12+
}
13+
14+
counter++
15+
return {
16+
initialPropsCounter: counter
17+
}
18+
}
19+
20+
reload () {
21+
const { pathname, query } = Router
22+
Router.push(format({ pathname, query }))
23+
}
24+
25+
incrementStateCounter () {
26+
const { url } = this.props
27+
const currentCounter = url.query.counter ? parseInt(url.query.counter) : 0
28+
const href = `/?counter=${currentCounter + 1}`
29+
Router.push(href, href, { shallow: true })
30+
}
31+
32+
render () {
33+
const { initialPropsCounter, url } = this.props
34+
35+
return (
36+
<div>
37+
<h2>This is the Home Page</h2>
38+
<Link href='/about'><a>About</a></Link>
39+
<button onClick={() => this.reload()}>Reload</button>
40+
<button onClick={() => this.incrementStateCounter()}>Change State Counter</button>
41+
<p>"getInitialProps" ran for "{initialPropsCounter}" times.</p>
42+
<p>Counter: "{url.query.counter || 0}".</p>
43+
</div>
44+
)
45+
}
46+
}

lib/app.js

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import React, { Component, PropTypes } from 'react'
22
import { AppContainer } from 'react-hot-loader'
33
import shallowEquals from './shallow-equals'
4-
import { warn } from './utils'
54

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

1918
render () {
2019
const { Component, props, hash, err, router } = this.props
21-
const containerProps = { Component, props, hash, router }
20+
const url = createUrl(router)
21+
const containerProps = { Component, props, hash, router, url }
2222

2323
return <div>
2424
<Container {...containerProps} />
@@ -52,8 +52,7 @@ class Container extends Component {
5252
}
5353

5454
render () {
55-
const { Component, props, router } = this.props
56-
const url = createUrl(router)
55+
const { Component, props, url } = this.props
5756

5857
// includes AppContainer which bypasses shouldComponentUpdate method
5958
// https://github.com/gaearon/react-hot-loader/issues/442
@@ -66,23 +65,6 @@ class Container extends Component {
6665
function createUrl (router) {
6766
return {
6867
query: router.query,
69-
pathname: router.pathname,
70-
back: () => router.back(),
71-
push: (url, as) => router.push(url, as),
72-
pushTo: (href, as) => {
73-
warn(`Warning: 'url.pushTo()' is deprecated. Please use 'url.push()' instead.`)
74-
const pushRoute = as ? href : null
75-
const pushUrl = as || href
76-
77-
return router.push(pushRoute, pushUrl)
78-
},
79-
replace: (url, as) => router.replace(url, as),
80-
replaceTo: (href, as) => {
81-
warn(`Warning: 'url.replaceTo()' is deprecated. Please use 'url.replace()' instead.`)
82-
const replaceRoute = as ? href : null
83-
const replaceUrl = as || href
84-
85-
return router.replace(replaceRoute, replaceUrl)
86-
}
68+
pathname: router.pathname
8769
}
8870
}

lib/router/router.js

Lines changed: 51 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,16 @@ export default class Router extends EventEmitter {
7171
return
7272
}
7373

74-
const { url, as } = e.state
75-
this.replace(url, as)
74+
const { url, as, options } = e.state
75+
this.replace(url, as, options)
7676
}
7777

7878
update (route, Component) {
79-
const data = this.components[route] || {}
79+
const data = this.components[route]
80+
if (!data) {
81+
throw new Error(`Cannot update unavailable route: ${route}`)
82+
}
83+
8084
const newData = { ...data, Component }
8185
this.components[route] = newData
8286

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

97101
this.emit('routeChangeStart', url)
98-
const {
99-
data,
100-
props,
101-
error
102-
} = await this.getRouteInfo(route, pathname, query, url)
102+
const routeInfo = await this.getRouteInfo(route, pathname, query, url)
103+
const { error } = routeInfo
103104

104105
if (error && error.cancelled) {
105106
return
106107
}
107108

108-
this.notify({ ...data, props })
109+
this.notify(routeInfo)
109110

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

122-
push (url, as = url) {
123-
return this.change('pushState', url, as)
123+
push (url, as = url, options = {}) {
124+
return this.change('pushState', url, as, options)
124125
}
125126

126-
replace (url, as = url) {
127-
return this.change('replaceState', url, as)
127+
replace (url, as = url, options = {}) {
128+
return this.change('replaceState', url, as, options)
128129
}
129130

130-
async change (method, url, as) {
131+
async change (method, url, as, options) {
131132
this.abortComponentLoad(as)
132133
const { pathname, query } = parse(url, true)
133134

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

149150
const route = toRoute(pathname)
151+
const { shallow = false } = options
152+
let routeInfo = null
150153

151154
this.emit('routeChangeStart', as)
152-
const {
153-
data, props, error
154-
} = await this.getRouteInfo(route, pathname, query, as)
155+
156+
// If shallow === false and other conditions met, we reuse the
157+
// existing routeInfo for this route.
158+
// Because of this, getInitialProps would not run.
159+
if (shallow && this.isShallowRoutingPossible(route)) {
160+
routeInfo = this.components[route]
161+
} else {
162+
routeInfo = await this.getRouteInfo(route, pathname, query, as)
163+
}
164+
165+
const { error } = routeInfo
155166

156167
if (error && error.cancelled) {
157168
return false
158169
}
159170

160-
this.changeState(method, url, as)
171+
this.changeState(method, url, as, options)
161172
const hash = window.location.hash.substring(1)
162173

163-
this.route = route
164-
this.set(pathname, query, as, { ...data, props, hash })
174+
this.set(route, pathname, query, as, { ...routeInfo, hash })
165175

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

175-
changeState (method, url, as) {
185+
changeState (method, url, as, options = {}) {
176186
if (method !== 'pushState' || getURL() !== as) {
177-
window.history[method]({ url, as }, null, as)
187+
window.history[method]({ url, as, options }, null, as)
178188
}
179189
}
180190

181191
async getRouteInfo (route, pathname, query, as) {
182-
const routeInfo = {}
192+
let routeInfo = null
183193

184194
try {
185-
routeInfo.data = await this.fetchComponent(route, as)
186-
if (!routeInfo.data) {
187-
return null
195+
routeInfo = this.components[route]
196+
if (!routeInfo) {
197+
routeInfo = await this.fetchComponent(route, as)
188198
}
189199

190-
const { Component, err, jsonPageRes } = routeInfo.data
200+
const { Component, err, jsonPageRes } = routeInfo
191201
const ctx = { err, pathname, query, jsonPageRes }
192202
routeInfo.props = await this.getInitialProps(Component, ctx)
203+
204+
this.components[route] = routeInfo
193205
} catch (err) {
194206
if (err.cancelled) {
195207
return { error: err }
196208
}
197209

198210
const Component = this.ErrorComponent
199-
routeInfo.data = { Component, err }
211+
routeInfo = { Component, err }
200212
const ctx = { err, pathname, query }
201213
routeInfo.props = await this.getInitialProps(Component, ctx)
202214

@@ -207,7 +219,8 @@ export default class Router extends EventEmitter {
207219
return routeInfo
208220
}
209221

210-
set (pathname, query, as, data) {
222+
set (route, pathname, query, as, data) {
223+
this.route = route
211224
this.pathname = pathname
212225
this.query = query
213226
this.as = as
@@ -238,6 +251,15 @@ export default class Router extends EventEmitter {
238251
return this.pathname !== pathname || !shallowEquals(query, this.query)
239252
}
240253

254+
isShallowRoutingPossible (route) {
255+
return (
256+
// If there's cached routeInfo for the route.
257+
Boolean(this.components[route]) &&
258+
// If the route is already rendered on the screen.
259+
this.route === route
260+
)
261+
}
262+
241263
async prefetch (url) {
242264
// We don't add support for prefetch in the development mode.
243265
// If we do that, our on-demand-entries optimization won't performs better
@@ -249,9 +271,6 @@ export default class Router extends EventEmitter {
249271
}
250272

251273
async fetchComponent (route, as) {
252-
let data = this.components[route]
253-
if (data) return data
254-
255274
let cancelled = false
256275
const cancel = this.componentLoadCancel = function () {
257276
cancelled = true
@@ -283,7 +302,6 @@ export default class Router extends EventEmitter {
283302
this.componentLoadCancel = null
284303
}
285304

286-
this.components[route] = newData
287305
return newData
288306
}
289307

readme.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ _**NOTE! the README on the `master` branch might not match that of the [latest s
2525
- [With `<Link>`](#with-link)
2626
- [Imperatively](#imperatively)
2727
- [Router Events](#router-events)
28+
- [Shallow Routing](#shallow-routing)
2829
- [Prefetching Pages](#prefetching-pages)
2930
- [With `<Link>`](#with-link-1)
3031
- [Imperatively](#imperatively-1)
@@ -349,6 +350,50 @@ Router.onAppUpdated = (nextUrl) => {
349350
}
350351
```
351352

353+
##### Shallow Routing
354+
355+
<p><details>
356+
<summary><b>Examples</b></summary>
357+
<ul>
358+
<li><a href="./examples/with-shallow-routing">Shallow Routing</a></li>
359+
</ul>
360+
</details></p>
361+
362+
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.
363+
364+
You can do this by invoking the eith `Router.push` or `Router.replace` with `shallow: true` option. Here's an example:
365+
366+
```js
367+
// Current URL is "/"
368+
const href = '/?counter=10'
369+
const as = href
370+
Router.push(href, as, { shallow: true })
371+
```
372+
373+
Now, the URL is updated to "/?counter=10" and page is re-rendered.
374+
You can see the updated URL with `this.props.url` inside the Component.
375+
376+
You can also watch for URL changes via [`componentWillReceiveProps`](https://facebook.github.io/react/docs/react-component.html#componentwillreceiveprops) hook as shown below:
377+
378+
```
379+
componentWillReceiveProps(nextProps) {
380+
const { pathname, query } = nextProps.url
381+
// fetch data based on the new query
382+
}
383+
```
384+
385+
> NOTES:
386+
>
387+
> Shallow routing works **only** for same page URL changes.
388+
>
389+
> For an example, let's assume we've another page called "about".
390+
> Now you are changing a URL like this:
391+
> ```js
392+
> Router.push('/about?counter=10', '/about?counter=10', { shallow: true })
393+
> ```
394+
> Since that's a new page, it'll run "getInitialProps" of the "about" page even we asked to do shallow routing.
395+
396+
352397
### Prefetching Pages
353398
354399
(This is a production only feature)

0 commit comments

Comments
 (0)