Merge commit '36c66dd290d3ce6eb1ccd310d0c658d4a32bb8eb' as 'repos/effect'

This commit is contained in:
-Puter
2026-07-19 03:25:10 +05:30
parent dd1071cc10
commit 2daf979036
2214 changed files with 673090 additions and 0 deletions

View File

@@ -0,0 +1,476 @@
/**
* 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<A> {
readonly subscribe: (f: () => void) => () => void
readonly snapshot: () => A
readonly getServerSnapshot: () => A
}
const storeRegistry = new WeakMap<AtomRegistry.AtomRegistry, WeakMap<Atom.Atom<any>, AtomStore<any>>>()
function makeStore<A>(registry: AtomRegistry.AtomRegistry, atom: Atom.Atom<A>): AtomStore<A> {
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<A> = {
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<A>(registry: AtomRegistry.AtomRegistry, atom: Atom.Atom<A>): A {
const store = makeStore(registry, atom)
return React.useSyncExternalStore(store.subscribe, store.snapshot, store.getServerSnapshot)
}
const initialValuesSet = new WeakMap<AtomRegistry.AtomRegistry, WeakSet<Atom.Atom<any>>>()
/**
* 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<readonly [Atom.Atom<any>, 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: {
<A>(atom: Atom.Atom<A>): A
<A, B>(atom: Atom.Atom<A>, f: (_: A) => B): B
} = <A>(atom: Atom.Atom<A>, 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<A>(registry: AtomRegistry.AtomRegistry, atom: Atom.Atom<A>): void {
React.useEffect(() => registry.mount(atom), [atom, registry])
}
function setAtom<R, W, Mode extends "value" | "promise" | "promiseExit" = never>(
registry: AtomRegistry.AtomRegistry,
atom: Atom.Writable<R, W>,
options?: {
readonly mode?: ([R] extends [AsyncResult.AsyncResult<any, any>] ? Mode : "value") | undefined
}
): "promise" extends Mode ? (
(value: W) => Promise<AsyncResult.AsyncResult.Success<R>>
) :
"promiseExit" extends Mode ? (
(value: W) => Promise<Exit.Exit<AsyncResult.AsyncResult.Success<R>, AsyncResult.AsyncResult.Failure<R>>>
) :
((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<AsyncResult.AsyncResult<any, any>>, {
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 = <A, E>(exit: Exit.Exit<A, E>): 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 = <A>(atom: Atom.Atom<A>): 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<R, W>,
options?: {
readonly mode?: ([R] extends [AsyncResult.AsyncResult<any, any>] ? Mode : "value") | undefined
}
): "promise" extends Mode ? (
(value: W) => Promise<AsyncResult.AsyncResult.Success<R>>
) :
"promiseExit" extends Mode ? (
(value: W) => Promise<Exit.Exit<AsyncResult.AsyncResult.Success<R>, AsyncResult.AsyncResult.Failure<R>>>
) :
((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 = <A>(atom: Atom.Atom<A>): () => 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 = <R, W, const Mode extends "value" | "promise" | "promiseExit" = never>(
atom: Atom.Writable<R, W>,
options?: {
readonly mode?: ([R] extends [AsyncResult.AsyncResult<any, any>] ? Mode : "value") | undefined
}
): readonly [
value: R,
write: "promise" extends Mode ? (
(value: W) => Promise<AsyncResult.AsyncResult.Success<R>>
) :
"promiseExit" extends Mode ? (
(value: W) => Promise<Exit.Exit<AsyncResult.AsyncResult.Success<R>, AsyncResult.AsyncResult.Failure<R>>>
) :
((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<Atom.Atom<any>, Promise<void>>(),
default: new Map<Atom.Atom<any>, Promise<void>>()
}
function atomToPromise<A, E>(
registry: AtomRegistry.AtomRegistry,
atom: Atom.Atom<AsyncResult.AsyncResult<A, E>>,
suspendOnWaiting: boolean
) {
const map = suspendOnWaiting ? atomPromiseMap.suspendOnWaiting : atomPromiseMap.default
let promise = map.get(atom)
if (promise !== undefined) {
return promise
}
promise = new Promise<void>((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<A, E>(
registry: AtomRegistry.AtomRegistry,
atom: Atom.Atom<AsyncResult.AsyncResult<A, E>>,
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 = <A, E, const IncludeFailure extends boolean = false>(
atom: Atom.Atom<AsyncResult.AsyncResult<A, E>>,
options?: {
readonly suspendOnWaiting?: boolean | undefined
readonly includeFailure?: IncludeFailure | undefined
}
): AsyncResult.Success<A, E> | (IncludeFailure extends true ? AsyncResult.Failure<A, E> : 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 = <A>(
atom: Atom.Atom<A>,
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 = <A>(ref: AtomRef.ReadonlyRef<A>): 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 = <A, K extends keyof A>(ref: AtomRef.AtomRef<A>, prop: K): AtomRef.AtomRef<A[K]> =>
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 = <A, K extends keyof A>(ref: AtomRef.AtomRef<A>, prop: K): A[K] =>
useAtomRef(useAtomRefProp(ref, prop))

View File

@@ -0,0 +1,109 @@
/**
* React helpers for applying dehydrated Effect Atom state to a React subtree.
* The `HydrationBoundary` component reads the nearest `RegistryContext`,
* hydrates new Atom values before children render, and delays updates for
* existing Atom values until after commit so React transitions do not update
* the current UI too early.
*
* @since 4.0.0
*/
"use client"
import * as Hydration from "effect/unstable/reactivity/Hydration"
import * as React from "react"
import { RegistryContext } from "./RegistryContext.ts"
/**
* Props for a boundary that applies dehydrated Atom values to the nearest
* {@link RegistryContext} while rendering its children.
*
* @category components
* @since 4.0.0
*/
export interface HydrationBoundaryProps {
state?: Iterable<Hydration.DehydratedAtom>
children?: React.ReactNode
}
/**
* Provides a React hydration boundary that loads dehydrated Atom values into
* the current Atom registry.
*
* **When to use**
*
* Use to apply dehydrated Atom state to a React subtree that reads from the
* nearest `RegistryContext`.
*
* **Details**
*
* New Atom values are hydrated during render so descendants can read them
* immediately, while values for existing Atoms are deferred until after commit
* so transition data does not update the current UI before React accepts it.
*
* @see {@link Hydration.dehydrate} for producing dehydrated Atom state
* @see {@link Hydration.hydrate} for lower-level non-React hydration
*
* @category components
* @since 4.0.0
*/
export const HydrationBoundary: React.FC<HydrationBoundaryProps> = ({
children,
state
}) => {
const registry = React.useContext(RegistryContext)
// This useMemo is for performance reasons only, everything inside it must
// be safe to run in every render and code here should be read as "in render".
//
// This code needs to happen during the render phase, because after initial
// SSR, hydration needs to happen _before_ children render. Also, if hydrating
// during a transition, we want to hydrate as much as is safe in render so
// we can prerender as much as possible.
//
// For any Atom values that already exist in the registry, we want to hold back on
// hydrating until _after_ the render phase. The reason for this is that during
// transitions, we don't want the existing Atom values and subscribers to update to
// the new data on the current page, only _after_ the transition is committed.
// If the transition is aborted, we will have hydrated any _new_ Atom values, but
// we throw away the fresh data for any existing ones to avoid unexpectedly
// updating the UI.
const hydrationQueue: Array<Hydration.DehydratedAtomValue> | undefined = React.useMemo(() => {
if (state) {
const dehydratedAtoms = Array.from(state) as Array<Hydration.DehydratedAtomValue>
const nodes = registry.getNodes()
const newDehydratedAtoms: Array<Hydration.DehydratedAtomValue> = []
const existingDehydratedAtoms: Array<Hydration.DehydratedAtomValue> = []
for (const dehydratedAtom of dehydratedAtoms) {
const existingNode = nodes.get(dehydratedAtom.key)
if (!existingNode) {
// This is a new Atom value, safe to hydrate immediately
newDehydratedAtoms.push(dehydratedAtom)
} else {
// This Atom value already exists, queue it for later hydration
existingDehydratedAtoms.push(dehydratedAtom)
}
}
if (newDehydratedAtoms.length > 0) {
// It's actually fine to call this with state that already exists
// in the registry, or is older. hydrate() is idempotent.
Hydration.hydrate(registry, newDehydratedAtoms)
}
if (existingDehydratedAtoms.length > 0) {
return existingDehydratedAtoms
}
}
return undefined
}, [registry, state])
React.useEffect(() => {
if (hydrationQueue) {
Hydration.hydrate(registry, hydrationQueue)
}
}, [registry, hydrationQueue])
return React.createElement(React.Fragment, {}, children)
}

View File

@@ -0,0 +1,108 @@
/**
* React context and provider for the Atom registry used by Effect Atom hooks.
* The registry stores atom values, schedules update work, and cleans up unused
* atoms. Sharing one registry through React context lets components in the same
* subtree read and write the same atom state.
*
* @since 4.0.0
*/
"use client"
import type * as Atom from "effect/unstable/reactivity/Atom"
import * as AtomRegistry from "effect/unstable/reactivity/AtomRegistry"
import * as React from "react"
import * as Scheduler from "scheduler"
/**
* Schedules Atom registry work with React's scheduler at low priority and
* returns a cancellation function for the scheduled task.
*
* @category context
* @since 4.0.0
*/
export function scheduleTask(f: () => void): () => void {
const node = Scheduler.unstable_scheduleCallback(Scheduler.unstable_LowPriority, f)
return () => Scheduler.unstable_cancelCallback(node)
}
/**
* Provides a React context that supplies the `AtomRegistry` used by Atom hooks and
* hydration helpers, defaulting to a standalone registry when no provider is
* present.
*
* **When to use**
*
* Use to supply an existing `AtomRegistry` through React context when hooks or
* hydration helpers need to share registry state that is managed outside
* `RegistryProvider`.
*
* @see {@link RegistryProvider} for creating and providing a registry for a React subtree
*
* @category context
* @since 4.0.0
*/
export const RegistryContext = React.createContext<AtomRegistry.AtomRegistry>(AtomRegistry.make({
scheduleTask,
defaultIdleTTL: 400
}))
/**
* Provides a stable `AtomRegistry` to a React subtree, optionally seeding
* initial atom values and overriding registry scheduling or idle settings.
*
* **When to use**
*
* Use to scope atom state, scheduling, and idle cleanup to a React subtree.
*
* **Details**
*
* The provider creates one `AtomRegistry` with `AtomRegistry.make`, passes it
* through `RegistryContext.Provider`, and forwards `initialValues`,
* `scheduleTask`, `timeoutResolution`, and `defaultIdleTTL` only when that
* registry is created.
*
* **Gotchas**
*
* Option changes after the first render do not rebuild the registry. When the
* provider unmounts, registry disposal is delayed briefly and canceled if the
* provider remounts before the timeout fires.
*
* @see {@link RegistryContext} for the React context supplied by this provider
*
* @category context
* @since 4.0.0
*/
export const RegistryProvider = (options: {
readonly children?: React.ReactNode | undefined
readonly initialValues?: Iterable<readonly [Atom.Atom<any>, any]> | undefined
readonly scheduleTask?: ((f: () => void) => () => void) | undefined
readonly timeoutResolution?: number | undefined
readonly defaultIdleTTL?: number | undefined
}) => {
const ref = React.useRef<{
readonly registry: AtomRegistry.AtomRegistry
timeout?: number | undefined
}>(null)
if (ref.current === null) {
ref.current = {
registry: AtomRegistry.make({
scheduleTask: options.scheduleTask ?? scheduleTask,
initialValues: options.initialValues,
timeoutResolution: options.timeoutResolution,
defaultIdleTTL: options.defaultIdleTTL
})
}
}
React.useEffect(() => {
if (ref.current?.timeout !== undefined) {
clearTimeout(ref.current.timeout)
}
return () => {
ref.current!.timeout = setTimeout(() => {
ref.current?.registry.dispose()
ref.current = null
}, 500) as any
}
}, [ref])
return React.createElement(RegistryContext.Provider, { value: ref.current.registry }, options?.children)
}

View File

@@ -0,0 +1,151 @@
/**
* 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<A extends Atom.Atom<any>, 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<A>
}
/**
* 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 = <A extends Atom.Atom<any>, Input = never>(
f: (() => A) | ((input: Input) => A)
): ScopedAtom<A, Input> => {
const Context = React.createContext<A>(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<A | null>(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
}
}

View File

@@ -0,0 +1,23 @@
/**
* @since 4.0.0
*/
/**
* @since 4.0.0
*/
export * from "./Hooks.ts"
/**
* @since 4.0.0
*/
export * from "./RegistryContext.ts"
/**
* @since 4.0.0
*/
export * from "./ReactHydration.ts"
/**
* @since 4.0.0
*/
export * from "./ScopedAtom.ts"