Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expose app.js #4129

Merged
merged 16 commits into from
Apr 12, 2018
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('./dist/lib/app')
18 changes: 13 additions & 5 deletions client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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()
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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

Expand Down
14 changes: 14 additions & 0 deletions client/webpack-hot-middleware-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
74 changes: 48 additions & 26 deletions lib/app.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,34 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import shallowEquals from './shallow-equals'
import { warn } from './utils'
import { warn, loadGetInitialProps } from './utils'
import { makePublicRouterInstance } from './router'

export default class App extends Component {
state = {
hasError: null
}

static displayName = 'App'

static async getInitialProps ({ Component, router, ctx }) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If user didn't override getInitialProps in his/her sub class, is this going to execute?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep 👍 (Basically the same as _document.js that also implements getInitialProps

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

Expand All @@ -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 <Container {...containerProps} />
return <Container>
<Component url={url} {...pageProps} />
</Container>
}
}

class Container extends Component {
export class Container extends Component {
static contextTypes = {
_containerProps: PropTypes.any
}

componentDidMount () {
this.scrollToHash()
}
Expand All @@ -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 (<Component {...props} url={url} />)
return <>{children}</>
} else {
const ErrorDebug = require('./error-debug').default
const { AppContainer } = require('react-hot-loader')
Expand All @@ -83,39 +96,48 @@ class Container extends Component {
// https://github.com/gaearon/react-hot-loader/issues/442
return (
<AppContainer warnings={false} errorReporter={ErrorDebug}>
<Component {...props} url={url} />
{children}
</AppContainer>
)
}
}
}

function createUrl (router) {
export function createUrl (router) {
return {
query: router.query,
pathname: router.pathname,
asPath: router.asPath,
get query () {
warn(`Warning: 'url.query' is deprecated. https://err.sh/next.js/url-deprecated`)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shall we make sure we only print this once?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done 👍

return router.query
},
get pathname () {
warn(`Warning: 'url.pathname' is deprecated. https://err.sh/next.js/url-deprecated`)
return router.pathname
},
get asPath () {
warn(`Warning: 'url.asPath' is deprecated. https://err.sh/next.js/url-deprecated`)
return router.asPath
},
back: () => {
warn(`Warning: 'url.back()' is deprecated. Use "window.history.back()"`)
warn(`Warning: 'url.back()' is deprecated. Use "window.history.back()" https://err.sh/next.js/url-deprecated`)
Copy link
Contributor

@arunoda arunoda Apr 10, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we use something like warnOnce or something. And for the rest of the places.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this was deprecated in v2 or v3. That's what I didn't want to touch it too much, just adding the url to the message. In theory we could use the same message though. Will update it.

router.back()
},
push: (url, as) => {
warn(`Warning: 'url.push()' is deprecated. Use "next/router" APIs.`)
warn(`Warning: 'url.push()' is deprecated. Use "next/router" APIs. https://err.sh/next.js/url-deprecated`)
return router.push(url, as)
},
pushTo: (href, as) => {
warn(`Warning: 'url.pushTo()' is deprecated. Use "next/router" APIs.`)
warn(`Warning: 'url.pushTo()' is deprecated. Use "next/router" APIs. https://err.sh/next.js/url-deprecated`)
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.`)
warn(`Warning: 'url.replace()' is deprecated. Use "next/router" APIs. https://err.sh/next.js/url-deprecated`)
return router.replace(url, as)
},
replaceTo: (href, as) => {
warn(`Warning: 'url.replaceTo()' is deprecated. Use "next/router" APIs.`)
warn(`Warning: 'url.replaceTo()' is deprecated. Use "next/router" APIs. https://err.sh/next.js/url-deprecated`)
const replaceRoute = as ? href : null
const replaceUrl = as || href

Expand Down
7 changes: 4 additions & 3 deletions lib/router/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -25,14 +25,15 @@ 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
this.events = new EventEmitter()

this.pageLoader = pageLoader
this.prefetchQueue = new PQueue({ concurrency: 2 })
this.App = App
this.ErrorComponent = ErrorComponent
this.pathname = pathname
this.query = query
Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
"asset.js",
"error.js",
"constants.js",
"config.js"
"config.js",
"app.js"
],
"bin": {
"next": "./dist/bin/next"
Expand Down
1 change: 1 addition & 0 deletions pages/_app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('next/app')
47 changes: 46 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -923,6 +923,47 @@ const HelloBundle = dynamic({
export default () => <HelloBundle title="Dynamic Bundle" />
```

### Custom `<App>`

<p><details>
<summary><b>Examples</b></summary>
<ul><li><a href="./examples/layout-component">Using `_app.js` for layout</a></li></ul>
<ul><li><a href="./examples/componentdidcatch">Using `_app.js` to override `componentDidCatch`</a></li></ul>
</details></p>

- Is rendered on both client and server side
- `this.state` should not be overwritten. Instead create a new component to handle the state ([layout example](./examples/layout-component))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not clear. What are we going to say.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a clearer example.

- Handles calling `getInitialProps` of the **page** `Component`
- Implements `componentDidCatch` on the client side, and renders the error page accordingly. ([overriding `componentDidCatch` example](./examples/componentdidcatch))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's try not say what've inside app.js. But what the user can do with this. Here's a sample:

Next.js has a App layout which initialize pages in your app. You can override it with pages/_app.js and implement:

  • Layouts works across pages
  • Animations in between pages
  • Keep the DOM state when navigating page
  • Provide additional properties to initial props of pages
  • Custom error messages by catching errors
    ...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated

- The `Container` implements React Hot Loader when in development mode.
- The `Container` implements scrolling to a hash when using `<Link href="/about#example-hash">`

To override the default `_app.js` behaviour you can create a file `./pages/_app.js`, where you can extend the `App` class:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To override, create a file in ./pages/_app.js 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 <Container>
<Component {...pageProps} />
</Container>
}
}
```

### Custom `<Document>`

<p><details>
Expand All @@ -931,6 +972,10 @@ export default () => <HelloBundle title="Dynamic Bundle" />
<ul><li><a href="./examples/with-amp">Google AMP</a></li></ul>
</details></p>

- 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 `<html>`, `<body>`, etc. To override that default behavior, you must create a file at `./pages/_document.js`, where you can extend the `Document` class:

```jsx
Expand Down Expand Up @@ -969,7 +1014,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 `<Main />` 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 `<Main />` 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

Expand Down
6 changes: 4 additions & 2 deletions server/build/webpack.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,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}`]
Expand Down Expand Up @@ -70,11 +71,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()
}
Expand Down
Loading