/** * @since 1.0.0 */ "use client" import * as Atom from "@effect-atom/atom/Atom" import type * as AtomRef from "@effect-atom/atom/AtomRef" import * as Registry from "@effect-atom/atom/Registry" import type * as Result from "@effect-atom/atom/Result" import { Effect } from "effect" import * as Cause from "effect/Cause" import * as Exit from "effect/Exit" import { globalValue } from "effect/GlobalValue" import * as React from "react" import { RegistryContext } from "./RegistryContext.js" interface AtomStore { readonly subscribe: (f: () => void) => () => void readonly snapshot: () => A readonly getServerSnapshot: () => A } const storeRegistry = globalValue( "@effect-atom/atom-react/storeRegistry", () => new WeakMap, AtomStore>>() ) function makeStore(registry: Registry.Registry, atom: Atom.Atom): AtomStore { let stores = storeRegistry.get(registry) if (stores === undefined) { stores = new WeakMap() storeRegistry.set(registry, stores) } const store = stores.get(atom) if (store !== undefined) { return store } const newStore: AtomStore = { subscribe(f) { return registry.subscribe(atom, f) }, snapshot() { return registry.get(atom) }, getServerSnapshot() { return Atom.getServerValue(atom, registry) } } stores.set(atom, newStore) return newStore } function useStore(registry: Registry.Registry, atom: Atom.Atom): A { const store = makeStore(registry, atom) return React.useSyncExternalStore(store.subscribe, store.snapshot, store.getServerSnapshot) } const initialValuesSet = globalValue( "@effect-atom/atom-react/initialValuesSet", () => new WeakMap>>() ) /** * @since 1.0.0 * @category hooks */ export const useAtomInitialValues = (initialValues: Iterable, any]>): void => { const registry = React.useContext(RegistryContext) let set = initialValuesSet.get(registry) if (set === undefined) { set = new WeakSet() initialValuesSet.set(registry, set) } for (const [atom, value] of initialValues) { if (!set.has(atom)) { set.add(atom) ;(registry as any).ensureNode(atom).setValue(value) } } } /** * @since 1.0.0 * @category hooks */ export const useAtomValue: { (atom: Atom.Atom): A (atom: Atom.Atom, f: (_: A) => B): B } = (atom: Atom.Atom, f?: (_: A) => A): A => { const registry = React.useContext(RegistryContext) if (f) { const atomB = React.useMemo(() => Atom.map(atom, f), [atom, f]) return useStore(registry, atomB) } return useStore(registry, atom) } function mountAtom(registry: Registry.Registry, atom: Atom.Atom): void { React.useEffect(() => registry.mount(atom), [atom, registry]) } function setAtom( registry: Registry.Registry, atom: Atom.Writable, options?: { readonly mode?: ([R] extends [Result.Result] ? Mode : "value") | undefined } ): "promise" extends Mode ? ( (value: W) => Promise> ) : "promiseExit" extends Mode ? ( (value: W) => Promise, Result.Result.Failure>> ) : ((value: W | ((value: R) => W)) => void) { if (options?.mode === "promise" || options?.mode === "promiseExit") { return React.useCallback((value: W) => { registry.set(atom, value) const promise = Effect.runPromiseExit( Registry.getResult(registry, atom as Atom.Atom>, { suspendOnWaiting: true }) ) return options!.mode === "promise" ? promise.then(flattenExit) : promise }, [registry, atom, options.mode]) as any } return React.useCallback((value: W | ((value: R) => W)) => { registry.set(atom, typeof value === "function" ? (value as any)(registry.get(atom)) : value) }, [registry, atom]) as any } const flattenExit = (exit: Exit.Exit): A => { if (Exit.isSuccess(exit)) return exit.value throw Cause.squash(exit.cause) } /** * @since 1.0.0 * @category hooks */ export const useAtomMount = (atom: Atom.Atom): void => { const registry = React.useContext(RegistryContext) mountAtom(registry, atom) } /** * @since 1.0.0 * @category hooks */ export const useAtomSet = < R, W, Mode extends "value" | "promise" | "promiseExit" = never >( atom: Atom.Writable, options?: { readonly mode?: ([R] extends [Result.Result] ? Mode : "value") | undefined } ): "promise" extends Mode ? ( (value: W) => Promise> ) : "promiseExit" extends Mode ? ( (value: W) => Promise, Result.Result.Failure>> ) : ((value: W | ((value: R) => W)) => void) => { const registry = React.useContext(RegistryContext) mountAtom(registry, atom) return setAtom(registry, atom, options) } /** * @since 1.0.0 * @category hooks */ export const useAtomRefresh = (atom: Atom.Atom): () => void => { const registry = React.useContext(RegistryContext) mountAtom(registry, atom) return React.useCallback(() => { registry.refresh(atom) }, [registry, atom]) } /** * @since 1.0.0 * @category hooks */ export const useAtom = ( atom: Atom.Writable, options?: { readonly mode?: ([R] extends [Result.Result] ? Mode : "value") | undefined } ): readonly [ value: R, write: "promise" extends Mode ? ( (value: W) => Promise> ) : "promiseExit" extends Mode ? ( (value: W) => Promise, Result.Result.Failure>> ) : ((value: W | ((value: R) => W)) => void) ] => { const registry = React.useContext(RegistryContext) return [ useStore(registry, atom), setAtom(registry, atom, options) ] as const } const atomPromiseMap = globalValue( "@effect-atom/atom-react/atomPromiseMap", () => ({ suspendOnWaiting: new Map, Promise>(), default: new Map, Promise>() }) ) function atomToPromise( registry: Registry.Registry, atom: Atom.Atom>, suspendOnWaiting: boolean ) { const map = suspendOnWaiting ? atomPromiseMap.suspendOnWaiting : atomPromiseMap.default let promise = map.get(atom) if (promise !== undefined) { return promise } promise = new Promise((resolve) => { const dispose = registry.subscribe(atom, (result) => { if (result._tag === "Initial" || (suspendOnWaiting && result.waiting)) { return } setTimeout(dispose, 1000) resolve() map.delete(atom) }) }) map.set(atom, promise) return promise } function atomResultOrSuspend( registry: Registry.Registry, atom: Atom.Atom>, suspendOnWaiting: boolean ) { const value = useStore(registry, atom) if (value._tag === "Initial" || (suspendOnWaiting && value.waiting)) { throw atomToPromise(registry, atom, suspendOnWaiting) } return value } /** * @since 1.0.0 * @category hooks */ export const useAtomSuspense = ( atom: Atom.Atom>, options?: { readonly suspendOnWaiting?: boolean | undefined readonly includeFailure?: IncludeFailure | undefined } ): Result.Success | (IncludeFailure extends true ? Result.Failure : never) => { const registry = React.useContext(RegistryContext) const result = atomResultOrSuspend(registry, atom, options?.suspendOnWaiting ?? false) if (result._tag === "Failure" && !options?.includeFailure) { throw Cause.squash(result.cause) } return result as any } /** * @since 1.0.0 * @category hooks */ export const useAtomSubscribe = ( atom: Atom.Atom, f: (_: A) => void, options?: { readonly immediate?: boolean } ): void => { const registry = React.useContext(RegistryContext) React.useEffect( () => registry.subscribe(atom, f, options), [registry, atom, f, options?.immediate] ) } /** * @since 1.0.0 * @category hooks */ export const useAtomRef = (ref: AtomRef.ReadonlyRef): A => { const [, setValue] = React.useState(ref.value) React.useEffect(() => ref.subscribe(setValue), [ref]) return ref.value } /** * @since 1.0.0 * @category hooks */ export const useAtomRefProp = (ref: AtomRef.AtomRef, prop: K): AtomRef.AtomRef => React.useMemo(() => ref.prop(prop), [ref, prop]) /** * @since 1.0.0 * @category hooks */ export const useAtomRefPropValue = (ref: AtomRef.AtomRef, prop: K): A[K] => useAtomRef(useAtomRefProp(ref, prop))