Skip to content

Commit f8f3fa7

Browse files
arunodarauchg
authored andcommitted
Introducing Shallow Routing (vercel#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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy