Skip to content

Commit

Permalink
Rewrite head side effects with hooks
Browse files Browse the repository at this point in the history
  • Loading branch information
huozhi committed Jun 7, 2022
1 parent dc36199 commit ebd1dd6
Show file tree
Hide file tree
Showing 3 changed files with 72 additions and 94 deletions.
79 changes: 35 additions & 44 deletions packages/next/client/head-manager.ts
Expand Up @@ -118,57 +118,48 @@ export default function initHeadManager(): {
mountedInstances: Set<unknown>
updateHead: (head: JSX.Element[]) => void
} {
let updatePromise: Promise<void> | null = null

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

updatePromise = null
const tags: Record<string, JSX.Element[]> = {}

head.forEach((h) => {
const tags: Record<string, JSX.Element[]> = {}

head.forEach((h) => {
if (
// If the font tag is loaded only on client navigation
// it won't be inlined. In this case revert to the original behavior
h.type === 'link' &&
h.props['data-optimized-fonts']
) {
if (
// If the font tag is loaded only on client navigation
// it won't be inlined. In this case revert to the original behavior
h.type === 'link' &&
h.props['data-optimized-fonts']
document.querySelector(`style[data-href="${h.props['data-href']}"]`)
) {
if (
document.querySelector(
`style[data-href="${h.props['data-href']}"]`
)
) {
return
} else {
h.props.href = h.props['data-href']
h.props['data-href'] = undefined
}
return
} else {
h.props.href = h.props['data-href']
h.props['data-href'] = undefined
}

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
: Array.isArray(children)
? children.join('')
: ''
}
if (title !== document.title) document.title = title
;['meta', 'base', 'link', 'style', 'script'].forEach((type) => {
updateElements(type, tags[type] || [])
})
}))

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
: Array.isArray(children)
? children.join('')
: ''
}
if (title !== document.title) document.title = title
;['meta', 'base', 'link', 'style', 'script'].forEach((type) => {
updateElements(type, tags[type] || [])
})
},
}
}
15 changes: 3 additions & 12 deletions packages/next/shared/lib/head.tsx
Expand Up @@ -116,22 +116,13 @@ function unique() {

/**
*
* @param headElements List of multiple <Head> instances
* @param headChildrenElements List of children of <Head>
*/
function reduceComponents(
headElements: Array<React.ReactElement<any>>,
headChildrenElements: Array<React.ReactElement<any>>,
props: WithInAmpMode
) {
return headElements
.reduce(
(list: React.ReactChild[], headElement: React.ReactElement<any>) => {
const headElementChildren = React.Children.toArray(
headElement.props.children
)
return list.concat(headElementChildren)
},
[]
)
return headChildrenElements
.reduce(onlyReactElement, [])
.reverse()
.concat(defaultHead(props.inAmpMode).reverse())
Expand Down
72 changes: 34 additions & 38 deletions packages/next/shared/lib/side-effect.tsx
@@ -1,6 +1,4 @@
import React, { Component } from 'react'

const isServer = typeof window === 'undefined'
import React, { Children, useEffect, useLayoutEffect } from 'react'

type State = JSX.Element[] | undefined

Expand All @@ -12,49 +10,47 @@ type SideEffectProps = {
handleStateChange?: (state: State) => void
headManager: any
inAmpMode?: boolean
children: React.ReactNode
}

export default class extends Component<SideEffectProps> {
private _hasHeadManager: boolean

emitChange = (): void => {
if (this._hasHeadManager) {
this.props.headManager.updateHead(
this.props.reduceComponentsToState(
[...this.props.headManager.mountedInstances],
this.props
)
)
export default function SideEffect(props: SideEffectProps) {
const { headManager, reduceComponentsToState } = props

function emitChange() {
if (headManager && headManager.mountedInstances) {
const headElements = Children.toArray(
headManager.mountedInstances
).filter(Boolean) as React.ReactElement[]
headManager.updateHead(reduceComponentsToState(headElements, props))
}
}

constructor(props: any) {
super(props)
this._hasHeadManager =
this.props.headManager && this.props.headManager.mountedInstances
if (typeof window === 'undefined') {
headManager?.mountedInstances?.add(props.children)
emitChange()
}

if (isServer && this._hasHeadManager) {
this.props.headManager.mountedInstances.add(this)
this.emitChange()
useLayoutEffect(() => {
headManager?.mountedInstances?.add(props.children)
return () => {
headManager?.mountedInstances?.delete(props.children)
}
}
componentDidMount() {
if (this._hasHeadManager) {
this.props.headManager.mountedInstances.add(this)
})

// Cache emitChange in headManager in layout effects and execute later in effects.
// Since `useEffect` is async effects emitChange will only keep the latest results.
useLayoutEffect(() => {
if (headManager) {
headManager._pendingUpdate = emitChange
}
this.emitChange()
}
componentDidUpdate() {
this.emitChange()
}
componentWillUnmount() {
if (this._hasHeadManager) {
this.props.headManager.mountedInstances.delete(this)
})

useEffect(() => {
if (headManager && headManager._pendingUpdate) {
headManager._pendingUpdate()
headManager._pendingUpdate = null
}
this.emitChange()
}
})

render() {
return null
}
return null
}

0 comments on commit ebd1dd6

Please sign in to comment.