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 `` + +

+ Examples + + +

+ +Next.js uses the `App` component to initialize pages. You can override it and control the page initialization. Which allows you can do amazing things like: + +- Persisting layout between page changes +- Keeping state when navigating pages +- Custom error handling using `componentDidCatch` +- Inject additional data into pages (for example by processing GraphQL queries) + +To override, create the `./pages/_app.js` file and override the App class as shown below: + +```js +import App, {Container} from 'next/app' +import React from 'react' + +export default class MyApp extends App { + static async getInitialProps ({ Component, router, ctx }) { + let pageProps = {} + + if (Component.getInitialProps) { + pageProps = await Component.getInitialProps(ctx) + } + + return {pageProps} + } + + render () { + const {Component, pageProps} = this.props + return + + + } +} +``` + +When using state inside app the `hasError` property has to be defined: + +```js +import App, {Container} from 'next/app' +import React from 'react' + +export default class MyApp extends App { + static async getInitialProps ({ Component, router, ctx }) { + let pageProps = {} + + if (Component.getInitialProps) { + pageProps = await Component.getInitialProps(ctx) + } + + return {pageProps} + } + + state = { + hasError: null + } + + render () { + const {Component, pageProps} = this.props + return + + + } +} +``` + ### Custom ``

@@ -931,6 +1028,10 @@ export default () =>

+- 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' &&