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 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
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
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 }) {
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,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')