Skip to content

Commit 22776c2

Browse files
arunodarauchg
authored andcommittedDec 19, 2016
Implement the Singleton Router API (vercel#429)
* Immplement the initial singleton Router. * Use the new SingletonRouter for HMR error handling. * Use SingletonRouter inside the Link. * Create an example app using the Router. * Make the url parameter optional in Router.push and Router.replace * Add a section about next/router in the README.
1 parent 955f681 commit 22776c2

File tree

14 files changed

+217
-55
lines changed

14 files changed

+217
-55
lines changed
 

‎README.md

+50-7
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ and add a script to your package.json like this:
2222
{
2323
"scripts": {
2424
"dev": "next"
25-
}
25+
}
2626
}
2727
```
2828

@@ -142,9 +142,9 @@ For the initial page load, `getInitialProps` will execute on the server only. `g
142142
- `xhr` - XMLHttpRequest object (client only)
143143
- `err` - Error object if any error is encountered during the rendering
144144

145-
### Routing
145+
### Routing with <Link>
146146

147-
Client-side transitions between routes are enabled via a `<Link>` component
147+
Client-side transitions between routes can be enabled via a `<Link>` component
148148

149149
#### pages/index.js
150150

@@ -178,11 +178,54 @@ Each top-level component receives a `url` property with the following API:
178178
- `pushTo(url)` - performs a `pushState` call that renders the new `url`. This is equivalent to following a `<Link>`
179179
- `replaceTo(url)` - performs a `replaceState` call that renders the new `url`
180180

181+
### Routing with next/router
182+
183+
You can also do client-side page transitions using the `next/router`. This is the same API used inside the above `<Link />` component.
184+
185+
```jsx
186+
import Router from 'next/router'
187+
188+
const routeTo(href) {
189+
return (e) => {
190+
e.preventDefault()
191+
Router.push(href)
192+
}
193+
}
194+
195+
export default () => (
196+
<div>Click <a href='#' onClick={routeTo('/about')}>here</a> to read more</div>
197+
)
198+
```
199+
200+
#### pages/about.js
201+
202+
```jsx
203+
export default () => (
204+
<p>Welcome to About!</p>
205+
)
206+
```
207+
208+
Above `Router` object comes with the following API:
209+
210+
- `route` - `String` of the current route
211+
- `pathname` - `String` of the current path excluding the query string
212+
- `query` - `Object` with the parsed query string. Defaults to `{}`
213+
- `push(url, pathname=url)` - performs a `pushState` call associated with the current component
214+
- `replace(url, pathname=url)` - performs a `replaceState` call associated with the current component
215+
216+
> Usually, route is the same as pathname.
217+
> But when used with programmatic API, route and pathname can be different.
218+
> "route" is your actual page's path while "pathname" is the path of the url mapped to it.
219+
>
220+
> Likewise, url and path is the same usually.
221+
> But when used with programmatic API, "url" is the route with the query string.
222+
> "pathname" is the path of the url mapped to it.
223+
181224
### Prefetching Pages
182225

183-
Next.js exposes a module that configures a `ServiceWorker` automatically to prefetch pages: `next/prefetch`.
226+
Next.js exposes a module that configures a `ServiceWorker` automatically to prefetch pages: `next/prefetch`.
184227

185-
Since Next.js server-renders your pages, this allows all the future interaction paths of your app to be instant. Effectively Next.js gives you the great initial download performance of a _website_, with the ahead-of-time download capabilities of an _app_. [Read more](https://zeit.co/blog/next#anticipation-is-the-key-to-performance).
228+
Since Next.js server-renders your pages, this allows all the future interaction paths of your app to be instant. Effectively Next.js gives you the great initial download performance of a _website_, with the ahead-of-time download capabilities of an _app_. [Read more](https://zeit.co/blog/next#anticipation-is-the-key-to-performance).
186229

187230
#### Link prefetching
188231

@@ -251,7 +294,7 @@ export default class Error extends React.Component {
251294

252295
### Custom configuration
253296

254-
For custom advanced behavior of Next.js, you can create a `next.config.js` in the root of your project directory (next to `pages/` and `package.json`).
297+
For custom advanced behavior of Next.js, you can create a `next.config.js` in the root of your project directory (next to `pages/` and `package.json`).
255298

256299
Note: `next.config.js` is a regular Node.js module, not a JSON file. It gets used by the Next server and build phases, and not included in the browser build.
257300

@@ -264,7 +307,7 @@ module.exports = {
264307

265308
### Customizing webpack config
266309

267-
In order to extend our usage of `webpack`, you can define a function that extends its config.
310+
In order to extend our usage of `webpack`, you can define a function that extends its config.
268311

269312
The following example shows how you can use [`react-svg-loader`](https://github.com/boopathi/react-svg-loader) to easily import any `.svg` file as a React component, without modification.
270313

‎client/next.js

+13-21
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import { createElement } from 'react'
22
import { render } from 'react-dom'
33
import HeadManager from './head-manager'
4-
import domready from 'domready'
54
import { rehydrate } from '../lib/css'
6-
import Router from '../lib/router'
5+
import { createRouter } from '../lib/router'
76
import App from '../lib/app'
87
import evalScript from '../lib/eval-script'
98

@@ -19,25 +18,18 @@ const {
1918
}
2019
} = window
2120

22-
domready(() => {
23-
const Component = evalScript(component).default
24-
const ErrorComponent = evalScript(errorComponent).default
21+
const Component = evalScript(component).default
22+
const ErrorComponent = evalScript(errorComponent).default
2523

26-
const router = new Router(pathname, query, {
27-
Component,
28-
ErrorComponent,
29-
ctx: { err }
30-
})
31-
32-
// This it to support error handling in the dev time with hot code reload.
33-
if (window.next) {
34-
window.next.router = router
35-
}
24+
export const router = createRouter(pathname, query, {
25+
Component,
26+
ErrorComponent,
27+
ctx: { err }
28+
})
3629

37-
const headManager = new HeadManager()
38-
const container = document.getElementById('__next')
39-
const appProps = { Component, props, router, headManager }
30+
const headManager = new HeadManager()
31+
const container = document.getElementById('__next')
32+
const appProps = { Component, props, router, headManager }
4033

41-
if (ids) rehydrate(ids)
42-
render(createElement(App, appProps), container)
43-
})
34+
if (ids) rehydrate(ids)
35+
render(createElement(App, appProps), container)

‎client/webpack-hot-middleware-client.js

+7-7
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,27 @@
1-
/* global next */
21
import webpackHotMiddlewareClient from 'webpack-hot-middleware/client?overlay=false&reload=true'
2+
import Router from '../lib/router'
33

44
const handlers = {
55
reload (route) {
66
if (route === '/_error') {
7-
for (const r of Object.keys(next.router.components)) {
8-
const { Component } = next.router.components[r]
7+
for (const r of Object.keys(Router.components)) {
8+
const { Component } = Router.components[r]
99
if (Component.__route === '/_error-debug') {
1010
// reload all '/_error-debug'
1111
// which are expected to be errors of '/_error' routes
12-
next.router.reload(r)
12+
Router.reload(r)
1313
}
1414
}
1515
return
1616
}
1717

18-
next.router.reload(route)
18+
Router.reload(route)
1919
},
2020
change (route) {
21-
const { Component } = next.router.components[route] || {}
21+
const { Component } = Router.components[route] || {}
2222
if (Component && Component.__route === '/_error-debug') {
2323
// reload to recover from runtime errors
24-
next.router.reload(route)
24+
Router.reload(route)
2525
}
2626
},
2727
hardReload () {

‎examples/using-router/README.md

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Example app utilizing next/router for routing
2+
3+
This example features:
4+
5+
* An app linking pages using `next/router` instead of `<Link>` component.
6+
* Access the pathname using `next/router` and render it in a component
7+
8+
## How to run it
9+
10+
```sh
11+
npm install
12+
npm run dev
13+
```
+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import React from 'react'
2+
import Router from 'next/router'
3+
4+
const styles = {
5+
a: {
6+
marginRight: 10
7+
}
8+
}
9+
10+
const Link = ({ children, href }) => (
11+
<a
12+
href='#'
13+
style={styles.a}
14+
onClick={(e) => {
15+
e.preventDefault()
16+
Router.push(href)
17+
}}
18+
>
19+
{ children }
20+
</a>
21+
)
22+
23+
export default () => (
24+
<div>
25+
<Link href='/'>Home</Link>
26+
<Link href='/about'>About</Link>
27+
<div>
28+
<small>Now you are in the route: {Router.route} </small>
29+
</div>
30+
</div>
31+
)

‎examples/using-router/package.json

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"name": "shared-modules",
3+
"version": "1.0.0",
4+
"description": "This example features:",
5+
"main": "index.js",
6+
"scripts": {
7+
"dev": "next",
8+
"build": "next build",
9+
"start": "next start"
10+
},
11+
"dependencies": {
12+
"next": "*"
13+
},
14+
"author": "",
15+
"license": "ISC"
16+
}

‎examples/using-router/pages/about.js

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import React from 'react'
2+
import Header from '../components/Header'
3+
4+
export default () => (
5+
<div>
6+
<Header />
7+
<p>This is the about page.</p>
8+
</div>
9+
)

‎examples/using-router/pages/index.js

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import React from 'react'
2+
import Header from '../components/Header'
3+
4+
export default () => (
5+
<div>
6+
<Header />
7+
<p>HOME PAGE is here!</p>
8+
</div>
9+
)

‎lib/link.js

+10-13
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
1-
import React, { Component, PropTypes, Children } from 'react'
1+
import React, { Component, Children } from 'react'
2+
import Router from './router'
23

34
export default class Link extends Component {
4-
static contextTypes = {
5-
router: PropTypes.object
6-
}
7-
85
constructor (props) {
96
super(props)
107
this.linkClicked = this.linkClicked.bind(this)
@@ -30,14 +27,14 @@ export default class Link extends Component {
3027
const url = as || href
3128

3229
// straight up redirect
33-
this.context.router.push(route, url)
34-
.then((success) => {
35-
if (!success) return
36-
if (scroll !== false) window.scrollTo(0, 0)
37-
})
38-
.catch((err) => {
39-
if (this.props.onError) this.props.onError(err)
40-
})
30+
Router.push(route, url)
31+
.then((success) => {
32+
if (!success) return
33+
if (scroll !== false) window.scrollTo(0, 0)
34+
})
35+
.catch((err) => {
36+
if (this.props.onError) this.props.onError(err)
37+
})
4138
}
4239

4340
render () {

‎lib/router/index.js

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import _Router from './router'
2+
3+
// holds the actual router instance
4+
let router = null
5+
6+
const SingletonRouter = {}
7+
8+
// Create public properties and methods of the router in the SingletonRouter
9+
const propertyFields = ['route', 'components', 'pathname', 'query']
10+
const methodFields = ['push', 'replace', 'reload', 'back']
11+
12+
propertyFields.forEach((field) => {
13+
// Here we need to use Object.defineProperty because, we need to return
14+
// the property assigned to the actual router
15+
// The value might get changed as we change routes and this is the
16+
// proper way to access it
17+
Object.defineProperty(SingletonRouter, field, {
18+
get () {
19+
return router[field]
20+
}
21+
})
22+
})
23+
24+
methodFields.forEach((field) => {
25+
SingletonRouter[field] = (...args) => {
26+
return router[field](...args)
27+
}
28+
})
29+
30+
// This is an internal method and it should not be called directly.
31+
//
32+
// ## Client Side Usage
33+
// We create the router in the client side only for a single time when we are
34+
// booting the app. It happens before rendering any components.
35+
// At the time of the component rendering, there'll be a router instance
36+
//
37+
// ## Server Side Usage
38+
// We create router for every SSR page render.
39+
// Since rendering happens in the same eventloop this works properly.
40+
export const createRouter = function (...args) {
41+
router = new _Router(...args)
42+
return router
43+
}
44+
45+
// Export the actual Router class, which is also use internally
46+
// You'll ever need to access this directly
47+
export const Router = _Router
48+
49+
// Export the SingletonRouter and this is the public API.
50+
// This is an client side API and doesn't available on the server
51+
export default SingletonRouter

‎lib/router.js ‎lib/router/router.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { parse } from 'url'
2-
import evalScript from './eval-script'
3-
import shallowEquals from './shallow-equals'
2+
import evalScript from '../eval-script'
3+
import shallowEquals from '../shallow-equals'
44

55
export default class Router {
66
constructor (pathname, query, { Component, ErrorComponent, ctx } = {}) {
@@ -97,11 +97,11 @@ export default class Router {
9797
window.history.back()
9898
}
9999

100-
push (route, url) {
100+
push (route, url = route) {
101101
return this.change('pushState', route, url)
102102
}
103103

104-
replace (route, url) {
104+
replace (route, url = route) {
105105
return this.change('replaceState', route, url)
106106
}
107107

‎package.json

-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@
4848
"chokidar": "1.6.1",
4949
"cross-spawn": "5.0.1",
5050
"del": "2.2.2",
51-
"domready": "1.0.8",
5251
"friendly-errors-webpack-plugin": "1.1.2",
5352
"glamor": "2.20.12",
5453
"glob-promise": "3.1.0",

‎router.js

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = require('./dist/lib/router')

‎server/render.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { createElement } from 'react'
33
import { renderToString, renderToStaticMarkup } from 'react-dom/server'
44
import requireModule from './require'
55
import read from './read'
6-
import Router from '../lib/router'
6+
import { createRouter } from '../lib/router'
77
import Head, { defaultHead } from '../lib/head'
88
import App from '../lib/app'
99

@@ -56,10 +56,11 @@ async function doRender (req, res, pathname, query, {
5656
if (res.finished) return
5757

5858
const renderPage = () => {
59+
const router = createRouter(pathname, query)
5960
const app = createElement(App, {
6061
Component,
6162
props,
62-
router: new Router(pathname, query)
63+
router
6364
})
6465
const html = (staticMarkup ? renderToStaticMarkup : renderToString)(app)
6566
const head = Head.rewind() || defaultHead()

0 commit comments

Comments
 (0)
Please sign in to comment.