rsnext/packages/next/shared/lib/side-effect.tsx
Jiachi Liu 16fd15b526
Migrate head side effects to hooks (#37526)
* rewrite head side effects component in hooks
* remove mapping from element to children in head manager since they're already the children of `<Head>`

When move `SideEffect` to hooks, the effects scheduling is earlier than life cycle. We're leverage layout effects and effects at the same time, always cache the latest head updating function in head manager in layout effects, and flush them in the effects. This could help get rid of the promises delaying approach in head manager.

Co-authored-by: Shu Ding <3676859+shuding@users.noreply.github.com>
2022-06-08 11:26:57 +00:00

74 lines
2.2 KiB
TypeScript

import React, { Children, useEffect, useLayoutEffect } from 'react'
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
children: React.ReactNode
}
const isServer = typeof window === 'undefined'
const useClientOnlyLayoutEffect = isServer ? () => {} : useLayoutEffect
const useClientOnlyEffect = isServer ? () => {} : useEffect
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))
}
}
if (isServer) {
headManager?.mountedInstances?.add(props.children)
emitChange()
}
useClientOnlyLayoutEffect(() => {
headManager?.mountedInstances?.add(props.children)
return () => {
headManager?.mountedInstances?.delete(props.children)
}
})
// We need to call `updateHead` method whenever the `SideEffect` is trigger in all
// life-cycles: mount, update, unmount. However, if there are multiple `SideEffect`s
// being rendered, we only trigger the method from the last one.
// This is ensured by keeping the last unflushed `updateHead` in the `_pendingUpdate`
// singleton in the layout effect pass, and actually trigger it in the effect pass.
useClientOnlyLayoutEffect(() => {
if (headManager) {
headManager._pendingUpdate = emitChange
}
return () => {
if (headManager) {
headManager._pendingUpdate = emitChange
}
}
})
useClientOnlyEffect(() => {
if (headManager && headManager._pendingUpdate) {
headManager._pendingUpdate()
headManager._pendingUpdate = null
}
return () => {
if (headManager && headManager._pendingUpdate) {
headManager._pendingUpdate()
headManager._pendingUpdate = null
}
}
})
return null
}