my fork of the bluesky client
at main 72 lines 1.9 kB view raw
1import React from 'react' 2 3type Component = React.ReactElement 4 5type ContextType = { 6 outlet: Component | null 7 append(id: string, component: Component): void 8 remove(id: string): void 9} 10 11type ComponentMap = { 12 [id: string]: Component 13} 14 15export function createPortalGroup() { 16 const Context = React.createContext<ContextType>({ 17 outlet: null, 18 append: () => {}, 19 remove: () => {}, 20 }) 21 22 function Provider(props: React.PropsWithChildren<{}>) { 23 const map = React.useRef<ComponentMap>({}) 24 const [outlet, setOutlet] = React.useState<ContextType['outlet']>(null) 25 26 const append = React.useCallback<ContextType['append']>((id, component) => { 27 if (map.current[id]) return 28 map.current[id] = <React.Fragment key={id}>{component}</React.Fragment> 29 setOutlet(<>{Object.values(map.current)}</>) 30 }, []) 31 32 const remove = React.useCallback<ContextType['remove']>(id => { 33 delete map.current[id] 34 setOutlet(<>{Object.values(map.current)}</>) 35 }, []) 36 37 const contextValue = React.useMemo( 38 () => ({ 39 outlet, 40 append, 41 remove, 42 }), 43 [outlet, append, remove], 44 ) 45 46 return ( 47 <Context.Provider value={contextValue}>{props.children}</Context.Provider> 48 ) 49 } 50 51 function Outlet() { 52 const ctx = React.useContext(Context) 53 return ctx.outlet 54 } 55 56 function Portal({children}: React.PropsWithChildren<{}>) { 57 const {append, remove} = React.useContext(Context) 58 const id = React.useId() 59 React.useEffect(() => { 60 append(id, children as Component) 61 return () => remove(id) 62 }, [id, children, append, remove]) 63 return null 64 } 65 66 return {Provider, Outlet, Portal} 67} 68 69const DefaultPortal = createPortalGroup() 70export const Provider = DefaultPortal.Provider 71export const Outlet = DefaultPortal.Outlet 72export const Portal = DefaultPortal.Portal