Skip to content

Commit

Permalink
Expose app.js (#4129)
Browse files Browse the repository at this point in the history
* Expose pages/_app.js

* Add tests for _app and _document

* Uncomment deprecation warnings

* Add documentation for _app, improve documentation of _document

* Update docs / test for _document

* Add _document to client compiler in development

* Add missing app.js to comment

* Only warn once

* Add url-deprecated error page

* Combine tests

* Yse same message for all methods of ‘props.url’

* Update docs around _app

* Update documentation

* Quotes

* Update table of contents
  • Loading branch information
timneutkens authored Apr 12, 2018
1 parent 15dde33 commit eca8e8f
Show file tree
Hide file tree
Showing 21 changed files with 477 additions and 68 deletions.
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
23 changes: 23 additions & 0 deletions errors/url-deprecated.md
Original file line number Diff line number Diff line change
@@ -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 <div>{router.pathname}</div>
}
}

export default withRouter(Page)
```
76 changes: 50 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 { execOnce, 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 }) {
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,50 @@ 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) {
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

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')
Loading

0 comments on commit eca8e8f

Please sign in to comment.