| 1 | /** |
| 2 | * React context and provider for the Atom registry used by Effect Atom hooks. |
| 3 | * The registry stores atom values, schedules update work, and cleans up unused |
| 4 | * atoms. Sharing one registry through React context lets components in the same |
| 5 | * subtree read and write the same atom state. |
| 6 | * |
| 7 | * @since 4.0.0 |
| 8 | */ |
| 9 | "use client" |
| 10 | |
| 11 | import type * as Atom from "effect/reactivity/Atom" |
| 12 | import * as AtomRegistry from "effect/reactivity/AtomRegistry" |
| 13 | import * as React from "react" |
| 14 | import * as Scheduler from "scheduler" |
| 15 | |
| 16 | /** |
| 17 | * Schedules Atom registry work with React's scheduler at low priority and |
| 18 | * returns a cancellation function for the scheduled task. |
| 19 | * |
| 20 | * @stability unstable |
| 21 | * @category context |
| 22 | * @since 4.0.0 |
| 23 | */ |
| 24 | export function scheduleTask(f: () => void): () => void { |
| 25 | const node = Scheduler.unstable_scheduleCallback(Scheduler.unstable_LowPriority, f) |
| 26 | return () => Scheduler.unstable_cancelCallback(node) |
| 27 | } |
| 28 | |
| 29 | /** |
| 30 | * Provides a React context that supplies the `AtomRegistry` used by Atom hooks and |
| 31 | * hydration helpers, defaulting to a standalone registry when no provider is |
| 32 | * present. |
| 33 | * |
| 34 | * **When to use** |
| 35 | * |
| 36 | * Use to supply an existing `AtomRegistry` through React context when hooks or |
| 37 | * hydration helpers need to share registry state that is managed outside |
| 38 | * `RegistryProvider`. |
| 39 | * |
| 40 | * @see {@link RegistryProvider} for creating and providing a registry for a React subtree |
| 41 | * |
| 42 | * @category context |
| 43 | * @since 4.0.0 |
| 44 | */ |
| 45 | export const RegistryContext = React.createContext<AtomRegistry.AtomRegistry>(AtomRegistry.make({ |
| 46 | scheduleTask, |
| 47 | defaultIdleTTL: 400 |
| 48 | })) |
| 49 | |
| 50 | /** |
| 51 | * Provides a stable `AtomRegistry` to a React subtree, optionally seeding |
| 52 | * initial atom values and overriding registry scheduling or idle settings. |
| 53 | * |
| 54 | * **When to use** |
| 55 | * |
| 56 | * Use to scope atom state, scheduling, and idle cleanup to a React subtree. |
| 57 | * |
| 58 | * **Details** |
| 59 | * |
| 60 | * The provider creates one `AtomRegistry` with `AtomRegistry.make`, passes it |
| 61 | * through `RegistryContext.Provider`, and forwards `initialValues`, |
| 62 | * `scheduleTask`, `timeoutResolution`, and `defaultIdleTTL` only when that |
| 63 | * registry is created. |
| 64 | * |
| 65 | * **Gotchas** |
| 66 | * |
| 67 | * Option changes after the first render do not rebuild the registry. When the |
| 68 | * provider unmounts, registry disposal is delayed briefly and canceled if the |
| 69 | * provider remounts before the timeout fires. |
| 70 | * |
| 71 | * @see {@link RegistryContext} for the React context supplied by this provider |
| 72 | * |
| 73 | * @category context |
| 74 | * @since 4.0.0 |
| 75 | */ |
| 76 | export const RegistryProvider = (options: { |
| 77 | readonly children?: React.ReactNode | undefined |
| 78 | readonly initialValues?: Iterable<readonly [Atom.Atom<any>, any]> | undefined |
| 79 | readonly scheduleTask?: ((f: () => void) => () => void) | undefined |
| 80 | readonly timeoutResolution?: number | undefined |
| 81 | readonly defaultIdleTTL?: number | undefined |
| 82 | }) => { |
| 83 | const ref = React.useRef<{ |
| 84 | readonly registry: AtomRegistry.AtomRegistry |
| 85 | timeout?: number | undefined |
| 86 | }>(null) |
| 87 | if (ref.current === null) { |
| 88 | ref.current = { |
| 89 | registry: AtomRegistry.make({ |
| 90 | scheduleTask: options.scheduleTask ?? scheduleTask, |
| 91 | initialValues: options.initialValues, |
| 92 | timeoutResolution: options.timeoutResolution, |
| 93 | defaultIdleTTL: options.defaultIdleTTL |
| 94 | }) |
| 95 | } |
| 96 | } |
| 97 | React.useEffect(() => { |
| 98 | if (ref.current?.timeout !== undefined) { |
| 99 | clearTimeout(ref.current.timeout) |
| 100 | } |
| 101 | return () => { |
| 102 | ref.current!.timeout = setTimeout(() => { |
| 103 | ref.current?.registry.dispose() |
| 104 | ref.current = null |
| 105 | }, 500) as any |
| 106 | } |
| 107 | }, [ref]) |
| 108 | return React.createElement(RegistryContext.Provider, { value: ref.current.registry }, options?.children) |
| 109 | } |