Skip to content

Instantly share code, notes, and snippets.

@sagarpanchal
Created December 18, 2024 06:30
Show Gist options
  • Save sagarpanchal/ca86bc696837f573cb271ac189eca3a0 to your computer and use it in GitHub Desktop.
Save sagarpanchal/ca86bc696837f573cb271ac189eca3a0 to your computer and use it in GitHub Desktop.
Hooks to manage Global Refs
import React from 'react'
declare global {
interface GlobalListsMap {
default: unknown
}
interface Window {
globalListsMap: {
[K in keyof GlobalListsMap]: GlobalListsMap[K] extends unknown ? unknown[] : GlobalListsMap[K][] // Turn types into arrays
}
}
interface WindowEventMap<T extends keyof GlobalListsMap = 'default'> {
globalListChange: CustomEvent<{
type: T
action: 1 | -1 // added 1, removed -1
element: GlobalListsMap[T]
}>
}
}
type GlobalRefList<T extends keyof GlobalListsMap> = GlobalListsMap[T][]
window.globalListsMap = { ...window.globalListsMap }
const getGlobalList = <T extends keyof GlobalListsMap>(type: T): GlobalRefList<T> => {
window.globalListsMap[type] ??= []
return window.globalListsMap[type] as GlobalRefList<T>
}
const setGlobalList = <T extends keyof GlobalListsMap>(type: T, value: GlobalRefList<T>) => {
window.globalListsMap[type] = value
}
const addGlobalListItem = <T extends keyof GlobalListsMap>(type: T, element: GlobalListsMap[T]) => {
const globalRefs = getGlobalList(type)
if (globalRefs.includes(element)) return
setGlobalList(type, [...globalRefs, element])
emitGlobalListChange(type, 1, element)
}
const removeGlobalListItem = <T extends keyof GlobalListsMap>(type: T, element: GlobalListsMap[T]) => {
const globalRefs = getGlobalList(type)
if (!globalRefs.includes(element)) return
setGlobalList(
type,
globalRefs.filter(el => el !== element)
)
emitGlobalListChange(type, -1, element)
}
const emitGlobalListChange = <T extends keyof GlobalListsMap>(
type: T,
action: 1 | -1,
element: GlobalListsMap[T]
) => {
const event = new CustomEvent('globalListChange', { detail: { type, action, element } })
setTimeout(() => window.dispatchEvent(event))
}
export const useRegisterGlobalData = <T extends keyof GlobalListsMap>(type: T) => {
const [localData, setLocalData] = React.useState<GlobalListsMap[T] | null>(null)
const refCallback = React.useCallback((element: GlobalListsMap[T] | null | undefined) => {
setLocalData(element ?? null)
}, [])
React.useEffect(() => {
if (!localData) return
addGlobalListItem(type, localData)
return () => {
removeGlobalListItem(type, localData)
}
}, [localData, type])
return refCallback
}
export const useGlobalList = <T extends keyof GlobalListsMap>(type: T): GlobalRefList<T> => {
const [listRef, setListRef] = React.useState(() => getGlobalList(type))
React.useEffect(() => {
const handleRefChange = (event: WindowEventMap['globalListChange']) => {
if (event.detail.type !== type) return
setListRef(() => getGlobalList(type))
}
window.addEventListener('globalListChange', handleRefChange)
return () => window.removeEventListener('globalListChange', handleRefChange)
}, [type])
return listRef
}
export const useTransformGlobalList = <T extends keyof GlobalListsMap, R>(
type: T,
transform: (elements: GlobalRefList<T>) => R
): R => {
const refs = useGlobalList(type)
return React.useMemo(() => transform(refs), [refs, transform])
}
import React from 'react'
import { useRegisterGlobalData, useTransformGlobalList } from './globalRefs'
declare global {
interface GlobalListsMap {
header: HTMLElement | SVGElement | MathMLElement
}
}
const getFilteredElements = (elements: GlobalListsMap['header'][]) => {
return elements.filter(el => el && document.body.contains(el))
}
const getTotalOffsetHeight = (elements: GlobalListsMap['header'][]) => {
return elements.reduce((acc, el) => acc + el.getBoundingClientRect().height, 0)
}
const getContentHeightCssValue = (elements: GlobalListsMap['header'][]) => {
const heightOffset = getTotalOffsetHeight(elements)
return `calc(100vh - ${heightOffset}px)`
}
export const useRegisterHeaderElement = () => useRegisterGlobalData('header')
export const useApplyHeightOffset = (el: GlobalListsMap['header'] | null = null) => {
const [element, setElement] = React.useState(() => el)
const headerElements = useTransformGlobalList('header', getFilteredElements)
React.useEffect(() => {
if (!element) return
const updateHeight = () => {
element.style.height = getContentHeightCssValue(headerElements)
element.style.minHeight = 'unset'
}
const resizeObserver = new ResizeObserver(updateHeight)
headerElements.forEach(el => resizeObserver.observe(el))
resizeObserver.observe(element)
setTimeout(updateHeight)
return () => {
resizeObserver.disconnect()
}
}, [headerElements, element])
React.useEffect(() => {
if (el) setElement(el)
}, [el])
return setElement
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment