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:
parent
d600957aeb
commit
56662b4d84
30
examples/using-with-router/README.md
Normal file
30
examples/using-with-router/README.md
Normal 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.
|
25
examples/using-with-router/components/ActiveLink.js
Normal file
25
examples/using-with-router/components/ActiveLink.js
Normal 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)
|
9
examples/using-with-router/components/Header.js
Normal file
9
examples/using-with-router/components/Header.js
Normal 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>
|
||||
)
|
16
examples/using-with-router/package.json
Normal file
16
examples/using-with-router/package.json
Normal 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"
|
||||
}
|
8
examples/using-with-router/pages/about.js
Normal file
8
examples/using-with-router/pages/about.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
import Header from '../components/Header'
|
||||
|
||||
export default () => (
|
||||
<div>
|
||||
<Header />
|
||||
<p>This is the about page.</p>
|
||||
</div>
|
||||
)
|
14
examples/using-with-router/pages/error.js
Normal file
14
examples/using-with-router/pages/error.js
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
8
examples/using-with-router/pages/index.js
Normal file
8
examples/using-with-router/pages/index.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
import Header from '../components/Header'
|
||||
|
||||
export default () => (
|
||||
<div>
|
||||
<Header />
|
||||
<p>HOME PAGE is here!</p>
|
||||
</div>
|
||||
)
|
|
@ -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 () {
|
||||
|
|
|
@ -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
27
lib/router/with-router.js
Normal 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)
|
||||
}
|
|
@ -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",
|
||||
|
|
41
readme.md
41
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 `<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)
|
||||
|
|
22
test/integration/basic/pages/nav/with-hoc.js
Normal file
22
test/integration/basic/pages/nav/with-hoc.js
Normal 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>
|
||||
)
|
|
@ -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 () => {
|
||||
|
|
20
yarn.lock
20
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:
|
||||
|
|
Loading…
Reference in a new issue