Created
December 18, 2024 06:30
-
-
Save sagarpanchal/ca86bc696837f573cb271ac189eca3a0 to your computer and use it in GitHub Desktop.
Hooks to manage Global Refs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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