Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add/with router #2870

Merged
merged 6 commits into from Aug 30, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
30 changes: 30 additions & 0 deletions 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.
25 changes: 25 additions & 0 deletions 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 (
<a href={href} onClick={handleClick} style={style}>
{children}
</a>
)
}

export default withRouter(ActiveLink)
9 changes: 9 additions & 0 deletions examples/using-with-router/components/Header.js
@@ -0,0 +1,9 @@
import ActiveLink from './ActiveLink'

export default () => (
<div>
<ActiveLink href='/'>Home</ActiveLink>
<ActiveLink href='/about'>About</ActiveLink>
<ActiveLink href='/error'>Error</ActiveLink>
</div>
)
16 changes: 16 additions & 0 deletions 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"
}
8 changes: 8 additions & 0 deletions examples/using-with-router/pages/about.js
@@ -0,0 +1,8 @@
import Header from '../components/Header'

export default () => (
<div>
<Header />
<p>This is the about page.</p>
</div>
)
14 changes: 14 additions & 0 deletions 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 (
<div>
<Header />
<p>This path({Router.pathname}) should not be rendered via SSR</p>
</div>
)
}
}
8 changes: 8 additions & 0 deletions examples/using-with-router/pages/index.js
@@ -0,0 +1,8 @@
import Header from '../components/Header'

export default () => (
<div>
<Header />
<p>HOME PAGE is here!</p>
</div>
)
9 changes: 7 additions & 2 deletions lib/app.js
Expand Up @@ -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 () {
Expand Down
27 changes: 27 additions & 0 deletions lib/router/index.js
Expand Up @@ -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)
Expand Down Expand Up @@ -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
}
27 changes: 27 additions & 0 deletions 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 <ComposedComponent {...props} />
}
}

return hoistStatics(WithRouteWrapper, ComposedComponent)
}
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -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",
Expand Down
41 changes: 40 additions & 1 deletion readme.md
Expand Up @@ -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 `<Link>`](#with-link-1)
- [Imperatively](#imperatively-1)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

<p><details>
<summary><b>Examples</b></summary>
<ul>
<li><a href="./examples/using-with-router">Using the `withRouter` utility</a></li>
</ul>
</details></p>

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 (
<a href={href} onClick={handleClick} style={style}>
{children}
</a>
)
}

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)
Expand Down
22 changes: 22 additions & 0 deletions 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 (
<div>
<span>Current path: {router.pathname}</span>
<a href='#' onClick={handleClick}>{children}</a>
</div>
)
})

export default () => (
<div className='nav-with-hoc'>
<Link href='/nav'>Go Back</Link>
<p>This is the about page.</p>
</div>
)
17 changes: 17 additions & 0 deletions test/integration/basic/test/client-navigation.js
Expand Up @@ -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 () => {
Expand Down
20 changes: 12 additions & 8 deletions yarn.lock
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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:
Expand Down