/** * React helpers for creating Atom instances that belong to one component * subtree. `make` returns a scoped atom with a provider, context, and `use` * accessor. Each provider creates its own Atom once, so different subtrees can * use the same scoped atom definition without sharing state. * * @since 4.0.0 */ "use client" import type * as Atom from "effect/unstable/reactivity/Atom" import * as React from "react" /** * Literal type used as the `ScopedAtom` type identifier. * * **Details** * * Used as the computed property key and marker value stored on `ScopedAtom` * objects. * * @category type IDs * @since 4.0.0 */ export type TypeId = "~@effect/atom-react/ScopedAtom" /** * Type identifier for ScopedAtom. * * **Details** * * Used as the computed property key and marker value stored on `ScopedAtom` * objects. * * @category type IDs * @since 4.0.0 */ export const TypeId: TypeId = "~@effect/atom-react/ScopedAtom" /** * Scoped Atom interface with a provider-backed instance. * * **Example** (Providing and reading a scoped atom) * * ```ts * import { make, useAtomValue } from "@effect/atom-react" * import { Atom } from "effect/unstable/reactivity" * import * as React from "react" * * const Counter = make(() => Atom.make(0)) * * function View() { * const atom = Counter.use() * const value = useAtomValue(atom) * return React.createElement("div", null, value) * } * * export function App() { * return React.createElement(Counter.Provider, null, React.createElement(View)) * } * ``` * * @category models * @since 4.0.0 */ export interface ScopedAtom, Input = never> { readonly [TypeId]: TypeId use(): A Provider: [Input] extends [never] ? React.FC<{ readonly children?: React.ReactNode | undefined }> : React.FC<{ readonly children?: React.ReactNode | undefined; readonly value: Input }> Context: React.Context } /** * Creates a ScopedAtom from a factory function. * * **When to use** * * Use to create an atom instance that is owned by a React provider and scoped * to a component subtree. * * **Details** * * The returned scoped atom includes a `Provider`, `Context`, and `use` * accessor. The provider creates the atom once for its lifetime, passing the * `value` prop to the factory when the scoped atom expects input. * * **Gotchas** * * `use` must run under the matching provider. Changing the provider `value` * prop after mount does not recreate the atom. * * **Example** (Creating a scoped atom with input) * * ```ts * import { make, useAtomValue } from "@effect/atom-react" * import { Atom } from "effect/unstable/reactivity" * import * as React from "react" * * const User = make((name: string) => Atom.make(name)) * * function UserName() { * const atom = User.use() * const value = useAtomValue(atom) * return React.createElement("span", null, value) * } * * export function App() { * return React.createElement( * User.Provider, * { value: "Ada" }, * React.createElement(UserName) * ) * } * ``` * * @category constructors * @since 4.0.0 */ export const make = , Input = never>( f: (() => A) | ((input: Input) => A) ): ScopedAtom => { const Context = React.createContext(undefined as unknown as A) const use = (): A => { const atom = React.useContext(Context) if (atom === undefined) { throw new Error("ScopedAtom used outside of its Provider") } return atom } const Provider: React.FC<{ readonly children?: React.ReactNode | undefined; readonly value?: Input }> = (props) => { const atom = React.useRef(null) if (atom.current === null) { if ("value" in props) { atom.current = (f as (input: Input) => A)(props.value as Input) } else { atom.current = (f as () => A)() } } return React.createElement(Context.Provider, { value: atom.current }, props.children) } return { [TypeId]: TypeId, use, Provider: Provider as any, Context } }