diff --git a/app.js b/app.js
new file mode 100644
index 0000000000000..0bcca7647b16e
--- /dev/null
+++ b/app.js
@@ -0,0 +1 @@
+module.exports = require('./dist/lib/app')
diff --git a/client/index.js b/client/index.js
index e29a523d3ec1b..149e222daa35f 100644
--- a/client/index.js
+++ b/client/index.js
@@ -3,7 +3,6 @@ import ReactDOM from 'react-dom'
import HeadManager from './head-manager'
import { createRouter } from '../lib/router'
import EventEmitter from '../lib/EventEmitter'
-import App from '../lib/app'
import { loadGetInitialProps, getURL } from '../lib/utils'
import PageLoader from '../lib/page-loader'
import * as asset from '../lib/asset'
@@ -69,6 +68,7 @@ export let router
export let ErrorComponent
let ErrorDebugComponent
let Component
+let App
let stripAnsi = (s) => s
export const emitter = new EventEmitter()
@@ -82,16 +82,23 @@ export default async ({ ErrorDebugComponent: passedDebugComponent, stripAnsi: pa
stripAnsi = passedStripAnsi || stripAnsi
ErrorDebugComponent = passedDebugComponent
ErrorComponent = await pageLoader.loadPage('/_error')
+ App = await pageLoader.loadPage('/_app')
try {
Component = await pageLoader.loadPage(page)
+
+ if (typeof Component !== 'function') {
+ throw new Error(`The default export is not a React Component in page: "${pathname}"`)
+ }
} catch (err) {
console.error(stripAnsi(`${err.message}\n${err.stack}`))
Component = ErrorComponent
}
router = createRouter(pathname, query, asPath, {
+ initialProps: props,
pageLoader,
+ App,
Component,
ErrorComponent,
err
@@ -136,7 +143,7 @@ export async function renderError (error) {
console.error(stripAnsi(errorMessage))
if (prod) {
- const initProps = { err: error, pathname, query, asPath }
+ const initProps = {Component: ErrorComponent, router, ctx: {err: error, pathname, query, asPath}}
const props = await loadGetInitialProps(ErrorComponent, initProps)
renderReactElement(createElement(ErrorComponent, props), errorContainer)
} else {
@@ -145,18 +152,19 @@ export async function renderError (error) {
}
async function doRender ({ Component, props, hash, err, emitter: emitterProp = emitter }) {
+ // Usual getInitialProps fetching is handled in next/router
+ // this is for when ErrorComponent gets replaced by Component by HMR
if (!props && Component &&
Component !== ErrorComponent &&
lastAppProps.Component === ErrorComponent) {
- // fetch props if ErrorComponent was replaced with a page component by HMR
const { pathname, query, asPath } = router
- props = await loadGetInitialProps(Component, { err, pathname, query, asPath })
+ props = await loadGetInitialProps(App, {Component, router, ctx: {err, pathname, query, asPath}})
}
Component = Component || lastAppProps.Component
props = props || lastAppProps.props
- const appProps = { Component, props, hash, err, router, headManager }
+ const appProps = { Component, hash, err, router, headManager, ...props }
// lastAppProps has to be set before ReactDom.render to account for ReactDom throwing an error.
lastAppProps = appProps
diff --git a/client/webpack-hot-middleware-client.js b/client/webpack-hot-middleware-client.js
index e05b01957fdd4..1fd58e57df7f0 100644
--- a/client/webpack-hot-middleware-client.js
+++ b/client/webpack-hot-middleware-client.js
@@ -27,6 +27,13 @@ export default () => {
return
}
+ // If the App component changes we have to reload the current route
+ if (route === '/_app') {
+ Router.reload(Router.route)
+ return
+ }
+
+ // Since _document is server only we need to reload the full page when it changes.
if (route === '/_document') {
window.location.reload()
return
@@ -36,6 +43,13 @@ export default () => {
},
change (route) {
+ // If the App component changes we have to reload the current route
+ if (route === '/_app') {
+ Router.reload(Router.route)
+ return
+ }
+
+ // Since _document is server only we need to reload the full page when it changes.
if (route === '/_document') {
window.location.reload()
return
diff --git a/errors/url-deprecated.md b/errors/url-deprecated.md
new file mode 100644
index 0000000000000..5c9c2a73f7f9d
--- /dev/null
+++ b/errors/url-deprecated.md
@@ -0,0 +1,23 @@
+# Url is deprecated
+
+#### Why This Error Occurred
+
+In version prior to 6.x `url` got magically injected into every page component, since this is confusing and can now be added by the user using a custom `_app.js` we have deprecated this feature. To be removed in Next.js 7.0
+
+#### Possible Ways to Fix It
+
+The easiest way to get the same values that `url` had is to use `withRouter`:
+
+```js
+import { withRouter } from 'next/router'
+
+class Page extends React.Component {
+ render() {
+ const {router} = this.props
+ console.log(router)
+ return
{router.pathname}
+ }
+}
+
+export default withRouter(Page)
+```
diff --git a/lib/app.js b/lib/app.js
index bae48f0b97679..c749f9ef30812 100644
--- a/lib/app.js
+++ b/lib/app.js
@@ -1,7 +1,7 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import shallowEquals from './shallow-equals'
-import { warn } from './utils'
+import { execOnce, warn, loadGetInitialProps } from './utils'
import { makePublicRouterInstance } from './router'
export default class App extends Component {
@@ -9,16 +9,26 @@ export default class App extends Component {
hasError: null
}
+ static displayName = 'App'
+
+ static async getInitialProps ({ Component, router, ctx }) {
+ const pageProps = await loadGetInitialProps(Component, ctx)
+ return {pageProps}
+ }
+
static childContextTypes = {
+ _containerProps: PropTypes.any,
headManager: PropTypes.object,
router: PropTypes.object
}
getChildContext () {
const { headManager } = this.props
+ const {hasError} = this.state
return {
headManager,
- router: makePublicRouterInstance(this.props.router)
+ router: makePublicRouterInstance(this.props.router),
+ _containerProps: {...this.props, hasError}
}
}
@@ -29,22 +39,19 @@ export default class App extends Component {
}
render () {
- if (this.state.hasError) return null
-
- const { Component, props, hash, router } = this.props
+ const {router, Component, pageProps} = this.props
const url = createUrl(router)
- // If there no component exported we can't proceed.
- // We'll tackle that here.
- if (typeof Component !== 'function') {
- throw new Error(`The default export is not a React Component in page: "${url.pathname}"`)
- }
- const containerProps = { Component, props, hash, router, url }
-
- return
+ return
+
+
}
}
-class Container extends Component {
+export class Container extends Component {
+ static contextTypes = {
+ _containerProps: PropTypes.any
+ }
+
componentDidMount () {
this.scrollToHash()
}
@@ -71,10 +78,16 @@ class Container extends Component {
}
render () {
- const { Component, props, url } = this.props
+ const { hasError } = this.context._containerProps
+
+ if (hasError) {
+ return null
+ }
+
+ const {children} = this.props
if (process.env.NODE_ENV === 'production') {
- return ()
+ return <>{children}>
} else {
const ErrorDebug = require('./error-debug').default
const { AppContainer } = require('react-hot-loader')
@@ -83,39 +96,50 @@ class Container extends Component {
// https://github.com/gaearon/react-hot-loader/issues/442
return (
-
+ {children}
)
}
}
}
-function createUrl (router) {
+const warnUrl = execOnce(() => warn(`Warning: the 'url' property is deprecated. https://err.sh/next.js/url-deprecated`))
+
+export function createUrl (router) {
return {
- query: router.query,
- pathname: router.pathname,
- asPath: router.asPath,
+ get query () {
+ warnUrl()
+ return router.query
+ },
+ get pathname () {
+ warnUrl()
+ return router.pathname
+ },
+ get asPath () {
+ warnUrl()
+ return router.asPath
+ },
back: () => {
- warn(`Warning: 'url.back()' is deprecated. Use "window.history.back()"`)
+ warnUrl()
router.back()
},
push: (url, as) => {
- warn(`Warning: 'url.push()' is deprecated. Use "next/router" APIs.`)
+ warnUrl()
return router.push(url, as)
},
pushTo: (href, as) => {
- warn(`Warning: 'url.pushTo()' is deprecated. Use "next/router" APIs.`)
+ warnUrl()
const pushRoute = as ? href : null
const pushUrl = as || href
return router.push(pushRoute, pushUrl)
},
replace: (url, as) => {
- warn(`Warning: 'url.replace()' is deprecated. Use "next/router" APIs.`)
+ warnUrl()
return router.replace(url, as)
},
replaceTo: (href, as) => {
- warn(`Warning: 'url.replaceTo()' is deprecated. Use "next/router" APIs.`)
+ warnUrl()
const replaceRoute = as ? href : null
const replaceUrl = as || href
diff --git a/lib/router/router.js b/lib/router/router.js
index ce3415c270699..5193be435b81e 100644
--- a/lib/router/router.js
+++ b/lib/router/router.js
@@ -15,7 +15,7 @@ const historyMethodWarning = execOnce((method) => {
})
export default class Router {
- constructor (pathname, query, as, { pageLoader, Component, ErrorComponent, err } = {}) {
+ constructor (pathname, query, as, { initialProps, pageLoader, App, Component, ErrorComponent, err } = {}) {
// represents the current component key
this.route = toRoute(pathname)
@@ -25,7 +25,7 @@ export default class Router {
// Otherwise, this cause issues when when going back and
// come again to the errored page.
if (Component !== ErrorComponent) {
- this.components[this.route] = { Component, err }
+ this.components[this.route] = { Component, props: initialProps, err }
}
// Handling Router Events
@@ -33,6 +33,7 @@ export default class Router {
this.pageLoader = pageLoader
this.prefetchQueue = new PQueue({ concurrency: 2 })
+ this.App = App
this.ErrorComponent = ErrorComponent
this.pathname = pathname
this.query = query
@@ -350,7 +351,7 @@ export default class Router {
const cancel = () => { cancelled = true }
this.componentLoadCancel = cancel
- const props = await loadGetInitialProps(Component, ctx)
+ const props = await loadGetInitialProps(this.App, {Component, router: this, ctx})
if (cancel === this.componentLoadCancel) {
this.componentLoadCancel = null
diff --git a/package.json b/package.json
index d372fad240206..74277b985d109 100644
--- a/package.json
+++ b/package.json
@@ -21,7 +21,8 @@
"asset.js",
"error.js",
"constants.js",
- "config.js"
+ "config.js",
+ "app.js"
],
"bin": {
"next": "./dist/bin/next"
diff --git a/pages/_app.js b/pages/_app.js
new file mode 100644
index 0000000000000..c96c4b94f9a62
--- /dev/null
+++ b/pages/_app.js
@@ -0,0 +1 @@
+module.exports = require('next/app')
diff --git a/readme.md b/readme.md
index 0505d64e50f69..34d1edeadc907 100644
--- a/readme.md
+++ b/readme.md
@@ -22,12 +22,20 @@ Next.js is a minimalistic framework for server-rendered React applications.
- [CSS](#css)
- [Built-in CSS support](#built-in-css-support)
- [CSS-in-JS](#css-in-js)
+ - [Importing CSS / Sass / Less / Stylus files](#importing-css--sass--less--stylus-files)
- [Static file serving (e.g.: images)](#static-file-serving-eg-images)
- [Populating ``](#populating-head)
- [Fetching data and component lifecycle](#fetching-data-and-component-lifecycle)
- [Routing](#routing)
- [With ``](#with-link)
+ - [With URL object](#with-url-object)
+ - [Replace instead of push url](#replace-instead-of-push-url)
+ - [Using a component that supports `onClick`](#using-a-component-that-supports-onclick)
+ - [Forcing the Link to expose `href` to its child](#forcing-the-link-to-expose-href-to-its-child)
+ - [Disabling the scroll changes to top on page](#disabling-the-scroll-changes-to-top-on-page)
- [Imperatively](#imperatively)
+ - [Intercepting `popstate`](#intercepting-popstate)
+ - [With URL object](#with-url-object-1)
- [Router Events](#router-events)
- [Shallow Routing](#shallow-routing)
- [Using a Higher Order Component](#using-a-higher-order-component)
@@ -35,16 +43,34 @@ Next.js is a minimalistic framework for server-rendered React applications.
- [With ``](#with-link-1)
- [Imperatively](#imperatively-1)
- [Custom server and routing](#custom-server-and-routing)
+ - [Disabling file-system routing](#disabling-file-system-routing)
+ - [Dynamic assetPrefix](#dynamic-assetprefix)
- [Dynamic Import](#dynamic-import)
+ - [1. Basic Usage (Also does SSR)](#1-basic-usage-also-does-ssr)
+ - [2. With Custom Loading Component](#2-with-custom-loading-component)
+ - [3. With No SSR](#3-with-no-ssr)
+ - [4. With Multiple Modules At Once](#4-with-multiple-modules-at-once)
+ - [Custom ``](#custom-app)
- [Custom ``](#custom-document)
- [Custom error handling](#custom-error-handling)
+ - [Reusing the built-in error page](#reusing-the-built-in-error-page)
- [Custom configuration](#custom-configuration)
+ - [Setting a custom build directory](#setting-a-custom-build-directory)
+ - [Disabling etag generation](#disabling-etag-generation)
+ - [Configuring the onDemandEntries](#configuring-the-ondemandentries)
+ - [Configuring extensions looked for when resolving pages in `pages`](#configuring-extensions-looked-for-when-resolving-pages-in-pages)
+ - [Configuring the build ID](#configuring-the-build-id)
- [Customizing webpack config](#customizing-webpack-config)
- [Customizing babel config](#customizing-babel-config)
+ - [Exposing configuration to the server / client side](#exposing-configuration-to-the-server--client-side)
- [CDN support with Asset Prefix](#cdn-support-with-asset-prefix)
- [Production deployment](#production-deployment)
- [Static HTML export](#static-html-export)
+ - [Usage](#usage)
+ - [Limitation](#limitation)
- [Multi Zones](#multi-zones)
+ - [How to define a zone](#how-to-define-a-zone)
+ - [How to merge them](#how-to-merge-them)
- [Recipes](#recipes)
- [FAQ](#faq)
- [Contributing](#contributing)
@@ -923,6 +949,77 @@ const HelloBundle = dynamic({
export default () =>
```
+### Custom ``
+
+
+- Is rendered on the server side
+- Is used to change the initial server side rendered document markup
+- Commonly used to implement server side rendering for css-in-js libraries like [styled-components](./examples/with-styled-components), [glamorous](./examples/with-glamorous) or [emotion](with-emotion). [styled-jsx](https://github.com/zeit/styled-jsx) is included with Next.js by default.
+
Pages in `Next.js` skip the definition of the surrounding document's markup. For example, you never include ``, ``, etc. To override that default behavior, you must create a file at `./pages/_document.js`, where you can extend the `Document` class:
```jsx
@@ -939,13 +1040,11 @@ Pages in `Next.js` skip the definition of the surrounding document's markup. For
// ./pages/_document.js
import Document, { Head, Main, NextScript } from 'next/document'
-import flush from 'styled-jsx/server'
export default class MyDocument extends Document {
- static getInitialProps({ renderPage }) {
- const { html, head, errorHtml, chunks, buildManifest } = renderPage()
- const styles = flush()
- return { html, head, errorHtml, chunks, styles, buildManifest }
+ static async getInitialProps(ctx) {
+ const initialProps = await Document.getInitialProps(ctx)
+ return { ...initialProps }
}
render() {
@@ -955,7 +1054,6 @@ export default class MyDocument extends Document {
- {this.props.customValue}
@@ -969,7 +1067,7 @@ The `ctx` object is equivalent to the one received in all [`getInitialProps`](#f
- `renderPage` (`Function`) a callback that executes the actual React rendering logic (synchronously). It's useful to decorate this function in order to support server-rendering wrappers like Aphrodite's [`renderStatic`](https://github.com/Khan/aphrodite#server-side-rendering)
-__Note: React-components outside of `` will not be initialised by the browser. If you need shared components in all your pages (like a menu or a toolbar), do _not_ add application logic here, but take a look at [this example](https://github.com/zeit/next.js/tree/master/examples/layout-component).__
+__Note: React-components outside of `` will not be initialised by the browser. Do _not_ add application logic here. If you need shared components in all your pages (like a menu or a toolbar), take a look at the `App` component instead.__
### Custom error handling
diff --git a/server/build/webpack.js b/server/build/webpack.js
index 344ee32c99bf0..55fccd029e9c3 100644
--- a/server/build/webpack.js
+++ b/server/build/webpack.js
@@ -22,7 +22,8 @@ const nextNodeModulesDir = path.join(nextDir, 'node_modules')
const nextPagesDir = path.join(nextDir, 'pages')
const defaultPages = [
'_error.js',
- '_document.js'
+ '_document.js',
+ '_app.js'
]
const interpolateNames = new Map(defaultPages.map((p) => {
return [path.join(nextPagesDir, p), `dist/bundles/pages/${p}`]
@@ -71,11 +72,12 @@ function externalsConfig (dir, isServer) {
return callback()
}
- // Webpack itself has to be compiled because it doesn't always use module relative paths
+ // Default pages have to be transpiled
if (res.match(/node_modules[/\\]next[/\\]dist[/\\]pages/)) {
return callback()
}
+ // Webpack itself has to be compiled because it doesn't always use module relative paths
if (res.match(/node_modules[/\\]webpack/)) {
return callback()
}
diff --git a/server/build/webpack/utils.js b/server/build/webpack/utils.js
index 876eac8b1c4d7..960226543feea 100644
--- a/server/build/webpack/utils.js
+++ b/server/build/webpack/utils.js
@@ -16,8 +16,9 @@ export async function getPagePaths (dir, {dev, isServer, pageExtensions}) {
let pages
if (dev) {
- // In development we only compile _document.js and _error.js when starting, since they're always needed. All other pages are compiled with on demand entries
- pages = await glob(isServer ? `pages/+(_document|_error).+(${pageExtensions})` : `pages/_error.+(${pageExtensions})`, { cwd: dir })
+ // In development we only compile _document.js, _error.js and _app.js when starting, since they're always needed. All other pages are compiled with on demand entries
+ // _document also has to be in the client compiler in development because we want to detect HMR changes and reload the client
+ pages = await glob(`pages/+(_document|_app|_error).+(${pageExtensions})`, { cwd: dir })
} else {
// In production get all pages from the pages directory
pages = await glob(isServer ? `pages/**/*.+(${pageExtensions})` : `pages/**/!(_document)*.+(${pageExtensions})`, { cwd: dir })
@@ -57,6 +58,12 @@ export function getPageEntries (pagePaths, {isServer = false, pageExtensions} =
entries[entry.name] = entry.files
}
+ const appPagePath = path.join(nextPagesDir, '_app.js')
+ const appPageEntry = createEntry(appPagePath, {name: 'pages/_app.js'}) // default app.js
+ if (!entries[appPageEntry.name]) {
+ entries[appPageEntry.name] = appPageEntry.files
+ }
+
const errorPagePath = path.join(nextPagesDir, '_error.js')
const errorPageEntry = createEntry(errorPagePath, {name: 'pages/_error.js'}) // default error.js
if (!entries[errorPageEntry.name]) {
diff --git a/server/document.js b/server/document.js
index 48fcdbc5fbaaa..92f6a90636450 100644
--- a/server/document.js
+++ b/server/document.js
@@ -91,6 +91,7 @@ export class Head extends Component {
return
{(head || []).map((h, i) => React.cloneElement(h, { key: h.key || i }))}
{page !== '/_error' && }
+
{this.getPreloadDynamicChunks()}
{this.getPreloadMainLinks()}
@@ -204,6 +205,7 @@ export class NextScript extends Component {
`
}} />}
{page !== '/_error' && }
+
{staticMarkup ? null : this.getDynamicChunks()}
{staticMarkup ? null : this.getScripts()}
diff --git a/server/index.js b/server/index.js
index 1d993c0084825..05f1f2b28c6b9 100644
--- a/server/index.js
+++ b/server/index.js
@@ -28,6 +28,7 @@ const access = promisify(fs.access)
const blockedPages = {
'/_document': true,
+ '/_app': true,
'/_error': true
}
@@ -178,20 +179,6 @@ export default class Server {
await serveStatic(req, res, path)
},
- // This is very similar to the following route.
- // But for this one, the page already built when the Next.js process starts.
- // There's no need to build it in on-demand manner and check for other things.
- // So, it's clean to have a seperate route for this.
- '/_next/:buildId/page/_error.js': async (req, res, params) => {
- if (!this.handleBuildId(params.buildId, res)) {
- const error = new Error('INVALID_BUILD_ID')
- return await renderScriptError(req, res, '/_error', error)
- }
-
- const p = join(this.dir, `${this.dist}/bundles/pages/_error.js`)
- await this.serveStatic(req, res, p)
- },
-
'/_next/:buildId/page/:path*.js': async (req, res, params) => {
const paths = params.path || ['']
const page = `/${paths.join('/')}`
@@ -201,7 +188,7 @@ export default class Server {
return await renderScriptError(req, res, page, error)
}
- if (this.dev) {
+ if (this.dev && page !== '/_error' && page !== '/_app') {
try {
await this.hotReloader.ensurePage(page)
} catch (error) {
diff --git a/server/render.js b/server/render.js
index aa98a5f858a70..4da7bd03b570e 100644
--- a/server/render.js
+++ b/server/render.js
@@ -9,7 +9,6 @@ import { Router } from '../lib/router'
import { loadGetInitialProps, isResSent } from '../lib/utils'
import { getAvailableChunks } from './utils'
import Head, { defaultHead } from '../lib/head'
-import App from '../lib/app'
import ErrorDebug from '../lib/error-debug'
import { flushChunks } from '../lib/dynamic'
import { BUILD_MANIFEST } from '../lib/constants'
@@ -55,17 +54,26 @@ async function doRender (req, res, pathname, query, {
}
const documentPath = join(dir, dist, 'dist', 'bundles', 'pages', '_document')
+ const appPath = join(dir, dist, 'dist', 'bundles', 'pages', '_app')
const buildManifest = require(join(dir, dist, BUILD_MANIFEST))
-
- let [Component, Document] = await Promise.all([
+ let [Component, Document, App] = await Promise.all([
requirePage(page, {dir, dist}),
- require(documentPath)
+ require(documentPath),
+ require(appPath)
])
+
Component = Component.default || Component
+
+ if (typeof Component !== 'function') {
+ throw new Error(`The default export is not a React Component in page: "${pathname}"`)
+ }
+
+ App = App.default || App
Document = Document.default || Document
const asPath = req.url
const ctx = { err, req, res, pathname, query, asPath }
- const props = await loadGetInitialProps(Component, ctx)
+ const router = new Router(pathname, query, asPath)
+ const props = await loadGetInitialProps(App, {Component, router, ctx})
// the response might be finshed on the getinitialprops call
if (isResSent(res)) return
@@ -73,8 +81,8 @@ async function doRender (req, res, pathname, query, {
const renderPage = (enhancer = Page => Page) => {
const app = createElement(App, {
Component: enhancer(Component),
- props,
- router: new Router(pathname, query, asPath)
+ router,
+ ...props
})
const render = staticMarkup ? renderToStaticMarkup : renderToString
diff --git a/test/integration/app-document/pages/_app.js b/test/integration/app-document/pages/_app.js
new file mode 100644
index 0000000000000..ed72d04ea36d7
--- /dev/null
+++ b/test/integration/app-document/pages/_app.js
@@ -0,0 +1,44 @@
+import App, {Container} from 'next/app'
+import React from 'react'
+
+class Layout extends React.Component {
+ state = {
+ random: false
+ }
+
+ componentDidMount () {
+ this.setState({random: Math.random()})
+ }
+
+ render () {
+ const {children} = this.props
+ const {random} = this.state
+ return