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