1/**
2 * React hooks for working with Effect atoms from components. The hooks read,
3 * write, mount, refresh, and subscribe to atoms from `RegistryContext`, handle
4 * `AsyncResult` atoms with React Suspense, and expose helpers for reading and
5 * deriving `AtomRef` values.
6 *
7 * @since 4.0.0
8 */
9"use client"
10
11import * as Cause from "effect/Cause"
12import * as Effect from "effect/Effect"
13import * as Exit from "effect/Exit"
14import type * as AsyncResult from "effect/reactivity/AsyncResult"
15import * as Atom from "effect/reactivity/Atom"
16import type * as AtomRef from "effect/reactivity/AtomRef"
17import * as AtomRegistry from "effect/reactivity/AtomRegistry"
18import * as React from "react"
19import { RegistryContext } from "./RegistryContext.ts"
20
21interface AtomStore<A> {
22 readonly subscribe: (f: () => void) => () => void
23 readonly snapshot: () => A
24 readonly getServerSnapshot: () => A
25}
26
27const storeRegistry = new WeakMap<AtomRegistry.AtomRegistry, WeakMap<Atom.Atom<any>, AtomStore<any>>>()
28
29function makeStore<A>(registry: AtomRegistry.AtomRegistry, atom: Atom.Atom<A>): AtomStore<A> {
30 let stores = storeRegistry.get(registry)
31 if (stores === undefined) {
32 stores = new WeakMap()
33 storeRegistry.set(registry, stores)
34 }
35 const store = stores.get(atom)
36 if (store !== undefined) {
37 return store
38 }
39 const newStore: AtomStore<A> = {
40 subscribe(f) {
41 return registry.subscribe(atom, f)
42 },
43 snapshot() {
44 return registry.get(atom)
45 },
46 getServerSnapshot() {
47 return Atom.getServerValue(atom, registry)
48 }
49 }
50 stores.set(atom, newStore)
51 return newStore
52}
53
54function useStore<A>(registry: AtomRegistry.AtomRegistry, atom: Atom.Atom<A>): A {
55 const store = makeStore(registry, atom)
56
57 return React.useSyncExternalStore(store.subscribe, store.snapshot, store.getServerSnapshot)
58}
59
60const initialValuesSet = new WeakMap<AtomRegistry.AtomRegistry, WeakSet<Atom.Atom<any>>>()
61
62/**
63 * Seeds initial atom values in the current React atom registry.
64 *
65 * **When to use**
66 *
67 * Use to seed atom values from a React component after the current registry
68 * already exists.
69 *
70 * **Gotchas**
71 *
72 * Each atom is initialized at most once for a given registry by this hook, so
73 * later calls for the same atom in that registry are ignored.
74 *
75 * @category hooks
76 * @since 4.0.0
77 */
78export const useAtomInitialValues = (initialValues: Iterable<readonly [Atom.Atom<any>, any]>): void => {
79 const registry = React.useContext(RegistryContext)
80 let set = initialValuesSet.get(registry)
81 if (set === undefined) {
82 set = new WeakSet()
83 initialValuesSet.set(registry, set)
84 }
85 for (const [atom, value] of initialValues) {
86 if (!set.has(atom)) {
87 set.add(atom)
88 ;(registry as any).ensureNode(atom).setValue(value)
89 }
90 }
91}
92
93/**
94 * Subscribes to an atom in the current React registry and returns its current
95 * value, optionally mapped through a selector.
96 *
97 * **When to use**
98 *
99 * Use when a React component needs to render from an atom value without also
100 * returning a setter.
101 *
102 * **Details**
103 *
104 * When a selector is provided, the hook maps the atom before subscribing so the
105 * component reads the selected value from the current `RegistryContext`.
106 *
107 * @see {@link useAtom} for reading and updating a writable atom from one component
108 * @see {@link useAtomRef} for reading an `AtomRef` directly
109 *
110 * @category hooks
111 * @since 4.0.0
112 */
113export const useAtomValue: {
114 <A>(atom: Atom.Atom<A>): A
115 <A, B>(atom: Atom.Atom<A>, f: (_: A) => B): B
116} = <A>(atom: Atom.Atom<A>, f?: (_: A) => A): A => {
117 const registry = React.useContext(RegistryContext)
118 if (f) {
119 const atomB = React.useMemo(() => Atom.map(atom, f), [atom, f])
120 return useStore(registry, atomB)
121 }
122 return useStore(registry, atom)
123}
124
125function mountAtom<A>(registry: AtomRegistry.AtomRegistry, atom: Atom.Atom<A>): void {
126 React.useEffect(() => registry.mount(atom), [atom, registry])
127}
128
129function setAtom<R, W, Mode extends "value" | "promise" | "promiseExit" = never>(
130 registry: AtomRegistry.AtomRegistry,
131 atom: Atom.Writable<R, W>,
132 options?: {
133 readonly mode?: ([R] extends [AsyncResult.AsyncResult<any, any>] ? Mode : "value") | undefined
134 }
135): "promise" extends Mode ? (
136 (value: W) => Promise<AsyncResult.AsyncResult.Success<R>>
137 ) :
138 "promiseExit" extends Mode ? (
139 (value: W) => Promise<Exit.Exit<AsyncResult.AsyncResult.Success<R>, AsyncResult.AsyncResult.Failure<R>>>
140 ) :
141 ((value: W | ((value: R) => W)) => void)
142{
143 if (options?.mode === "promise" || options?.mode === "promiseExit") {
144 return React.useCallback((value: W) => {
145 registry.set(atom, value)
146 const promise = Effect.runPromiseExit(
147 AtomRegistry.getResult(registry, atom as Atom.Atom<AsyncResult.AsyncResult<any, any>>, {
148 suspendOnWaiting: true
149 })
150 )
151 return options!.mode === "promise" ? promise.then(flattenExit) : promise
152 }, [registry, atom, options.mode]) as any
153 }
154 return React.useCallback((value: W | ((value: R) => W)) => {
155 registry.set(atom, typeof value === "function" ? (value as any)(registry.get(atom)) : value)
156 }, [registry, atom]) as any
157}
158
159const flattenExit = <A, E>(exit: Exit.Exit<A, E>): A => {
160 if (Exit.isSuccess(exit)) return exit.value
161 throw Cause.squash(exit.cause)
162}
163
164/**
165 * Mounts an atom in the current React registry for the lifetime of the
166 * component.
167 *
168 * **When to use**
169 *
170 * Use to keep an atom mounted from a React component without reading, writing,
171 * or refreshing it.
172 *
173 * **Details**
174 *
175 * The hook uses the current `RegistryContext` and releases the mount through
176 * React effect cleanup when the component unmounts or when the registry or atom
177 * dependency changes.
178 *
179 * @see {@link useAtomSet} for mounting a writable atom while returning a setter
180 * @see {@link useAtomRefresh} for mounting an atom while returning a refresh callback
181 *
182 * @category hooks
183 * @since 4.0.0
184 */
185export const useAtomMount = <A>(atom: Atom.Atom<A>): void => {
186 const registry = React.useContext(RegistryContext)
187 mountAtom(registry, atom)
188}
189
190/**
191 * Mounts a writable atom and returns a setter without subscribing to its value.
192 *
193 * **When to use**
194 *
195 * Use when a React component needs to update a writable atom without rendering
196 * from that atom's value.
197 *
198 * **Details**
199 *
200 * The hook mounts the atom and returns a setter. In value mode the setter
201 * accepts a write value or updater function; for `AsyncResult` atoms, `promise`
202 * and `promiseExit` modes return a promise for the success value or full `Exit`.
203 *
204 * @see {@link useAtom} for reading and updating the same writable atom
205 *
206 * @category hooks
207 * @since 4.0.0
208 */
209export const useAtomSet = <
210 R,
211 W,
212 Mode extends "value" | "promise" | "promiseExit" = never
213>(
214 atom: Atom.Writable<R, W>,
215 options?: {
216 readonly mode?: ([R] extends [AsyncResult.AsyncResult<any, any>] ? Mode : "value") | undefined
217 }
218): "promise" extends Mode ? (
219 (value: W) => Promise<AsyncResult.AsyncResult.Success<R>>
220 ) :
221 "promiseExit" extends Mode ? (
222 (value: W) => Promise<Exit.Exit<AsyncResult.AsyncResult.Success<R>, AsyncResult.AsyncResult.Failure<R>>>
223 ) :
224 ((value: W | ((value: R) => W)) => void) =>
225{
226 const registry = React.useContext(RegistryContext)
227 mountAtom(registry, atom)
228 return setAtom(registry, atom, options)
229}
230
231/**
232 * Mounts an atom and returns a callback that refreshes it in the current React
233 * registry.
234 *
235 * **When to use**
236 *
237 * Use to expose a React callback that requests a refresh for an atom without
238 * reading or writing its value.
239 *
240 * **Details**
241 *
242 * The hook uses the current `RegistryContext`, mounts the atom for the
243 * component lifetime, and returns a callback that calls `registry.refresh`.
244 *
245 * @see {@link useAtomMount} for mounting an atom without returning a refresh callback
246 *
247 * @category hooks
248 * @since 4.0.0
249 */
250export const useAtomRefresh = <A>(atom: Atom.Atom<A>): () => void => {
251 const registry = React.useContext(RegistryContext)
252 mountAtom(registry, atom)
253 return React.useCallback(() => {
254 registry.refresh(atom)
255 }, [registry, atom])
256}
257
258/**
259 * Subscribes to a writable atom and returns its current value together with a
260 * setter for updating it.
261 *
262 * **When to use**
263 *
264 * Use when a React component needs both to render the current value of a
265 * writable atom and update it from the same component.
266 *
267 * @see {@link useAtomValue} for subscribing to an atom without a setter
268 * @see {@link useAtomSet} for updating a writable atom without subscribing to its value
269 *
270 * @category hooks
271 * @since 4.0.0
272 */
273export const useAtom = <R, W, const Mode extends "value" | "promise" | "promiseExit" = never>(
274 atom: Atom.Writable<R, W>,
275 options?: {
276 readonly mode?: ([R] extends [AsyncResult.AsyncResult<any, any>] ? Mode : "value") | undefined
277 }
278): readonly [
279 value: R,
280 write: "promise" extends Mode ? (
281 (value: W) => Promise<AsyncResult.AsyncResult.Success<R>>
282 ) :
283 "promiseExit" extends Mode ? (
284 (value: W) => Promise<Exit.Exit<AsyncResult.AsyncResult.Success<R>, AsyncResult.AsyncResult.Failure<R>>>
285 ) :
286 ((value: W | ((value: R) => W)) => void)
287] => {
288 const registry = React.useContext(RegistryContext)
289 return [
290 useStore(registry, atom),
291 setAtom(registry, atom, options)
292 ] as const
293}
294
295const atomPromiseMap = {
296 suspendOnWaiting: new WeakMap<
297 AtomRegistry.AtomRegistry,
298 WeakMap<Atom.Atom<any>, Promise<void>>
299 >(),
300 default: new WeakMap<
301 AtomRegistry.AtomRegistry,
302 WeakMap<Atom.Atom<any>, Promise<void>>
303 >()
304}
305
306function atomToPromise<A, E>(
307 registry: AtomRegistry.AtomRegistry,
308 atom: Atom.Atom<AsyncResult.AsyncResult<A, E>>,
309 suspendOnWaiting: boolean
310) {
311 const registries = suspendOnWaiting ? atomPromiseMap.suspendOnWaiting : atomPromiseMap.default
312 let map = registries.get(registry)
313 if (map === undefined) {
314 map = new WeakMap()
315 registries.set(registry, map)
316 }
317 let promise = map.get(atom)
318 if (promise !== undefined) {
319 return promise
320 }
321 promise = new Promise<void>((resolve) => {
322 const dispose = registry.subscribe(atom, (result) => {
323 if (result._tag === "Initial" || (suspendOnWaiting && result.waiting)) {
324 return
325 }
326 setTimeout(dispose, 1000)
327 resolve()
328 map.delete(atom)
329 })
330 })
331 map.set(atom, promise)
332 return promise
333}
334
335function atomResultOrSuspend<A, E>(
336 registry: AtomRegistry.AtomRegistry,
337 atom: Atom.Atom<AsyncResult.AsyncResult<A, E>>,
338 suspendOnWaiting: boolean
339) {
340 const value = useStore(registry, atom)
341 if (value._tag === "Initial" || (suspendOnWaiting && value.waiting)) {
342 throw atomToPromise(registry, atom, suspendOnWaiting)
343 }
344 return value
345}
346
347/**
348 * Reads an `AsyncResult` atom through React Suspense, suspending while the
349 * result is initial or configured as waiting.
350 *
351 * **When to use**
352 *
353 * Use when a React component should render only after an `AsyncResult` atom has
354 * left its initial state, with loading delegated to a Suspense boundary.
355 *
356 * **Details**
357 *
358 * `suspendOnWaiting` defaults to `false`. When `includeFailure` is `true`, a
359 * failure result is returned instead of being thrown.
360 *
361 * **Gotchas**
362 *
363 * Without `includeFailure`, failure results are thrown with
364 * `Cause.squash(result.cause)`, so callers need an error boundary for failures.
365 *
366 * @see {@link useAtomValue} for reading the raw `AsyncResult` value without Suspense
367 *
368 * @category hooks
369 * @since 4.0.0
370 */
371export const useAtomSuspense = <A, E, const IncludeFailure extends boolean = false>(
372 atom: Atom.Atom<AsyncResult.AsyncResult<A, E>>,
373 options?: {
374 readonly suspendOnWaiting?: boolean | undefined
375 readonly includeFailure?: IncludeFailure | undefined
376 }
377): AsyncResult.Success<A, E> | (IncludeFailure extends true ? AsyncResult.Failure<A, E> : never) => {
378 const registry = React.useContext(RegistryContext)
379 const result = atomResultOrSuspend(registry, atom, options?.suspendOnWaiting ?? false)
380 if (result._tag === "Failure" && !options?.includeFailure) {
381 throw Cause.squash(result.cause)
382 }
383 return result as any
384}
385
386/**
387 * Subscribes a callback to an atom in the current React registry for the
388 * component lifetime.
389 *
390 * **When to use**
391 *
392 * Use when a React component needs to run a callback for atom changes without
393 * reading the atom value during render.
394 *
395 * **Details**
396 *
397 * The subscription is installed in a React effect and cleaned up on unmount or
398 * dependency change. When `options.immediate` is enabled, the callback receives
399 * the current value when the effect subscribes.
400 *
401 * @see {@link useAtomValue} for reading an atom value during render instead of running a callback
402 *
403 * @category hooks
404 * @since 4.0.0
405 */
406export const useAtomSubscribe = <A>(
407 atom: Atom.Atom<A>,
408 f: (_: A) => void,
409 options?: { readonly immediate?: boolean }
410): void => {
411 const registry = React.useContext(RegistryContext)
412 React.useEffect(
413 () => registry.subscribe(atom, f, options),
414 [registry, atom, f, options?.immediate]
415 )
416}
417
418/**
419 * Subscribes to an atom ref and returns its latest value.
420 *
421 * **When to use**
422 *
423 * Use when a React component should render from an `AtomRef.ReadonlyRef`
424 * directly instead of reading an atom through the current registry.
425 *
426 * **Details**
427 *
428 * The hook subscribes with `ref.subscribe`, triggers re-renders through React
429 * state, and returns the current `ref.value`.
430 *
431 * @see {@link useAtomValue} for reading an `Atom` from the current registry
432 * @see {@link useAtomRefPropValue} for reading a property ref value
433 *
434 * @category hooks
435 * @since 4.0.0
436 */
437export const useAtomRef = <A>(ref: AtomRef.ReadonlyRef<A>): A => {
438 const [, forceUpdate] = React.useReducer((n) => n + 1, 0)
439 React.useEffect(() => ref.subscribe(forceUpdate), [ref])
440 return ref.value
441}
442
443/**
444 * Returns a memoized atom ref for a property of another atom ref.
445 *
446 * **When to use**
447 *
448 * Use to derive an `AtomRef` for one property of an object-shaped atom ref.
449 *
450 * **Details**
451 *
452 * The hook memoizes `ref.prop(prop)` for the `[ref, prop]` dependency pair and
453 * returns the property ref so callers can read, set, update, or subscribe to
454 * that nested property.
455 *
456 * @see {@link useAtomRef} for subscribing to an atom ref value
457 * @see {@link useAtomRefPropValue} for subscribing directly to a property value
458 *
459 * @category hooks
460 * @since 4.0.0
461 */
462export const useAtomRefProp = <A, K extends keyof A>(ref: AtomRef.AtomRef<A>, prop: K): AtomRef.AtomRef<A[K]> =>
463 React.useMemo(() => ref.prop(prop), [ref, prop])
464
465/**
466 * Subscribes to a property ref derived from an atom ref and returns its current
467 * value.
468 *
469 * **When to use**
470 *
471 * Use when a React component needs only the current value of one property from
472 * an object-shaped `AtomRef`.
473 *
474 * **Details**
475 *
476 * The hook composes `useAtomRefProp(ref, prop)` with `useAtomRef`, so the
477 * property ref is memoized for the `[ref, prop]` pair and then subscribed
478 * through `ref.subscribe`.
479 *
480 * @see {@link useAtomRefProp} for returning the property ref directly
481 * @see {@link useAtomRef} for subscribing to a whole atom ref value
482 *
483 * @category hooks
484 * @since 4.0.0
485 */
486export const useAtomRefPropValue = <A, K extends keyof A>(ref: AtomRef.AtomRef<A>, prop: K): A[K] =>
487 useAtomRef(useAtomRefProp(ref, prop))