diff --git a/examples/using-with-router/README.md b/examples/using-with-router/README.md
new file mode 100644
index 00000000..7d8988af
--- /dev/null
+++ b/examples/using-with-router/README.md
@@ -0,0 +1,30 @@
+[![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)
+# Example app utilizing `withRouter` utility for routing
+
+## How to use
+
+Download the example [or clone the repo](https://github.com/zeit/next.js):
+
+```bash
+curl https://codeload.github.com/zeit/next.js/tar.gz/master | tar -xz --strip=2 next.js-master/examples/using-with-router
+cd using-with-router
+```
+
+Install it and run:
+
+```bash
+npm install
+npm run dev
+```
+
+Deploy it to the cloud with [now](https://zeit.co/now) ([download](https://zeit.co/download))
+
+```bash
+now
+```
+
+## The idea behind the example
+
+Sometimes, we want to use the `router` inside component of our app without using the singleton `next/router` API.
+
+You can do that by creating a React Higher Order Component with the help of the `withRouter` utility.
diff --git a/examples/using-with-router/components/ActiveLink.js b/examples/using-with-router/components/ActiveLink.js
new file mode 100644
index 00000000..5869e753
--- /dev/null
+++ b/examples/using-with-router/components/ActiveLink.js
@@ -0,0 +1,25 @@
+import { withRouter } from 'next/router'
+
+// typically you want to use `next/link` for this usecase
+// but this example shows how you can also access the router
+// using the withRouter utility.
+
+const ActiveLink = ({ children, router, href }) => {
+ const style = {
+ marginRight: 10,
+ color: router.pathname === href ? 'red' : 'black'
+ }
+
+ const handleClick = (e) => {
+ e.preventDefault()
+ router.push(href)
+ }
+
+ return (
+
+ {children}
+
+ )
+}
+
+export default withRouter(ActiveLink)
diff --git a/examples/using-with-router/components/Header.js b/examples/using-with-router/components/Header.js
new file mode 100644
index 00000000..4e886aac
--- /dev/null
+++ b/examples/using-with-router/components/Header.js
@@ -0,0 +1,9 @@
+import ActiveLink from './ActiveLink'
+
+export default () => (
+
+)
diff --git a/examples/using-with-router/package.json b/examples/using-with-router/package.json
new file mode 100644
index 00000000..23a3c5eb
--- /dev/null
+++ b/examples/using-with-router/package.json
@@ -0,0 +1,16 @@
+{
+ "name": "using-router",
+ "version": "1.0.0",
+ "scripts": {
+ "dev": "next",
+ "build": "next build",
+ "start": "next start"
+ },
+ "dependencies": {
+ "hoist-non-react-statics": "^2.2.2",
+ "next": "latest",
+ "react": "^15.4.2",
+ "react-dom": "^15.4.2"
+ },
+ "license": "ISC"
+}
diff --git a/examples/using-with-router/pages/about.js b/examples/using-with-router/pages/about.js
new file mode 100644
index 00000000..7299950d
--- /dev/null
+++ b/examples/using-with-router/pages/about.js
@@ -0,0 +1,8 @@
+import Header from '../components/Header'
+
+export default () => (
+
+
+
This is the about page.
+
+)
diff --git a/examples/using-with-router/pages/error.js b/examples/using-with-router/pages/error.js
new file mode 100644
index 00000000..21e54b20
--- /dev/null
+++ b/examples/using-with-router/pages/error.js
@@ -0,0 +1,14 @@
+import {Component} from 'react'
+import Header from '../components/Header'
+import Router from 'next/router'
+
+export default class extends Component {
+ render () {
+ return (
+
+
+
This path({Router.pathname}) should not be rendered via SSR
+
+ )
+ }
+}
diff --git a/examples/using-with-router/pages/index.js b/examples/using-with-router/pages/index.js
new file mode 100644
index 00000000..37f528b6
--- /dev/null
+++ b/examples/using-with-router/pages/index.js
@@ -0,0 +1,8 @@
+import Header from '../components/Header'
+
+export default () => (
+
+)
diff --git a/lib/app.js b/lib/app.js
index b42579e6..b8590061 100644
--- a/lib/app.js
+++ b/lib/app.js
@@ -2,15 +2,20 @@ import React, { Component } from 'react'
import PropTypes from 'prop-types'
import shallowEquals from './shallow-equals'
import { warn } from './utils'
+import { makePublicRouterInstance } from './router'
export default class App extends Component {
static childContextTypes = {
- headManager: PropTypes.object
+ headManager: PropTypes.object,
+ router: PropTypes.object
}
getChildContext () {
const { headManager } = this.props
- return { headManager }
+ return {
+ headManager,
+ router: makePublicRouterInstance(this.props.router)
+ }
}
render () {
diff --git a/lib/router/index.js b/lib/router/index.js
index c5dd5135..8fa37e7d 100644
--- a/lib/router/index.js
+++ b/lib/router/index.js
@@ -64,6 +64,9 @@ function throwIfNoRouter () {
// Export the SingletonRouter and this is the public API.
export default SingletonRouter
+// Reexport the withRoute HOC
+export { default as withRouter } from './with-router'
+
// INTERNAL APIS
// -------------
// (do not use following exports inside the app)
@@ -109,3 +112,27 @@ export function _rewriteUrlForNextExport (url) {
return newPath
}
+
+export function makePublicRouterInstance (router) {
+ const instance = {}
+
+ 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(instance, field, {
+ get () {
+ return router[field]
+ }
+ })
+ })
+
+ coreMethodFields.forEach((field) => {
+ instance[field] = (...args) => {
+ return router[field](...args)
+ }
+ })
+
+ return instance
+}
diff --git a/lib/router/with-router.js b/lib/router/with-router.js
new file mode 100644
index 00000000..992cd2f8
--- /dev/null
+++ b/lib/router/with-router.js
@@ -0,0 +1,27 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import hoistStatics from 'hoist-non-react-statics'
+import { getDisplayName } from '../utils'
+
+export default function withRoute (ComposedComponent) {
+ const displayName = getDisplayName(ComposedComponent)
+
+ class WithRouteWrapper extends Component {
+ static contextTypes = {
+ router: PropTypes.object
+ }
+
+ static displayName = `withRoute(${displayName})`
+
+ render () {
+ const props = {
+ router: this.context.router,
+ ...this.props
+ }
+
+ return
+ }
+ }
+
+ return hoistStatics(WithRouteWrapper, ComposedComponent)
+}
diff --git a/package.json b/package.json
index 52a5b0e8..7c94bd85 100644
--- a/package.json
+++ b/package.json
@@ -72,6 +72,7 @@
"friendly-errors-webpack-plugin": "1.5.0",
"glob": "7.1.1",
"glob-promise": "3.1.0",
+ "hoist-non-react-statics": "^2.2.2",
"htmlescape": "1.1.1",
"http-status": "1.0.1",
"json-loader": "0.5.4",
diff --git a/readme.md b/readme.md
index d844670b..b4ca4e69 100644
--- a/readme.md
+++ b/readme.md
@@ -29,6 +29,7 @@ Next.js is a minimalistic framework for server-rendered React applications.
- [Imperatively](#imperatively)
- [Router Events](#router-events)
- [Shallow Routing](#shallow-routing)
+ - [Using a Higher Order Component](#using-a-higher-order-component)
- [Prefetching Pages](#prefetching-pages)
- [With ` `](#with-link-1)
- [Imperatively](#imperatively-1)
@@ -255,7 +256,7 @@ export default Page
- `pathname` - path section of URL
- `query` - query string section of URL parsed as an object
-- `asPath` - the actual url path
+- `asPath` - `String` of the actual path (including the query) shows in the browser
- `req` - HTTP request object (server only)
- `res` - HTTP response object (server only)
- `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:
- `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 `{}`
+- `asPath` - `String` of the actual path (including the query) shows in the browser
- `push(url, as=url)` - performs a `pushState` call with the given url
- `replace(url, as=url)` - performs a `replaceState` call with the given url
@@ -504,6 +506,43 @@ componentWillReceiveProps(nextProps) {
> ```
> 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.
+#### Using a Higher Order Component
+
+
+ Examples
+
+
+
+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:
+
+```jsx
+import { withRouter } from 'next/router'
+
+const ActiveLink = ({ children, router, href }) => {
+ const style = {
+ marginRight: 10,
+ color: router.pathname === href? 'red' : 'black'
+ }
+
+ const handleClick = (e) => {
+ e.preventDefault()
+ router.push(href)
+ }
+
+ return (
+
+ {children}
+
+ )
+}
+
+export default withRouter(ActiveLink)
+```
+
+The above `router` object comes with an API similar to [`next/router`](#imperatively).
+
### Prefetching Pages
(This is a production only feature)
diff --git a/test/integration/basic/pages/nav/with-hoc.js b/test/integration/basic/pages/nav/with-hoc.js
new file mode 100644
index 00000000..cbab2539
--- /dev/null
+++ b/test/integration/basic/pages/nav/with-hoc.js
@@ -0,0 +1,22 @@
+import { withRouter } from 'next/router'
+
+const Link = withRouter(({router, children, href}) => {
+ const handleClick = (e) => {
+ e.preventDefault()
+ router.push(href)
+ }
+
+ return (
+
+ )
+})
+
+export default () => (
+
+
Go Back
+
This is the about page.
+
+)
diff --git a/test/integration/basic/test/client-navigation.js b/test/integration/basic/test/client-navigation.js
index e12b02a5..28093677 100644
--- a/test/integration/basic/test/client-navigation.js
+++ b/test/integration/basic/test/client-navigation.js
@@ -388,6 +388,23 @@ export default (context, render) => {
})
})
+ describe('with the HOC based router', () => {
+ it('should navigate as expected', async () => {
+ const browser = await webdriver(context.appPort, '/nav/with-hoc')
+
+ const spanText = await browser.elementByCss('span').text()
+ expect(spanText).toBe('Current path: /nav/with-hoc')
+
+ const text = await browser
+ .elementByCss('.nav-with-hoc a').click()
+ .waitForElementByCss('.nav-home')
+ .elementByCss('p').text()
+
+ expect(text).toBe('This is the home.')
+ browser.close()
+ })
+ })
+
describe('with asPath', () => {
describe('inside getInitialProps', () => {
it('should show the correct asPath with a Link with as prop', async () => {
diff --git a/yarn.lock b/yarn.lock
index fb8f2aa1..df262115 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2687,6 +2687,10 @@ hoek@2.x.x:
version "2.16.3"
resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed"
+hoist-non-react-statics@^2.2.2:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.2.2.tgz#c0eca5a7d5a28c5ada3107eb763b01da6bfa81fb"
+
home-or-tmp@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8"
@@ -5120,18 +5124,18 @@ stringstream@~0.0.4:
version "0.0.5"
resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878"
-strip-ansi@4.0.0, strip-ansi@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f"
- dependencies:
- ansi-regex "^3.0.0"
-
-strip-ansi@^3.0.0, strip-ansi@^3.0.1:
+strip-ansi@3.0.1, strip-ansi@^3.0.0, strip-ansi@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
dependencies:
ansi-regex "^2.0.0"
+strip-ansi@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f"
+ dependencies:
+ ansi-regex "^3.0.0"
+
strip-bom@3.0.0, strip-bom@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
@@ -5386,7 +5390,7 @@ uglify-to-browserify@~1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7"
-uglifyjs-webpack-plugin@0.4.6, uglifyjs-webpack-plugin@^0.4.6:
+uglifyjs-webpack-plugin@^0.4.6:
version "0.4.6"
resolved "https://registry.yarnpkg.com/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-0.4.6.tgz#b951f4abb6bd617e66f63eb891498e391763e309"
dependencies: