1/**
2 * React helpers for creating Atom instances that belong to one component
3 * subtree. `make` returns a scoped atom with a provider, context, and `use`
4 * accessor. Each provider creates its own Atom once, so different subtrees can
5 * use the same scoped atom definition without sharing state.
6 *
7 * @since 4.0.0
8 */
9"use client"
10
11import type * as Atom from "effect/reactivity/Atom"
12import * as React from "react"
13
14/**
15 * Literal type used as the `ScopedAtom` type identifier.
16 *
17 * **Details**
18 *
19 * Used as the computed property key and marker value stored on `ScopedAtom`
20 * objects.
21 *
22 * @category type IDs
23 * @since 4.0.0
24 */
25export type TypeId = "~@effect/atom-react/ScopedAtom"
26
27/**
28 * Type identifier for ScopedAtom.
29 *
30 * **Details**
31 *
32 * Used as the computed property key and marker value stored on `ScopedAtom`
33 * objects.
34 *
35 * @category type IDs
36 * @since 4.0.0
37 */
38export const TypeId: TypeId = "~@effect/atom-react/ScopedAtom"
39
40/**
41 * Scoped Atom interface with a provider-backed instance.
42 *
43 * **Example** (Providing and reading a scoped atom)
44 *
45 * ```ts import.meta.vitest
46 * import { make, useAtomValue } from "@effect/atom-react"
47 * import { Atom } from "effect/reactivity"
48 * import * as React from "react"
49 * import { renderToStaticMarkup } from "react-dom/server"
50 *
51 * const Counter = make(() => Atom.make(0))
52 *
53 * function View() {
54 * const atom = Counter.use()
55 * const value = useAtomValue(atom)
56 * return React.createElement("div", null, value)
57 * }
58 *
59 * export function App() {
60 * return React.createElement(Counter.Provider, null, React.createElement(View))
61 * }
62 *
63 * renderToStaticMarkup(React.createElement(App)) // => "<div>0</div>"
64 * ```
65 *
66 * @category models
67 * @since 4.0.0
68 */
69export interface ScopedAtom<A extends Atom.Atom<any>, Input = never> {
70 readonly [TypeId]: TypeId
71 use(): A
72 Provider: [Input] extends [never] ? React.FC<{ readonly children?: React.ReactNode | undefined }>
73 : React.FC<{ readonly children?: React.ReactNode | undefined; readonly value: Input }>
74 Context: React.Context<A>
75}
76
77/**
78 * Creates a ScopedAtom from a factory function.
79 *
80 * **When to use**
81 *
82 * Use to create an atom instance that is owned by a React provider and scoped
83 * to a component subtree.
84 *
85 * **Details**
86 *
87 * The returned scoped atom includes a `Provider`, `Context`, and `use`
88 * accessor. The provider creates the atom once for its lifetime, passing the
89 * `value` prop to the factory when the scoped atom expects input.
90 *
91 * **Gotchas**
92 *
93 * `use` must run under the matching provider. Changing the provider `value`
94 * prop after mount does not recreate the atom.
95 *
96 * **Example** (Creating a scoped atom with input)
97 *
98 * ```ts import.meta.vitest
99 * import { make, useAtomValue } from "@effect/atom-react"
100 * import { Atom } from "effect/reactivity"
101 * import * as React from "react"
102 * import { renderToStaticMarkup } from "react-dom/server"
103 *
104 * const User = make((name: string) => Atom.make(name))
105 *
106 * function UserName() {
107 * const atom = User.use()
108 * const value = useAtomValue(atom)
109 * return React.createElement("span", null, value)
110 * }
111 *
112 * export function App() {
113 * return React.createElement(
114 * User.Provider,
115 * { value: "Ada" },
116 * React.createElement(UserName)
117 * )
118 * }
119 *
120 * renderToStaticMarkup(React.createElement(App)) // => "<span>Ada</span>"
121 * ```
122 *
123 * @category constructors
124 * @since 4.0.0
125 */
126export const make = <A extends Atom.Atom<any>, Input = never>(
127 f: (() => A) | ((input: Input) => A)
128): ScopedAtom<A, Input> => {
129 const Context = React.createContext<A>(undefined as unknown as A)
130
131 const use = (): A => {
132 const atom = React.useContext(Context)
133 if (atom === undefined) {
134 throw new Error("ScopedAtom used outside of its Provider")
135 }
136 return atom
137 }
138
139 const Provider: React.FC<{ readonly children?: React.ReactNode | undefined; readonly value?: Input }> = (props) => {
140 const atom = React.useRef<A | null>(null)
141 if (atom.current === null) {
142 if ("value" in props) {
143 atom.current = (f as (input: Input) => A)(props.value as Input)
144 } else {
145 atom.current = (f as () => A)()
146 }
147 }
148 return React.createElement(Context.Provider, { value: atom.current }, props.children)
149 }
150
151 return {
152 [TypeId]: TypeId,
153 use,
154 Provider: Provider as any,
155 Context
156 }
157}