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 () => ( +
+ Home + About + Error +
+) 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 () => ( +
+
+

HOME PAGE is here!

+
+) 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 ( +
+ Current path: {router.pathname} + {children} +
+ ) +}) + +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: