Skip to content

Commit 56662b4

Browse files
authored
Add/with router (vercel#2870)
* Add withRoute HOC Rebased (squashed) - removed routerToProps - updated hoist-non-react-statics - improved propTypes * Expose the whole Router instead of the route. * Make the example simple. * Update examples and the readme. * Add a test case.
1 parent d600957 commit 56662b4

File tree

15 files changed

+263
-11
lines changed

15 files changed

+263
-11
lines changed

examples/using-with-router/README.md

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
[![Deploy to now](https://deploy.now.sh/static/button.svg)](https://deploy.now.sh/?repo=https://github.com/zeit/next.js/tree/master/examples/using-with-router)
2+
# Example app utilizing `withRouter` utility for routing
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/using-with-router
10+
cd using-with-router
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+
Sometimes, we want to use the `router` inside component of our app without using the singleton `next/router` API.
29+
30+
You can do that by creating a React Higher Order Component with the help of the `withRouter` utility.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { withRouter } from 'next/router'
2+
3+
// typically you want to use `next/link` for this usecase
4+
// but this example shows how you can also access the router
5+
// using the withRouter utility.
6+
7+
const ActiveLink = ({ children, router, href }) => {
8+
const style = {
9+
marginRight: 10,
10+
color: router.pathname === href ? 'red' : 'black'
11+
}
12+
13+
const handleClick = (e) => {
14+
e.preventDefault()
15+
router.push(href)
16+
}
17+
18+
return (
19+
<a href={href} onClick={handleClick} style={style}>
20+
{children}
21+
</a>
22+
)
23+
}
24+
25+
export default withRouter(ActiveLink)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import ActiveLink from './ActiveLink'
2+
3+
export default () => (
4+
<div>
5+
<ActiveLink href='/'>Home</ActiveLink>
6+
<ActiveLink href='/about'>About</ActiveLink>
7+
<ActiveLink href='/error'>Error</ActiveLink>
8+
</div>
9+
)
+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"name": "using-router",
3+
"version": "1.0.0",
4+
"scripts": {
5+
"dev": "next",
6+
"build": "next build",
7+
"start": "next start"
8+
},
9+
"dependencies": {
10+
"hoist-non-react-statics": "^2.2.2",
11+
"next": "latest",
12+
"react": "^15.4.2",
13+
"react-dom": "^15.4.2"
14+
},
15+
"license": "ISC"
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import Header from '../components/Header'
2+
3+
export default () => (
4+
<div>
5+
<Header />
6+
<p>This is the about page.</p>
7+
</div>
8+
)
+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import {Component} from 'react'
2+
import Header from '../components/Header'
3+
import Router from 'next/router'
4+
5+
export default class extends Component {
6+
render () {
7+
return (
8+
<div>
9+
<Header />
10+
<p>This path({Router.pathname}) should not be rendered via SSR</p>
11+
</div>
12+
)
13+
}
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import Header from '../components/Header'
2+
3+
export default () => (
4+
<div>
5+
<Header />
6+
<p>HOME PAGE is here!</p>
7+
</div>
8+
)

lib/app.js

+7-2
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,20 @@ import React, { Component } from 'react'
22
import PropTypes from 'prop-types'
33
import shallowEquals from './shallow-equals'
44
import { warn } from './utils'
5+
import { makePublicRouterInstance } from './router'
56

67
export default class App extends Component {
78
static childContextTypes = {
8-
headManager: PropTypes.object
9+
headManager: PropTypes.object,
10+
router: PropTypes.object
911
}
1012

1113
getChildContext () {
1214
const { headManager } = this.props
13-
return { headManager }
15+
return {
16+
headManager,
17+
router: makePublicRouterInstance(this.props.router)
18+
}
1419
}
1520

1621
render () {

lib/router/index.js

+27
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ function throwIfNoRouter () {
6464
// Export the SingletonRouter and this is the public API.
6565
export default SingletonRouter
6666

67+
// Reexport the withRoute HOC
68+
export { default as withRouter } from './with-router'
69+
6770
// INTERNAL APIS
6871
// -------------
6972
// (do not use following exports inside the app)
@@ -109,3 +112,27 @@ export function _rewriteUrlForNextExport (url) {
109112

110113
return newPath
111114
}
115+
116+
export function makePublicRouterInstance (router) {
117+
const instance = {}
118+
119+
propertyFields.forEach((field) => {
120+
// Here we need to use Object.defineProperty because, we need to return
121+
// the property assigned to the actual router
122+
// The value might get changed as we change routes and this is the
123+
// proper way to access it
124+
Object.defineProperty(instance, field, {
125+
get () {
126+
return router[field]
127+
}
128+
})
129+
})
130+
131+
coreMethodFields.forEach((field) => {
132+
instance[field] = (...args) => {
133+
return router[field](...args)
134+
}
135+
})
136+
137+
return instance
138+
}

lib/router/with-router.js

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import React, { Component } from 'react'
2+
import PropTypes from 'prop-types'
3+
import hoistStatics from 'hoist-non-react-statics'
4+
import { getDisplayName } from '../utils'
5+
6+
export default function withRoute (ComposedComponent) {
7+
const displayName = getDisplayName(ComposedComponent)
8+
9+
class WithRouteWrapper extends Component {
10+
static contextTypes = {
11+
router: PropTypes.object
12+
}
13+
14+
static displayName = `withRoute(${displayName})`
15+
16+
render () {
17+
const props = {
18+
router: this.context.router,
19+
...this.props
20+
}
21+
22+
return <ComposedComponent {...props} />
23+
}
24+
}
25+
26+
return hoistStatics(WithRouteWrapper, ComposedComponent)
27+
}

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
"friendly-errors-webpack-plugin": "1.5.0",
7373
"glob": "7.1.1",
7474
"glob-promise": "3.1.0",
75+
"hoist-non-react-statics": "^2.2.2",
7576
"htmlescape": "1.1.1",
7677
"http-status": "1.0.1",
7778
"json-loader": "0.5.4",

readme.md

+40-1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ Next.js is a minimalistic framework for server-rendered React applications.
2929
- [Imperatively](#imperatively)
3030
- [Router Events](#router-events)
3131
- [Shallow Routing](#shallow-routing)
32+
- [Using a Higher Order Component](#using-a-higher-order-component)
3233
- [Prefetching Pages](#prefetching-pages)
3334
- [With `<Link>`](#with-link-1)
3435
- [Imperatively](#imperatively-1)
@@ -255,7 +256,7 @@ export default Page
255256

256257
- `pathname` - path section of URL
257258
- `query` - query string section of URL parsed as an object
258-
- `asPath` - the actual url path
259+
- `asPath` - `String` of the actual path (including the query) shows in the browser
259260
- `req` - HTTP request object (server only)
260261
- `res` - HTTP response object (server only)
261262
- `jsonPageRes` - [Fetch Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) object (client only)
@@ -392,6 +393,7 @@ Above `Router` object comes with the following API:
392393
- `route` - `String` of the current route
393394
- `pathname` - `String` of the current path excluding the query string
394395
- `query` - `Object` with the parsed query string. Defaults to `{}`
396+
- `asPath` - `String` of the actual path (including the query) shows in the browser
395397
- `push(url, as=url)` - performs a `pushState` call with the given url
396398
- `replace(url, as=url)` - performs a `replaceState` call with the given url
397399

@@ -504,6 +506,43 @@ componentWillReceiveProps(nextProps) {
504506
> ```
505507
> Since that's a new page, it'll unload the current page, load the new one and call `getInitialProps` even though we asked to do shallow routing.
506508
509+
#### Using a Higher Order Component
510+
511+
<p><details>
512+
<summary><b>Examples</b></summary>
513+
<ul>
514+
<li><a href="./examples/using-with-router">Using the `withRouter` utility</a></li>
515+
</ul>
516+
</details></p>
517+
518+
If you want to access the `router` object inside any component in your app, you can use the `withRouter` Higher-Order Component. Here's how to use it:
519+
520+
```jsx
521+
import { withRouter } from 'next/router'
522+
523+
const ActiveLink = ({ children, router, href }) => {
524+
const style = {
525+
marginRight: 10,
526+
color: router.pathname === href? 'red' : 'black'
527+
}
528+
529+
const handleClick = (e) => {
530+
e.preventDefault()
531+
router.push(href)
532+
}
533+
534+
return (
535+
<a href={href} onClick={handleClick} style={style}>
536+
{children}
537+
</a>
538+
)
539+
}
540+
541+
export default withRouter(ActiveLink)
542+
```
543+
544+
The above `router` object comes with an API similar to [`next/router`](#imperatively).
545+
507546
### Prefetching Pages
508547

509548
(This is a production only feature)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { withRouter } from 'next/router'
2+
3+
const Link = withRouter(({router, children, href}) => {
4+
const handleClick = (e) => {
5+
e.preventDefault()
6+
router.push(href)
7+
}
8+
9+
return (
10+
<div>
11+
<span>Current path: {router.pathname}</span>
12+
<a href='#' onClick={handleClick}>{children}</a>
13+
</div>
14+
)
15+
})
16+
17+
export default () => (
18+
<div className='nav-with-hoc'>
19+
<Link href='/nav'>Go Back</Link>
20+
<p>This is the about page.</p>
21+
</div>
22+
)

test/integration/basic/test/client-navigation.js

+17
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,23 @@ export default (context, render) => {
388388
})
389389
})
390390

391+
describe('with the HOC based router', () => {
392+
it('should navigate as expected', async () => {
393+
const browser = await webdriver(context.appPort, '/nav/with-hoc')
394+
395+
const spanText = await browser.elementByCss('span').text()
396+
expect(spanText).toBe('Current path: /nav/with-hoc')
397+
398+
const text = await browser
399+
.elementByCss('.nav-with-hoc a').click()
400+
.waitForElementByCss('.nav-home')
401+
.elementByCss('p').text()
402+
403+
expect(text).toBe('This is the home.')
404+
browser.close()
405+
})
406+
})
407+
391408
describe('with asPath', () => {
392409
describe('inside getInitialProps', () => {
393410
it('should show the correct asPath with a Link with as prop', async () => {

yarn.lock

+12-8
Original file line numberDiff line numberDiff line change
@@ -2687,6 +2687,10 @@ [email protected]:
26872687
version "2.16.3"
26882688
resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed"
26892689

2690+
hoist-non-react-statics@^2.2.2:
2691+
version "2.2.2"
2692+
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.2.2.tgz#c0eca5a7d5a28c5ada3107eb763b01da6bfa81fb"
2693+
26902694
home-or-tmp@^2.0.0:
26912695
version "2.0.0"
26922696
resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8"
@@ -5120,18 +5124,18 @@ stringstream@~0.0.4:
51205124
version "0.0.5"
51215125
resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878"
51225126

5123-
[email protected], strip-ansi@^4.0.0:
5124-
version "4.0.0"
5125-
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f"
5126-
dependencies:
5127-
ansi-regex "^3.0.0"
5128-
5129-
strip-ansi@^3.0.0, strip-ansi@^3.0.1:
5127+
[email protected], strip-ansi@^3.0.0, strip-ansi@^3.0.1:
51305128
version "3.0.1"
51315129
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
51325130
dependencies:
51335131
ansi-regex "^2.0.0"
51345132

5133+
strip-ansi@^4.0.0:
5134+
version "4.0.0"
5135+
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f"
5136+
dependencies:
5137+
ansi-regex "^3.0.0"
5138+
51355139
[email protected], strip-bom@^3.0.0:
51365140
version "3.0.0"
51375141
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
@@ -5386,7 +5390,7 @@ uglify-to-browserify@~1.0.0:
53865390
version "1.0.2"
53875391
resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7"
53885392

5389-
uglifyjs-webpack-plugin@0.4.6, uglifyjs-webpack-plugin@^0.4.6:
5393+
uglifyjs-webpack-plugin@^0.4.6:
53905394
version "0.4.6"
53915395
resolved "https://registry.yarnpkg.com/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-0.4.6.tgz#b951f4abb6bd617e66f63eb891498e391763e309"
53925396
dependencies:

0 commit comments

Comments
 (0)