/** * React hooks for working with Effect atoms from components. The hooks read, * write, mount, refresh, and subscribe to atoms from `RegistryContext`, handle * `AsyncResult` atoms with React Suspense, and expose helpers for reading and * deriving `AtomRef` values. * * @since 4.0.0 */ "use client" import * as Cause from "effect/Cause" import * as Effect from "effect/Effect" import * as Exit from "effect/Exit" import type * as AsyncResult from "effect/unstable/reactivity/AsyncResult" import * as Atom from "effect/unstable/reactivity/Atom" import type * as AtomRef from "effect/unstable/reactivity/AtomRef" import * as AtomRegistry from "effect/unstable/reactivity/AtomRegistry" import * as React from "react" import { RegistryContext } from "./RegistryContext.ts" interface AtomStore { readonly subscribe: (f: () => void) => () => void readonly snapshot: () => A readonly getServerSnapshot: () => A } const storeRegistry = new WeakMap, AtomStore>>() function makeStore(registry: AtomRegistry.AtomRegistry, 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: AtomRegistry.AtomRegistry, atom: Atom.Atom): A { const store = makeStore(registry, atom) return React.useSyncExternalStore(store.subscribe, store.snapshot, store.getServerSnapshot) } const initialValuesSet = new WeakMap>>() /** * Seeds initial atom values in the current React atom registry. * * **When to use** * * Use to seed atom values from a React component after the current registry * already exists. * * **Gotchas** * * Each atom is initialized at most once for a given registry by this hook, so * later calls for the same atom in that registry are ignored. * * @category hooks * @since 4.0.0 */ 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) } } } /** * Subscribes to an atom in the current React registry and returns its current * value, optionally mapped through a selector. * * **When to use** * * Use when a React component needs to render from an atom value without also * returning a setter. * * **Details** * * When a selector is provided, the hook maps the atom before subscribing so the * component reads the selected value from the current `RegistryContext`. * * @see {@link useAtom} for reading and updating a writable atom from one component * @see {@link useAtomRef} for reading an `AtomRef` directly * * @category hooks * @since 4.0.0 */ 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: AtomRegistry.AtomRegistry, atom: Atom.Atom): void { React.useEffect(() => registry.mount(atom), [atom, registry]) } function setAtom( registry: AtomRegistry.AtomRegistry, atom: Atom.Writable, options?: { readonly mode?: ([R] extends [AsyncResult.AsyncResult] ? Mode : "value") | undefined } ): "promise" extends Mode ? ( (value: W) => Promise> ) : "promiseExit" extends Mode ? ( (value: W) => Promise, AsyncResult.AsyncResult.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( AtomRegistry.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) } /** * Mounts an atom in the current React registry for the lifetime of the * component. * * **When to use** * * Use to keep an atom mounted from a React component without reading, writing, * or refreshing it. * * **Details** * * The hook uses the current `RegistryContext` and releases the mount through * React effect cleanup when the component unmounts or when the registry or atom * dependency changes. * * @see {@link useAtomSet} for mounting a writable atom while returning a setter * @see {@link useAtomRefresh} for mounting an atom while returning a refresh callback * * @category hooks * @since 4.0.0 */ export const useAtomMount = (atom: Atom.Atom): void => { const registry = React.useContext(RegistryContext) mountAtom(registry, atom) } /** * Mounts a writable atom and returns a setter without subscribing to its value. * * **When to use** * * Use when a React component needs to update a writable atom without rendering * from that atom's value. * * **Details** * * The hook mounts the atom and returns a setter. In value mode the setter * accepts a write value or updater function; for `AsyncResult` atoms, `promise` * and `promiseExit` modes return a promise for the success value or full `Exit`. * * @see {@link useAtom} for reading and updating the same writable atom * * @category hooks * @since 4.0.0 */ export const useAtomSet = < R, W, Mode extends "value" | "promise" | "promiseExit" = never >( atom: Atom.Writable, options?: { readonly mode?: ([R] extends [AsyncResult.AsyncResult] ? Mode : "value") | undefined } ): "promise" extends Mode ? ( (value: W) => Promise> ) : "promiseExit" extends Mode ? ( (value: W) => Promise, AsyncResult.AsyncResult.Failure>> ) : ((value: W | ((value: R) => W)) => void) => { const registry = React.useContext(RegistryContext) mountAtom(registry, atom) return setAtom(registry, atom, options) } /** * Mounts an atom and returns a callback that refreshes it in the current React * registry. * * **When to use** * * Use to expose a React callback that requests a refresh for an atom without * reading or writing its value. * * **Details** * * The hook uses the current `RegistryContext`, mounts the atom for the * component lifetime, and returns a callback that calls `registry.refresh`. * * @see {@link useAtomMount} for mounting an atom without returning a refresh callback * * @category hooks * @since 4.0.0 */ export const useAtomRefresh = (atom: Atom.Atom): () => void => { const registry = React.useContext(RegistryContext) mountAtom(registry, atom) return React.useCallback(() => { registry.refresh(atom) }, [registry, atom]) } /** * Subscribes to a writable atom and returns its current value together with a * setter for updating it. * * **When to use** * * Use when a React component needs both to render the current value of a * writable atom and update it from the same component. * * @see {@link useAtomValue} for subscribing to an atom without a setter * @see {@link useAtomSet} for updating a writable atom without subscribing to its value * * @category hooks * @since 4.0.0 */ export const useAtom = ( atom: Atom.Writable, options?: { readonly mode?: ([R] extends [AsyncResult.AsyncResult] ? Mode : "value") | undefined } ): readonly [ value: R, write: "promise" extends Mode ? ( (value: W) => Promise> ) : "promiseExit" extends Mode ? ( (value: W) => Promise, AsyncResult.AsyncResult.Failure>> ) : ((value: W | ((value: R) => W)) => void) ] => { const registry = React.useContext(RegistryContext) return [ useStore(registry, atom), setAtom(registry, atom, options) ] as const } const atomPromiseMap = { suspendOnWaiting: new Map, Promise>(), default: new Map, Promise>() } function atomToPromise( registry: AtomRegistry.AtomRegistry, 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: AtomRegistry.AtomRegistry, atom: Atom.Atom>, suspendOnWaiting: boolean ) { const value = useStore(registry, atom) if (value._tag === "Initial" || (suspendOnWaiting && value.waiting)) { throw atomToPromise(registry, atom, suspendOnWaiting) } return value } /** * Reads an `AsyncResult` atom through React Suspense, suspending while the * result is initial or configured as waiting. * * **When to use** * * Use when a React component should render only after an `AsyncResult` atom has * left its initial state, with loading delegated to a Suspense boundary. * * **Details** * * `suspendOnWaiting` defaults to `false`. When `includeFailure` is `true`, a * failure result is returned instead of being thrown. * * **Gotchas** * * Without `includeFailure`, failure results are thrown with * `Cause.squash(result.cause)`, so callers need an error boundary for failures. * * @see {@link useAtomValue} for reading the raw `AsyncResult` value without Suspense * * @category hooks * @since 4.0.0 */ export const useAtomSuspense = ( atom: Atom.Atom>, options?: { readonly suspendOnWaiting?: boolean | undefined readonly includeFailure?: IncludeFailure | undefined } ): AsyncResult.Success | (IncludeFailure extends true ? AsyncResult.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 } /** * Subscribes a callback to an atom in the current React registry for the * component lifetime. * * **When to use** * * Use when a React component needs to run a callback for atom changes without * reading the atom value during render. * * **Details** * * The subscription is installed in a React effect and cleaned up on unmount or * dependency change. When `options.immediate` is enabled, the callback receives * the current value when the effect subscribes. * * @see {@link useAtomValue} for reading an atom value during render instead of running a callback * * @category hooks * @since 4.0.0 */ 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] ) } /** * Subscribes to an atom ref and returns its latest value. * * **When to use** * * Use when a React component should render from an `AtomRef.ReadonlyRef` * directly instead of reading an atom through the current registry. * * **Details** * * The hook subscribes with `ref.subscribe`, triggers re-renders through React * state, and returns the current `ref.value`. * * @see {@link useAtomValue} for reading an `Atom` from the current registry * @see {@link useAtomRefPropValue} for reading a property ref value * * @category hooks * @since 4.0.0 */ export const useAtomRef = (ref: AtomRef.ReadonlyRef): A => { const [, setValue] = React.useState(ref.value) React.useEffect(() => ref.subscribe(setValue), [ref]) return ref.value } /** * Returns a memoized atom ref for a property of another atom ref. * * **When to use** * * Use to derive an `AtomRef` for one property of an object-shaped atom ref. * * **Details** * * The hook memoizes `ref.prop(prop)` for the `[ref, prop]` dependency pair and * returns the property ref so callers can read, set, update, or subscribe to * that nested property. * * @see {@link useAtomRef} for subscribing to an atom ref value * @see {@link useAtomRefPropValue} for subscribing directly to a property value * * @category hooks * @since 4.0.0 */ export const useAtomRefProp = (ref: AtomRef.AtomRef, prop: K): AtomRef.AtomRef => React.useMemo(() => ref.prop(prop), [ref, prop]) /** * Subscribes to a property ref derived from an atom ref and returns its current * value. * * **When to use** * * Use when a React component needs only the current value of one property from * an object-shaped `AtomRef`. * * **Details** * * The hook composes `useAtomRefProp(ref, prop)` with `useAtomRef`, so the * property ref is memoized for the `[ref, prop]` pair and then subscribed * through `ref.subscribe`. * * @see {@link useAtomRefProp} for returning the property ref directly * @see {@link useAtomRef} for subscribing to a whole atom ref value * * @category hooks * @since 4.0.0 */ export const useAtomRefPropValue = (ref: AtomRef.AtomRef, prop: K): A[K] => useAtomRef(useAtomRefProp(ref, prop))