1
0
Fork 0
mirror of https://github.com/terribleplan/next.js.git synced 2024-01-19 02:48:18 +00:00

Add/with router (#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.
This commit is contained in:
Arunoda Susiripala 2017-08-30 19:37:12 +05:30 committed by GitHub
parent d600957aeb
commit 56662b4d84
15 changed files with 263 additions and 11 deletions

View file

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

View file

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

View file

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

View file

@ -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"
}

View file

@ -0,0 +1,8 @@
import Header from '../components/Header'
export default () => (
<div>
<Header />
<p>This is the about page.</p>
</div>
)

View file

@ -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>
)
}
}

View file

@ -0,0 +1,8 @@
import Header from '../components/Header'
export default () => (
<div>
<Header />
<p>HOME PAGE is here!</p>
</div>
)

View file

@ -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 () {

View file

@ -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
}

27
lib/router/with-router.js Normal file
View file

@ -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)
}

View file

@ -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",

View file

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

View file

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

View file

@ -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 () => {

View file

@ -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: