Skip to content

Commit

Permalink
Remove Head.rewind as it's no longer needed (vercel#14091)
Browse files Browse the repository at this point in the history
  • Loading branch information
timneutkens authored and rokinsky committed Jul 11, 2020
1 parent 13c4f29 commit cc84163
Show file tree
Hide file tree
Showing 7 changed files with 99 additions and 119 deletions.
45 changes: 24 additions & 21 deletions packages/next/client/head-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,29 +70,32 @@ function updateElements(type, components) {
export default function initHeadManager() {
let updatePromise = null

return (head) => {
const promise = (updatePromise = Promise.resolve().then(() => {
if (promise !== updatePromise) return
return {
mountedInstances: new Set(),
updateHead: (head) => {
const promise = (updatePromise = Promise.resolve().then(() => {
if (promise !== updatePromise) return

updatePromise = null
const tags = {}
updatePromise = null
const tags = {}

head.forEach((h) => {
const components = tags[h.type] || []
components.push(h)
tags[h.type] = components
})
head.forEach((h) => {
const components = tags[h.type] || []
components.push(h)
tags[h.type] = components
})

const titleComponent = tags.title ? tags.title[0] : null
let title = ''
if (titleComponent) {
const { children } = titleComponent.props
title = typeof children === 'string' ? children : children.join('')
}
if (title !== document.title) document.title = title
;['meta', 'base', 'link', 'style', 'script'].forEach((type) => {
updateElements(type, tags[type] || [])
})
}))
const titleComponent = tags.title ? tags.title[0] : null
let title = ''
if (titleComponent) {
const { children } = titleComponent.props
title = typeof children === 'string' ? children : children.join('')
}
if (title !== document.title) document.title = title
;['meta', 'base', 'link', 'style', 'script'].forEach((type) => {
updateElements(type, tags[type] || [])
})
}))
},
}
}
4 changes: 2 additions & 2 deletions packages/next/client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ if (window.__NEXT_P) {
window.__NEXT_P = []
window.__NEXT_P.push = register

const updateHead = initHeadManager()
const headManager = initHeadManager()
const appElement = document.getElementById('__next')

let lastAppProps
Expand Down Expand Up @@ -484,7 +484,7 @@ function AppContainer({ children }) {
}
>
<RouterContext.Provider value={makePublicRouterInstance(router)}>
<HeadManagerContext.Provider value={updateHead}>
<HeadManagerContext.Provider value={headManager}>
{children}
</HeadManagerContext.Provider>
</RouterContext.Provider>
Expand Down
5 changes: 4 additions & 1 deletion packages/next/next-server/lib/head-manager-context.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import React from 'react'

export const HeadManagerContext: React.Context<any> = React.createContext(null)
export const HeadManagerContext: React.Context<{
updateHead?: (state: any) => void
mountedInstances?: any
}> = React.createContext({})

if (process.env.NODE_ENV !== 'production') {
HeadManagerContext.displayName = 'HeadManagerContext'
Expand Down
33 changes: 13 additions & 20 deletions packages/next/next-server/lib/head.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react'
import withSideEffect from './side-effect'
import React, { useContext } from 'react'
import Effect from './side-effect'
import { AmpStateContext } from './amp-context'
import { HeadManagerContext } from './head-manager-context'
import { isInAmpMode } from './amp'
Expand Down Expand Up @@ -140,32 +140,25 @@ function reduceComponents(
})
}

const Effect = withSideEffect()

/**
* This component injects elements to `<head>` of your page.
* To avoid duplicated `tags` in `<head>` you can use the `key` property, which will make sure every tag is only rendered once.
*/
function Head({ children }: { children: React.ReactNode }) {
const ampState = useContext(AmpStateContext)
const headManager = useContext(HeadManagerContext)
return (
<AmpStateContext.Consumer>
{(ampState) => (
<HeadManagerContext.Consumer>
{(updateHead) => (
<Effect
reduceComponentsToState={reduceComponents}
handleStateChange={updateHead}
inAmpMode={isInAmpMode(ampState)}
>
{children}
</Effect>
)}
</HeadManagerContext.Consumer>
)}
</AmpStateContext.Consumer>
<Effect
reduceComponentsToState={reduceComponents}
headManager={headManager}
inAmpMode={isInAmpMode(ampState)}
>
{children}
</Effect>
)
}

Head.rewind = Effect.rewind
// TODO: Remove in the next major release
Head.rewind = () => {}

export default Head
69 changes: 28 additions & 41 deletions packages/next/next-server/lib/side-effect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,61 +2,48 @@ import React, { Component } from 'react'

const isServer = typeof window === 'undefined'

type State = Array<React.ReactElement<any>> | undefined
type State = JSX.Element[] | undefined

type SideEffectProps = {
reduceComponentsToState: <T>(
components: Array<React.ReactElement<any>>,
props: T
) => State
handleStateChange?: (state: State) => void
headManager: any
inAmpMode?: boolean
}

export default () => {
const mountedInstances: Set<any> = new Set()
let state: State

function emitChange(component: React.Component<SideEffectProps>): void {
state = component.props.reduceComponentsToState(
[...mountedInstances],
component.props
export default class extends Component<SideEffectProps> {
emitChange = (): void => {
this.props.headManager.updateHead(
this.props.reduceComponentsToState(
[...this.props.headManager.mountedInstances],
this.props
)
)
if (component.props.handleStateChange) {
component.props.handleStateChange(state)
}
}

return class extends Component<SideEffectProps> {
// Used when server rendering
static rewind() {
const recordedState = state
state = undefined
mountedInstances.clear()
return recordedState
}

constructor(props: any) {
super(props)
if (isServer) {
mountedInstances.add(this)
emitChange(this)
}
}
componentDidMount() {
mountedInstances.add(this)
emitChange(this)
}
componentDidUpdate() {
emitChange(this)
}
componentWillUnmount() {
mountedInstances.delete(this)
emitChange(this)
constructor(props: any) {
super(props)
if (isServer) {
this.props.headManager.mountedInstances.add(this)
this.emitChange()
}
}
componentDidMount() {
this.props.headManager.mountedInstances.add(this)
this.emitChange()
}
componentDidUpdate() {
this.emitChange()
}
componentWillUnmount() {
this.props.headManager.mountedInstances.delete(this)
this.emitChange()
}

render() {
return null
}
render() {
return null
}
}
61 changes: 27 additions & 34 deletions packages/next/next-server/server/render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ import {
STATIC_PROPS_ID,
SERVER_PROPS_ID,
} from '../lib/constants'
import Head, { defaultHead } from '../lib/head'
import { defaultHead } from '../lib/head'
import Loadable from '../lib/loadable'
import { LoadableContext } from '../lib/loadable-context'
import { HeadManagerContext } from '../lib/head-manager-context'
import mitt, { MittEmitter } from '../lib/mitt'
import { RouterContext } from '../lib/router-context'
import { NextRouter } from '../lib/router/router'
Expand Down Expand Up @@ -118,22 +119,6 @@ function enhanceComponents(
}
}

function render(
element: React.ReactElement<any>,
ampMode: any
): { html: string; head: React.ReactElement[] } {
let html
let head

try {
html = renderToString(element)
} finally {
head = Head.rewind() || defaultHead(isInAmpMode(ampMode))
}

return { html, head }
}

export type RenderOptsPartial = {
buildId: string
canonicalBase: string
Expand Down Expand Up @@ -463,16 +448,29 @@ export async function renderToHTML(
hybrid: pageConfig.amp === 'hybrid',
}

const inAmpMode = isInAmpMode(ampState)

const reactLoadableModules: string[] = []

let head: JSX.Element[] = defaultHead(inAmpMode)

const AppContainer = ({ children }: any) => (
<RouterContext.Provider value={router}>
<AmpStateContext.Provider value={ampState}>
<LoadableContext.Provider
value={(moduleName) => reactLoadableModules.push(moduleName)}
<HeadManagerContext.Provider
value={{
updateHead: (state) => {
head = state
},
mountedInstances: new Set(),
}}
>
{children}
</LoadableContext.Provider>
<LoadableContext.Provider
value={(moduleName) => reactLoadableModules.push(moduleName)}
>
{children}
</LoadableContext.Provider>
</HeadManagerContext.Provider>
</AmpStateContext.Provider>
</RouterContext.Provider>
)
Expand Down Expand Up @@ -661,35 +659,31 @@ export async function renderToHTML(
]),
]

const renderPageError = (): { html: string; head: any } | void => {
const renderPage: RenderPage = (
options: ComponentsEnhancer = {}
): { html: string; head: any } => {
if (ctx.err && ErrorDebug) {
return render(<ErrorDebug error={ctx.err} />, ampState)
return { html: renderToString(<ErrorDebug error={ctx.err} />), head }
}

if (dev && (props.router || props.Component)) {
throw new Error(
`'router' and 'Component' can not be returned in getInitialProps from _app.js https://err.sh/vercel/next.js/cant-override-next-props`
)
}
}

let renderPage: RenderPage = (
options: ComponentsEnhancer = {}
): { html: string; head: any } => {
const renderError = renderPageError()
if (renderError) return renderError

const {
App: EnhancedApp,
Component: EnhancedComponent,
} = enhanceComponents(options, App, Component)

return render(
const html = renderToString(
<AppContainer>
<EnhancedApp Component={EnhancedComponent} router={router} {...props} />
</AppContainer>,
ampState
</AppContainer>
)

return { html, head }
}
const documentCtx = { ...ctx, renderPage }
const docProps: DocumentInitialProps = await loadGetInitialProps(
Expand Down Expand Up @@ -721,7 +715,6 @@ export async function renderToHTML(
}

const dynamicImportsIds = [...dynamicImportIdsSet]
const inAmpMode = isInAmpMode(ampState)
const hybridAmp = ampState.hybrid

// update renderOpts so export knows current state
Expand Down
1 change: 1 addition & 0 deletions test/integration/basic/test/error-recovery.js
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ export default (context, renderViaHTTP) => {
in Context.Provider
in Context.Provider
in Context.Provider
in Context.Provider
in AppContainer
This error happened while generating the page. Any console logs will be displayed in the terminal window."
Expand Down

0 comments on commit cc84163

Please sign in to comment.