Skip to content

Commit

Permalink
Implement the Singleton Router API (#429)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
arunoda authored and rauchg committed Dec 19, 2016
1 parent 955f681 commit 22776c2
Show file tree
Hide file tree
Showing 14 changed files with 217 additions and 55 deletions.
57 changes: 50 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ and add a script to your package.json like this:
{
"scripts": {
"dev": "next"
}
}
}
```

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

### Routing
### Routing with <Link>

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

#### pages/index.js

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

### Routing with next/router

You can also do client-side page transitions using the `next/router`. This is the same API used inside the above `<Link />` component.

```jsx
import Router from 'next/router'

const routeTo(href) {
return (e) => {
e.preventDefault()
Router.push(href)
}
}

export default () => (
<div>Click <a href='#' onClick={routeTo('/about')}>here</a> to read more</div>
)
```

#### pages/about.js

```jsx
export default () => (
<p>Welcome to About!</p>
)
```

Above `Router` object comes with the following API:

- `route` - `String` of the current route
- `pathname` - `String` of the current path excluding the query string
- `query` - `Object` with the parsed query string. Defaults to `{}`
- `push(url, pathname=url)` - performs a `pushState` call associated with the current component
- `replace(url, pathname=url)` - performs a `replaceState` call associated with the current component

> Usually, route is the same as pathname.
> But when used with programmatic API, route and pathname can be different.
> "route" is your actual page's path while "pathname" is the path of the url mapped to it.
>
> Likewise, url and path is the same usually.
> But when used with programmatic API, "url" is the route with the query string.
> "pathname" is the path of the url mapped to it.
### Prefetching Pages

Next.js exposes a module that configures a `ServiceWorker` automatically to prefetch pages: `next/prefetch`.
Next.js exposes a module that configures a `ServiceWorker` automatically to prefetch pages: `next/prefetch`.

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).
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).

#### Link prefetching

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

### Custom configuration

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`).
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`).

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.

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

### Customizing webpack config

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

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.

Expand Down
34 changes: 13 additions & 21 deletions client/next.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { createElement } from 'react'
import { render } from 'react-dom'
import HeadManager from './head-manager'
import domready from 'domready'
import { rehydrate } from '../lib/css'
import Router from '../lib/router'
import { createRouter } from '../lib/router'
import App from '../lib/app'
import evalScript from '../lib/eval-script'

Expand All @@ -19,25 +18,18 @@ const {
}
} = window

domready(() => {
const Component = evalScript(component).default
const ErrorComponent = evalScript(errorComponent).default
const Component = evalScript(component).default
const ErrorComponent = evalScript(errorComponent).default

const router = new Router(pathname, query, {
Component,
ErrorComponent,
ctx: { err }
})

// This it to support error handling in the dev time with hot code reload.
if (window.next) {
window.next.router = router
}
export const router = createRouter(pathname, query, {
Component,
ErrorComponent,
ctx: { err }
})

const headManager = new HeadManager()
const container = document.getElementById('__next')
const appProps = { Component, props, router, headManager }
const headManager = new HeadManager()
const container = document.getElementById('__next')
const appProps = { Component, props, router, headManager }

if (ids) rehydrate(ids)
render(createElement(App, appProps), container)
})
if (ids) rehydrate(ids)
render(createElement(App, appProps), container)
14 changes: 7 additions & 7 deletions client/webpack-hot-middleware-client.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
/* global next */
import webpackHotMiddlewareClient from 'webpack-hot-middleware/client?overlay=false&reload=true'
import Router from '../lib/router'

const handlers = {
reload (route) {
if (route === '/_error') {
for (const r of Object.keys(next.router.components)) {
const { Component } = next.router.components[r]
for (const r of Object.keys(Router.components)) {
const { Component } = Router.components[r]
if (Component.__route === '/_error-debug') {
// reload all '/_error-debug'
// which are expected to be errors of '/_error' routes
next.router.reload(r)
Router.reload(r)
}
}
return
}

next.router.reload(route)
Router.reload(route)
},
change (route) {
const { Component } = next.router.components[route] || {}
const { Component } = Router.components[route] || {}
if (Component && Component.__route === '/_error-debug') {
// reload to recover from runtime errors
next.router.reload(route)
Router.reload(route)
}
},
hardReload () {
Expand Down
13 changes: 13 additions & 0 deletions examples/using-router/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Example app utilizing next/router for routing

This example features:

* An app linking pages using `next/router` instead of `<Link>` component.
* Access the pathname using `next/router` and render it in a component

## How to run it

```sh
npm install
npm run dev
```
31 changes: 31 additions & 0 deletions examples/using-router/components/Header.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React from 'react'
import Router from 'next/router'

const styles = {
a: {
marginRight: 10
}
}

const Link = ({ children, href }) => (
<a
href='#'
style={styles.a}
onClick={(e) => {
e.preventDefault()
Router.push(href)
}}
>
{ children }
</a>
)

export default () => (
<div>
<Link href='/'>Home</Link>
<Link href='/about'>About</Link>
<div>
<small>Now you are in the route: {Router.route} </small>
</div>
</div>
)
16 changes: 16 additions & 0 deletions examples/using-router/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "shared-modules",
"version": "1.0.0",
"description": "This example features:",
"main": "index.js",
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
},
"dependencies": {
"next": "*"
},
"author": "",
"license": "ISC"
}
9 changes: 9 additions & 0 deletions examples/using-router/pages/about.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React from 'react'
import Header from '../components/Header'

export default () => (
<div>
<Header />
<p>This is the about page.</p>
</div>
)
9 changes: 9 additions & 0 deletions examples/using-router/pages/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React from 'react'
import Header from '../components/Header'

export default () => (
<div>
<Header />
<p>HOME PAGE is here!</p>
</div>
)
23 changes: 10 additions & 13 deletions lib/link.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import React, { Component, PropTypes, Children } from 'react'
import React, { Component, Children } from 'react'
import Router from './router'

export default class Link extends Component {
static contextTypes = {
router: PropTypes.object
}

constructor (props) {
super(props)
this.linkClicked = this.linkClicked.bind(this)
Expand All @@ -30,14 +27,14 @@ export default class Link extends Component {
const url = as || href

// straight up redirect
this.context.router.push(route, url)
.then((success) => {
if (!success) return
if (scroll !== false) window.scrollTo(0, 0)
})
.catch((err) => {
if (this.props.onError) this.props.onError(err)
})
Router.push(route, url)
.then((success) => {
if (!success) return
if (scroll !== false) window.scrollTo(0, 0)
})
.catch((err) => {
if (this.props.onError) this.props.onError(err)
})
}

render () {
Expand Down
51 changes: 51 additions & 0 deletions lib/router/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import _Router from './router'

// holds the actual router instance
let router = null

const SingletonRouter = {}

// Create public properties and methods of the router in the SingletonRouter
const propertyFields = ['route', 'components', 'pathname', 'query']
const methodFields = ['push', 'replace', 'reload', 'back']

propertyFields.forEach((field) => {
// Here we need to use Object.defineProperty because, we need to return
// the property assigned to the actual router
// The value might get changed as we change routes and this is the
// proper way to access it
Object.defineProperty(SingletonRouter, field, {
get () {
return router[field]
}
})
})

methodFields.forEach((field) => {
SingletonRouter[field] = (...args) => {
return router[field](...args)
}
})

// This is an internal method and it should not be called directly.
//
// ## Client Side Usage
// We create the router in the client side only for a single time when we are
// booting the app. It happens before rendering any components.
// At the time of the component rendering, there'll be a router instance
//
// ## Server Side Usage
// We create router for every SSR page render.
// Since rendering happens in the same eventloop this works properly.
export const createRouter = function (...args) {
router = new _Router(...args)
return router
}

// Export the actual Router class, which is also use internally
// You'll ever need to access this directly
export const Router = _Router

// Export the SingletonRouter and this is the public API.
// This is an client side API and doesn't available on the server
export default SingletonRouter
8 changes: 4 additions & 4 deletions lib/router.js → lib/router/router.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { parse } from 'url'
import evalScript from './eval-script'
import shallowEquals from './shallow-equals'
import evalScript from '../eval-script'
import shallowEquals from '../shallow-equals'

export default class Router {
constructor (pathname, query, { Component, ErrorComponent, ctx } = {}) {
Expand Down Expand Up @@ -97,11 +97,11 @@ export default class Router {
window.history.back()
}

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

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

Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@
"chokidar": "1.6.1",
"cross-spawn": "5.0.1",
"del": "2.2.2",
"domready": "1.0.8",
"friendly-errors-webpack-plugin": "1.1.2",
"glamor": "2.20.12",
"glob-promise": "3.1.0",
Expand Down
1 change: 1 addition & 0 deletions router.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('./dist/lib/router')
Loading

0 comments on commit 22776c2

Please sign in to comment.