about summary refs log tree commit diff
path: root/src/components/Portal.tsx
blob: b4bebce4defb29462201ff106298b044c27a2ea1 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
import {
  createContext,
  Fragment,
  useCallback,
  useContext,
  useEffect,
  useId,
  useMemo,
  useRef,
  useState,
} from 'react'

type Component = React.ReactElement

type ContextType = {
  outlet: Component | null
  append(id: string, component: Component): void
  remove(id: string): void
}

type ComponentMap = {
  [id: string]: Component | null
}

export function createPortalGroup() {
  const Context = createContext<ContextType>({
    outlet: null,
    append: () => {},
    remove: () => {},
  })
  Context.displayName = 'PortalContext'

  function Provider(props: React.PropsWithChildren<{}>) {
    const map = useRef<ComponentMap>({})
    const [outlet, setOutlet] = useState<ContextType['outlet']>(null)

    const append = useCallback<ContextType['append']>((id, component) => {
      if (map.current[id]) return
      map.current[id] = <Fragment key={id}>{component}</Fragment>
      setOutlet(<>{Object.values(map.current)}</>)
    }, [])

    const remove = useCallback<ContextType['remove']>(id => {
      map.current[id] = null
      setOutlet(<>{Object.values(map.current)}</>)
    }, [])

    const contextValue = useMemo(
      () => ({
        outlet,
        append,
        remove,
      }),
      [outlet, append, remove],
    )

    return (
      <Context.Provider value={contextValue}>{props.children}</Context.Provider>
    )
  }

  function Outlet() {
    const ctx = useContext(Context)
    return ctx.outlet
  }

  function Portal({children}: React.PropsWithChildren<{}>) {
    const {append, remove} = useContext(Context)
    const id = useId()
    useEffect(() => {
      append(id, children as Component)
      return () => remove(id)
    }, [id, children, append, remove])
    return null
  }

  return {Provider, Outlet, Portal}
}

const DefaultPortal = createPortalGroup()
export const Provider = DefaultPortal.Provider
export const Outlet = DefaultPortal.Outlet
export const Portal = DefaultPortal.Portal