diff --git a/README.md b/README.md index cadddb53..290caa14 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ and add a script to your package.json like this: { "scripts": { "dev": "next" - } + } } ``` @@ -142,9 +142,9 @@ For the initial page load, `getInitialProps` will execute on the server only. `g - `xhr` - XMLHttpRequest object (client only) - `err` - Error object if any error is encountered during the rendering -### Routing +### Routing with -Client-side transitions between routes are enabled via a `` component +Client-side transitions between routes can be enabled via a `` component #### pages/index.js @@ -178,11 +178,54 @@ Each top-level component receives a `url` property with the following API: - `pushTo(url)` - performs a `pushState` call that renders the new `url`. This is equivalent to following a `` - `replaceTo(url)` - performs a `replaceState` call that renders the new `url` +### Routing with next/router + +You can also do client-side page transitions using the `next/router`. This is the same API used inside the above `` component. + +```jsx +import Router from 'next/router' + +const routeTo(href) { + return (e) => { + e.preventDefault() + Router.push(href) + } +} + +export default () => ( +
Click here to read more
+) +``` + +#### pages/about.js + +```jsx +export default () => ( +

Welcome to About!

+) +``` + +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 `{}` +- `push(url, pathname=url)` - performs a `pushState` call associated with the current component +- `replace(url, pathname=url)` - performs a `replaceState` call associated with the current component + +> Usually, route is the same as pathname. +> But when used with programmatic API, route and pathname can be different. +> "route" is your actual page's path while "pathname" is the path of the url mapped to it. +> +> Likewise, url and path is the same usually. +> But when used with programmatic API, "url" is the route with the query string. +> "pathname" is the path of the url mapped to it. + ### Prefetching Pages -Next.js exposes a module that configures a `ServiceWorker` automatically to prefetch pages: `next/prefetch`. +Next.js exposes a module that configures a `ServiceWorker` automatically to prefetch pages: `next/prefetch`. -Since Next.js server-renders your pages, this allows all the future interaction paths of your app to be instant. Effectively Next.js gives you the great initial download performance of a _website_, with the ahead-of-time download capabilities of an _app_. [Read more](https://zeit.co/blog/next#anticipation-is-the-key-to-performance). +Since Next.js server-renders your pages, this allows all the future interaction paths of your app to be instant. Effectively Next.js gives you the great initial download performance of a _website_, with the ahead-of-time download capabilities of an _app_. [Read more](https://zeit.co/blog/next#anticipation-is-the-key-to-performance). #### Link prefetching @@ -251,7 +294,7 @@ export default class Error extends React.Component { ### Custom configuration -For custom advanced behavior of Next.js, you can create a `next.config.js` in the root of your project directory (next to `pages/` and `package.json`). +For custom advanced behavior of Next.js, you can create a `next.config.js` in the root of your project directory (next to `pages/` and `package.json`). Note: `next.config.js` is a regular Node.js module, not a JSON file. It gets used by the Next server and build phases, and not included in the browser build. @@ -264,7 +307,7 @@ module.exports = { ### Customizing webpack config -In order to extend our usage of `webpack`, you can define a function that extends its config. +In order to extend our usage of `webpack`, you can define a function that extends its config. The following example shows how you can use [`react-svg-loader`](https://github.com/boopathi/react-svg-loader) to easily import any `.svg` file as a React component, without modification. diff --git a/client/next.js b/client/next.js index 0c2b7604..c44e0c29 100644 --- a/client/next.js +++ b/client/next.js @@ -1,9 +1,8 @@ import { createElement } from 'react' import { render } from 'react-dom' import HeadManager from './head-manager' -import domready from 'domready' import { rehydrate } from '../lib/css' -import Router from '../lib/router' +import { createRouter } from '../lib/router' import App from '../lib/app' import evalScript from '../lib/eval-script' @@ -19,25 +18,18 @@ const { } } = window -domready(() => { - const Component = evalScript(component).default - const ErrorComponent = evalScript(errorComponent).default +const Component = evalScript(component).default +const ErrorComponent = evalScript(errorComponent).default - const router = new Router(pathname, query, { - Component, - ErrorComponent, - ctx: { err } - }) - - // This it to support error handling in the dev time with hot code reload. - if (window.next) { - window.next.router = router - } - - const headManager = new HeadManager() - const container = document.getElementById('__next') - const appProps = { Component, props, router, headManager } - - if (ids) rehydrate(ids) - render(createElement(App, appProps), container) +export const router = createRouter(pathname, query, { + Component, + ErrorComponent, + ctx: { err } }) + +const headManager = new HeadManager() +const container = document.getElementById('__next') +const appProps = { Component, props, router, headManager } + +if (ids) rehydrate(ids) +render(createElement(App, appProps), container) diff --git a/client/webpack-hot-middleware-client.js b/client/webpack-hot-middleware-client.js index 079089e3..8e463305 100644 --- a/client/webpack-hot-middleware-client.js +++ b/client/webpack-hot-middleware-client.js @@ -1,27 +1,27 @@ -/* global next */ import webpackHotMiddlewareClient from 'webpack-hot-middleware/client?overlay=false&reload=true' +import Router from '../lib/router' const handlers = { reload (route) { if (route === '/_error') { - for (const r of Object.keys(next.router.components)) { - const { Component } = next.router.components[r] + for (const r of Object.keys(Router.components)) { + const { Component } = Router.components[r] if (Component.__route === '/_error-debug') { // reload all '/_error-debug' // which are expected to be errors of '/_error' routes - next.router.reload(r) + Router.reload(r) } } return } - next.router.reload(route) + Router.reload(route) }, change (route) { - const { Component } = next.router.components[route] || {} + const { Component } = Router.components[route] || {} if (Component && Component.__route === '/_error-debug') { // reload to recover from runtime errors - next.router.reload(route) + Router.reload(route) } }, hardReload () { diff --git a/examples/using-router/README.md b/examples/using-router/README.md new file mode 100644 index 00000000..0a0c2733 --- /dev/null +++ b/examples/using-router/README.md @@ -0,0 +1,13 @@ +# Example app utilizing next/router for routing + +This example features: + +* An app linking pages using `next/router` instead of `` component. +* Access the pathname using `next/router` and render it in a component + +## How to run it + +```sh +npm install +npm run dev +``` diff --git a/examples/using-router/components/Header.js b/examples/using-router/components/Header.js new file mode 100644 index 00000000..a07ab14b --- /dev/null +++ b/examples/using-router/components/Header.js @@ -0,0 +1,31 @@ +import React from 'react' +import Router from 'next/router' + +const styles = { + a: { + marginRight: 10 + } +} + +const Link = ({ children, href }) => ( + { + e.preventDefault() + Router.push(href) + }} + > + { children } + +) + +export default () => ( +
+ Home + About +
+ Now you are in the route: {Router.route} +
+
+) diff --git a/examples/using-router/package.json b/examples/using-router/package.json new file mode 100644 index 00000000..8d7c733d --- /dev/null +++ b/examples/using-router/package.json @@ -0,0 +1,16 @@ +{ + "name": "shared-modules", + "version": "1.0.0", + "description": "This example features:", + "main": "index.js", + "scripts": { + "dev": "next", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "next": "*" + }, + "author": "", + "license": "ISC" +} diff --git a/examples/using-router/pages/about.js b/examples/using-router/pages/about.js new file mode 100644 index 00000000..2bf1cea4 --- /dev/null +++ b/examples/using-router/pages/about.js @@ -0,0 +1,9 @@ +import React from 'react' +import Header from '../components/Header' + +export default () => ( +
+
+

This is the about page.

+
+) diff --git a/examples/using-router/pages/index.js b/examples/using-router/pages/index.js new file mode 100644 index 00000000..994c3c7f --- /dev/null +++ b/examples/using-router/pages/index.js @@ -0,0 +1,9 @@ +import React from 'react' +import Header from '../components/Header' + +export default () => ( +
+
+

HOME PAGE is here!

+
+) diff --git a/lib/link.js b/lib/link.js index cb9f5134..fffed123 100644 --- a/lib/link.js +++ b/lib/link.js @@ -1,10 +1,7 @@ -import React, { Component, PropTypes, Children } from 'react' +import React, { Component, Children } from 'react' +import Router from './router' export default class Link extends Component { - static contextTypes = { - router: PropTypes.object - } - constructor (props) { super(props) this.linkClicked = this.linkClicked.bind(this) @@ -30,14 +27,14 @@ export default class Link extends Component { const url = as || href // straight up redirect - this.context.router.push(route, url) - .then((success) => { - if (!success) return - if (scroll !== false) window.scrollTo(0, 0) - }) - .catch((err) => { - if (this.props.onError) this.props.onError(err) - }) + Router.push(route, url) + .then((success) => { + if (!success) return + if (scroll !== false) window.scrollTo(0, 0) + }) + .catch((err) => { + if (this.props.onError) this.props.onError(err) + }) } render () { diff --git a/lib/router/index.js b/lib/router/index.js new file mode 100644 index 00000000..3cfcea30 --- /dev/null +++ b/lib/router/index.js @@ -0,0 +1,51 @@ +import _Router from './router' + +// holds the actual router instance +let router = null + +const SingletonRouter = {} + +// Create public properties and methods of the router in the SingletonRouter +const propertyFields = ['route', 'components', 'pathname', 'query'] +const methodFields = ['push', 'replace', 'reload', 'back'] + +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(SingletonRouter, field, { + get () { + return router[field] + } + }) +}) + +methodFields.forEach((field) => { + SingletonRouter[field] = (...args) => { + return router[field](...args) + } +}) + +// This is an internal method and it should not be called directly. +// +// ## Client Side Usage +// We create the router in the client side only for a single time when we are +// booting the app. It happens before rendering any components. +// At the time of the component rendering, there'll be a router instance +// +// ## Server Side Usage +// We create router for every SSR page render. +// Since rendering happens in the same eventloop this works properly. +export const createRouter = function (...args) { + router = new _Router(...args) + return router +} + +// Export the actual Router class, which is also use internally +// You'll ever need to access this directly +export const Router = _Router + +// Export the SingletonRouter and this is the public API. +// This is an client side API and doesn't available on the server +export default SingletonRouter diff --git a/lib/router.js b/lib/router/router.js similarity index 97% rename from lib/router.js rename to lib/router/router.js index 70cea91c..fe9f09a5 100644 --- a/lib/router.js +++ b/lib/router/router.js @@ -1,6 +1,6 @@ import { parse } from 'url' -import evalScript from './eval-script' -import shallowEquals from './shallow-equals' +import evalScript from '../eval-script' +import shallowEquals from '../shallow-equals' export default class Router { constructor (pathname, query, { Component, ErrorComponent, ctx } = {}) { @@ -97,11 +97,11 @@ export default class Router { window.history.back() } - push (route, url) { + push (route, url = route) { return this.change('pushState', route, url) } - replace (route, url) { + replace (route, url = route) { return this.change('replaceState', route, url) } diff --git a/package.json b/package.json index 35d5a969..42722d17 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,6 @@ "chokidar": "1.6.1", "cross-spawn": "5.0.1", "del": "2.2.2", - "domready": "1.0.8", "friendly-errors-webpack-plugin": "1.1.2", "glamor": "2.20.12", "glob-promise": "3.1.0", diff --git a/router.js b/router.js new file mode 100644 index 00000000..b8c9ff94 --- /dev/null +++ b/router.js @@ -0,0 +1 @@ +module.exports = require('./dist/lib/router') diff --git a/server/render.js b/server/render.js index 8f99394f..aad29fe6 100644 --- a/server/render.js +++ b/server/render.js @@ -3,7 +3,7 @@ import { createElement } from 'react' import { renderToString, renderToStaticMarkup } from 'react-dom/server' import requireModule from './require' import read from './read' -import Router from '../lib/router' +import { createRouter } from '../lib/router' import Head, { defaultHead } from '../lib/head' import App from '../lib/app' @@ -56,10 +56,11 @@ async function doRender (req, res, pathname, query, { if (res.finished) return const renderPage = () => { + const router = createRouter(pathname, query) const app = createElement(App, { Component, props, - router: new Router(pathname, query) + router }) const html = (staticMarkup ? renderToStaticMarkup : renderToString)(app) const head = Head.rewind() || defaultHead()