Merge commit '36c66dd290d3ce6eb1ccd310d0c658d4a32bb8eb' as 'repos/effect'
This commit is contained in:
476
repos/effect/packages/atom/react/src/Hooks.ts
Normal file
476
repos/effect/packages/atom/react/src/Hooks.ts
Normal 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))
|
||||
109
repos/effect/packages/atom/react/src/ReactHydration.ts
Normal file
109
repos/effect/packages/atom/react/src/ReactHydration.ts
Normal 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)
|
||||
}
|
||||
108
repos/effect/packages/atom/react/src/RegistryContext.ts
Normal file
108
repos/effect/packages/atom/react/src/RegistryContext.ts
Normal 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)
|
||||
}
|
||||
151
repos/effect/packages/atom/react/src/ScopedAtom.ts
Normal file
151
repos/effect/packages/atom/react/src/ScopedAtom.ts
Normal 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
|
||||
}
|
||||
}
|
||||
23
repos/effect/packages/atom/react/src/index.ts
Normal file
23
repos/effect/packages/atom/react/src/index.ts
Normal 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"
|
||||
Reference in New Issue
Block a user