my fork of the bluesky client
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