@effect-atom/atom-react 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/Hooks.ts ADDED
@@ -0,0 +1,341 @@
1
+ /**
2
+ * @since 1.0.0
3
+ */
4
+ "use client"
5
+
6
+ import * as Atom from "@effect-atom/atom/Atom"
7
+ import type * as AtomRef from "@effect-atom/atom/AtomRef"
8
+ import * as Registry from "@effect-atom/atom/Registry"
9
+ import type * as Result from "@effect-atom/atom/Result"
10
+ import { Effect } from "effect"
11
+ import * as Cause from "effect/Cause"
12
+ import * as Exit from "effect/Exit"
13
+ import { globalValue } from "effect/GlobalValue"
14
+ import * as React from "react"
15
+ import { RegistryContext } from "./RegistryContext.js"
16
+
17
+ interface AtomStore<A> {
18
+ readonly subscribe: (f: () => void) => () => void
19
+ readonly snapshot: () => A
20
+ readonly getServerSnapshot: () => A
21
+ }
22
+
23
+ const storeRegistry = globalValue(
24
+ "@effect-atom/atom-react/storeRegistry",
25
+ () => new WeakMap<Registry.Registry, WeakMap<Atom.Atom<any>, AtomStore<any>>>()
26
+ )
27
+
28
+ function makeStore<A>(registry: Registry.Registry, atom: Atom.Atom<A>): AtomStore<A> {
29
+ let stores = storeRegistry.get(registry)
30
+ if (stores === undefined) {
31
+ stores = new WeakMap()
32
+ storeRegistry.set(registry, stores)
33
+ }
34
+ const store = stores.get(atom)
35
+ if (store !== undefined) {
36
+ return store
37
+ }
38
+ const newStore: AtomStore<A> = {
39
+ subscribe(f) {
40
+ return registry.subscribe(atom, f)
41
+ },
42
+ snapshot() {
43
+ return registry.get(atom)
44
+ },
45
+ getServerSnapshot() {
46
+ return Atom.getServerValue(atom, registry)
47
+ }
48
+ }
49
+ stores.set(atom, newStore)
50
+ return newStore
51
+ }
52
+
53
+ function useStore<A>(registry: Registry.Registry, atom: Atom.Atom<A>): A {
54
+ const store = makeStore(registry, atom)
55
+
56
+ return React.useSyncExternalStore(store.subscribe, store.snapshot, store.getServerSnapshot)
57
+ }
58
+
59
+ const initialValuesSet = globalValue(
60
+ "@effect-atom/atom-react/initialValuesSet",
61
+ () => new WeakMap<Registry.Registry, WeakSet<Atom.Atom<any>>>()
62
+ )
63
+
64
+ /**
65
+ * @since 1.0.0
66
+ * @category hooks
67
+ */
68
+ export const useAtomInitialValues = (initialValues: Iterable<readonly [Atom.Atom<any>, any]>): void => {
69
+ const registry = React.useContext(RegistryContext)
70
+ let set = initialValuesSet.get(registry)
71
+ if (set === undefined) {
72
+ set = new WeakSet()
73
+ initialValuesSet.set(registry, set)
74
+ }
75
+ for (const [atom, value] of initialValues) {
76
+ if (!set.has(atom)) {
77
+ set.add(atom)
78
+ ;(registry as any).ensureNode(atom).setValue(value)
79
+ }
80
+ }
81
+ }
82
+
83
+ /**
84
+ * @since 1.0.0
85
+ * @category hooks
86
+ */
87
+ export const useAtomValue: {
88
+ <A>(atom: Atom.Atom<A>): A
89
+ <A, B>(atom: Atom.Atom<A>, f: (_: A) => B): B
90
+ } = <A>(atom: Atom.Atom<A>, f?: (_: A) => A): A => {
91
+ const registry = React.useContext(RegistryContext)
92
+ if (f) {
93
+ const atomB = React.useMemo(() => Atom.map(atom, f), [atom, f])
94
+ return useStore(registry, atomB)
95
+ }
96
+ return useStore(registry, atom)
97
+ }
98
+
99
+ function mountAtom<A>(registry: Registry.Registry, atom: Atom.Atom<A>): void {
100
+ React.useEffect(() => registry.mount(atom), [atom, registry])
101
+ }
102
+
103
+ function setAtom<R, W, Mode extends "value" | "promise" | "promiseExit" = never>(
104
+ registry: Registry.Registry,
105
+ atom: Atom.Writable<R, W>,
106
+ options?: {
107
+ readonly mode?: ([R] extends [Result.Result<any, any>] ? Mode : "value") | undefined
108
+ }
109
+ ): "promise" extends Mode ? (
110
+ (
111
+ value: W,
112
+ options?: {
113
+ readonly signal?: AbortSignal | undefined
114
+ } | undefined
115
+ ) => Promise<Result.Result.Success<R>>
116
+ ) :
117
+ "promiseExit" extends Mode ? (
118
+ (
119
+ value: W,
120
+ options?: {
121
+ readonly signal?: AbortSignal | undefined
122
+ } | undefined
123
+ ) => Promise<Exit.Exit<Result.Result.Success<R>, Result.Result.Failure<R>>>
124
+ ) :
125
+ ((value: W | ((value: R) => W)) => void)
126
+ {
127
+ if (options?.mode === "promise" || options?.mode === "promiseExit") {
128
+ return React.useCallback((value: W, opts?: any) => {
129
+ registry.set(atom, value)
130
+ const promise = Effect.runPromiseExit(
131
+ Registry.getResult(registry, atom as Atom.Atom<Result.Result<any, any>>, { suspendOnWaiting: true }),
132
+ opts
133
+ )
134
+ return options!.mode === "promise" ? promise.then(flattenExit) : promise
135
+ }, [registry, atom, options.mode]) as any
136
+ }
137
+ return React.useCallback((value: W | ((value: R) => W)) => {
138
+ registry.set(atom, typeof value === "function" ? (value as any)(registry.get(atom)) : value)
139
+ }, [registry, atom]) as any
140
+ }
141
+
142
+ const flattenExit = <A, E>(exit: Exit.Exit<A, E>): A => {
143
+ if (Exit.isSuccess(exit)) return exit.value
144
+ throw Cause.squash(exit.cause)
145
+ }
146
+
147
+ /**
148
+ * @since 1.0.0
149
+ * @category hooks
150
+ */
151
+ export const useAtomMount = <A>(atom: Atom.Atom<A>): void => {
152
+ const registry = React.useContext(RegistryContext)
153
+ mountAtom(registry, atom)
154
+ }
155
+
156
+ /**
157
+ * @since 1.0.0
158
+ * @category hooks
159
+ */
160
+ export const useAtomSet = <
161
+ R,
162
+ W,
163
+ Mode extends "value" | "promise" | "promiseExit" = never
164
+ >(
165
+ atom: Atom.Writable<R, W>,
166
+ options?: {
167
+ readonly mode?: ([R] extends [Result.Result<any, any>] ? Mode : "value") | undefined
168
+ }
169
+ ): "promise" extends Mode ? (
170
+ (
171
+ value: W,
172
+ options?: {
173
+ readonly signal?: AbortSignal | undefined
174
+ } | undefined
175
+ ) => Promise<Result.Result.Success<R>>
176
+ ) :
177
+ "promiseExit" extends Mode ? (
178
+ (
179
+ value: W,
180
+ options?: {
181
+ readonly signal?: AbortSignal | undefined
182
+ } | undefined
183
+ ) => Promise<Exit.Exit<Result.Result.Success<R>, Result.Result.Failure<R>>>
184
+ ) :
185
+ ((value: W | ((value: R) => W)) => void) =>
186
+ {
187
+ const registry = React.useContext(RegistryContext)
188
+ mountAtom(registry, atom)
189
+ return setAtom(registry, atom, options)
190
+ }
191
+
192
+ /**
193
+ * @since 1.0.0
194
+ * @category hooks
195
+ */
196
+ export const useAtomRefresh = <A>(atom: Atom.Atom<A>): () => void => {
197
+ const registry = React.useContext(RegistryContext)
198
+ mountAtom(registry, atom)
199
+ return React.useCallback(() => {
200
+ registry.refresh(atom)
201
+ }, [registry, atom])
202
+ }
203
+
204
+ /**
205
+ * @since 1.0.0
206
+ * @category hooks
207
+ */
208
+ export const useAtom = <R, W, const Mode extends "value" | "promise" | "promiseExit" = never>(
209
+ atom: Atom.Writable<R, W>,
210
+ options?: {
211
+ readonly mode?: ([R] extends [Result.Result<any, any>] ? Mode : "value") | undefined
212
+ }
213
+ ): readonly [
214
+ value: R,
215
+ write: "promise" extends Mode ? (
216
+ (
217
+ value: W,
218
+ options?: {
219
+ readonly signal?: AbortSignal | undefined
220
+ } | undefined
221
+ ) => Promise<Result.Result.Success<R>>
222
+ ) :
223
+ "promiseExit" extends Mode ? (
224
+ (
225
+ value: W,
226
+ options?: {
227
+ readonly signal?: AbortSignal | undefined
228
+ } | undefined
229
+ ) => Promise<Exit.Exit<Result.Result.Success<R>, Result.Result.Failure<R>>>
230
+ ) :
231
+ ((value: W | ((value: R) => W)) => void)
232
+ ] => {
233
+ const registry = React.useContext(RegistryContext)
234
+ return [
235
+ useStore(registry, atom),
236
+ setAtom(registry, atom, options)
237
+ ] as const
238
+ }
239
+
240
+ const atomPromiseMap = globalValue(
241
+ "@effect-atom/atom-react/atomPromiseMap",
242
+ () => ({
243
+ suspendOnWaiting: new Map<Atom.Atom<any>, Promise<void>>(),
244
+ default: new Map<Atom.Atom<any>, Promise<void>>()
245
+ })
246
+ )
247
+
248
+ function atomToPromise<A, E>(
249
+ registry: Registry.Registry,
250
+ atom: Atom.Atom<Result.Result<A, E>>,
251
+ suspendOnWaiting: boolean
252
+ ) {
253
+ const map = suspendOnWaiting ? atomPromiseMap.suspendOnWaiting : atomPromiseMap.default
254
+ let promise = map.get(atom)
255
+ if (promise !== undefined) {
256
+ return promise
257
+ }
258
+ promise = new Promise<void>((resolve) => {
259
+ const dispose = registry.subscribe(atom, (result) => {
260
+ if (result._tag === "Initial" || (suspendOnWaiting && result.waiting)) {
261
+ return
262
+ }
263
+ setTimeout(dispose, 1000)
264
+ resolve()
265
+ map.delete(atom)
266
+ })
267
+ })
268
+ map.set(atom, promise)
269
+ return promise
270
+ }
271
+
272
+ function atomResultOrSuspend<A, E>(
273
+ registry: Registry.Registry,
274
+ atom: Atom.Atom<Result.Result<A, E>>,
275
+ suspendOnWaiting: boolean
276
+ ) {
277
+ const value = useStore(registry, atom)
278
+ if (value._tag === "Initial" || (suspendOnWaiting && value.waiting)) {
279
+ throw atomToPromise(registry, atom, suspendOnWaiting)
280
+ }
281
+ return value
282
+ }
283
+
284
+ /**
285
+ * @since 1.0.0
286
+ * @category hooks
287
+ */
288
+ export const useAtomSuspense = <A, E, const IncludeFailure extends boolean = false>(
289
+ atom: Atom.Atom<Result.Result<A, E>>,
290
+ options?: {
291
+ readonly suspendOnWaiting?: boolean | undefined
292
+ readonly includeFailure?: IncludeFailure | undefined
293
+ }
294
+ ): Result.Success<A, E> | (IncludeFailure extends true ? Result.Failure<A, E> : never) => {
295
+ const registry = React.useContext(RegistryContext)
296
+ const result = atomResultOrSuspend(registry, atom, options?.suspendOnWaiting ?? false)
297
+ if (result._tag === "Failure" && !options?.includeFailure) {
298
+ throw Cause.squash(result.cause)
299
+ }
300
+ return result as any
301
+ }
302
+
303
+ /**
304
+ * @since 1.0.0
305
+ * @category hooks
306
+ */
307
+ export const useAtomSubscribe = <A>(
308
+ atom: Atom.Atom<A>,
309
+ f: (_: A) => void,
310
+ options?: { readonly immediate?: boolean }
311
+ ): void => {
312
+ const registry = React.useContext(RegistryContext)
313
+ React.useEffect(
314
+ () => registry.subscribe(atom, f, options),
315
+ [registry, atom, f, options?.immediate]
316
+ )
317
+ }
318
+
319
+ /**
320
+ * @since 1.0.0
321
+ * @category hooks
322
+ */
323
+ export const useAtomRef = <A>(ref: AtomRef.ReadonlyRef<A>): A => {
324
+ const [, setValue] = React.useState(ref.value)
325
+ React.useEffect(() => ref.subscribe(setValue), [ref])
326
+ return ref.value
327
+ }
328
+
329
+ /**
330
+ * @since 1.0.0
331
+ * @category hooks
332
+ */
333
+ export const useAtomRefProp = <A, K extends keyof A>(ref: AtomRef.AtomRef<A>, prop: K): AtomRef.AtomRef<A[K]> =>
334
+ React.useMemo(() => ref.prop(prop), [ref, prop])
335
+
336
+ /**
337
+ * @since 1.0.0
338
+ * @category hooks
339
+ */
340
+ export const useAtomRefPropValue = <A, K extends keyof A>(ref: AtomRef.AtomRef<A>, prop: K): A[K] =>
341
+ useAtomRef(useAtomRefProp(ref, prop))
@@ -0,0 +1,84 @@
1
+ /**
2
+ * @since 1.0.0
3
+ */
4
+ "use client"
5
+ import * as Hydration from "@effect-atom/atom/Hydration"
6
+ import * as React from "react"
7
+ import { RegistryContext } from "./RegistryContext.js"
8
+
9
+ /**
10
+ * @since 1.0.0
11
+ * @category components
12
+ */
13
+ export interface HydrationBoundaryProps {
14
+ state?: Iterable<Hydration.DehydratedAtom>
15
+ children?: React.ReactNode
16
+ }
17
+
18
+ /**
19
+ * @since 1.0.0
20
+ * @category components
21
+ */
22
+ export const HydrationBoundary: React.FC<HydrationBoundaryProps> = ({
23
+ children,
24
+ state
25
+ }) => {
26
+ const registry = React.useContext(RegistryContext)
27
+
28
+ // This useMemo is for performance reasons only, everything inside it must
29
+ // be safe to run in every render and code here should be read as "in render".
30
+ //
31
+ // This code needs to happen during the render phase, because after initial
32
+ // SSR, hydration needs to happen _before_ children render. Also, if hydrating
33
+ // during a transition, we want to hydrate as much as is safe in render so
34
+ // we can prerender as much as possible.
35
+ //
36
+ // For any Atom values that already exist in the registry, we want to hold back on
37
+ // hydrating until _after_ the render phase. The reason for this is that during
38
+ // transitions, we don't want the existing Atom values and subscribers to update to
39
+ // the new data on the current page, only _after_ the transition is committed.
40
+ // If the transition is aborted, we will have hydrated any _new_ Atom values, but
41
+ // we throw away the fresh data for any existing ones to avoid unexpectedly
42
+ // updating the UI.
43
+ const hydrationQueue: Array<Hydration.DehydratedAtom> | undefined = React.useMemo(() => {
44
+ if (state) {
45
+ const dehydratedAtoms = Array.from(state)
46
+ const nodes = registry.getNodes()
47
+
48
+ const newDehydratedAtoms: Array<Hydration.DehydratedAtom> = []
49
+ const existingDehydratedAtoms: Array<Hydration.DehydratedAtom> = []
50
+
51
+ for (const dehydratedAtom of dehydratedAtoms) {
52
+ const existingNode = nodes.get(dehydratedAtom.key)
53
+
54
+ if (!existingNode) {
55
+ // This is a new Atom value, safe to hydrate immediately
56
+ newDehydratedAtoms.push(dehydratedAtom)
57
+ } else {
58
+ // This Atom value already exists, queue it for later hydration
59
+ // TODO: Add logic to check if hydration data is newer
60
+ existingDehydratedAtoms.push(dehydratedAtom)
61
+ }
62
+ }
63
+
64
+ if (newDehydratedAtoms.length > 0) {
65
+ // It's actually fine to call this with state that already exists
66
+ // in the registry, or is older. hydrate() is idempotent.
67
+ Hydration.hydrate(registry, newDehydratedAtoms)
68
+ }
69
+
70
+ if (existingDehydratedAtoms.length > 0) {
71
+ return existingDehydratedAtoms
72
+ }
73
+ }
74
+ return undefined
75
+ }, [registry, state])
76
+
77
+ React.useEffect(() => {
78
+ if (hydrationQueue) {
79
+ Hydration.hydrate(registry, hydrationQueue)
80
+ }
81
+ }, [registry, hydrationQueue])
82
+
83
+ return React.createElement(React.Fragment, {}, children)
84
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * @since 1.0.0
3
+ */
4
+ "use client"
5
+ import type * as Atom from "@effect-atom/atom/Atom"
6
+ import * as Registry from "@effect-atom/atom/Registry"
7
+ import * as React from "react"
8
+ import * as Scheduler from "scheduler"
9
+
10
+ /**
11
+ * @since 1.0.0
12
+ * @category context
13
+ */
14
+ export function scheduleTask(f: () => void): void {
15
+ Scheduler.unstable_scheduleCallback(Scheduler.unstable_LowPriority, f)
16
+ }
17
+
18
+ /**
19
+ * @since 1.0.0
20
+ * @category context
21
+ */
22
+ export const RegistryContext = React.createContext<Registry.Registry>(Registry.make({
23
+ scheduleTask,
24
+ defaultIdleTTL: 400
25
+ }))
26
+
27
+ /**
28
+ * @since 1.0.0
29
+ * @category context
30
+ */
31
+ export const RegistryProvider = (options: {
32
+ readonly children?: React.ReactNode | undefined
33
+ readonly initialValues?: Iterable<readonly [Atom.Atom<any>, any]> | undefined
34
+ readonly scheduleTask?: ((f: () => void) => void) | undefined
35
+ readonly timeoutResolution?: number | undefined
36
+ readonly defaultIdleTTL?: number | undefined
37
+ }) => {
38
+ const ref = React.useRef<{
39
+ readonly registry: Registry.Registry
40
+ timeout?: number | undefined
41
+ }>(null)
42
+ if (ref.current === null) {
43
+ ref.current = {
44
+ registry: Registry.make({
45
+ scheduleTask: options.scheduleTask ?? scheduleTask,
46
+ initialValues: options.initialValues,
47
+ timeoutResolution: options.timeoutResolution,
48
+ defaultIdleTTL: options.defaultIdleTTL
49
+ })
50
+ }
51
+ }
52
+ React.useEffect(() => {
53
+ if (ref.current?.timeout !== undefined) {
54
+ clearTimeout(ref.current.timeout)
55
+ }
56
+ return () => {
57
+ ref.current!.timeout = setTimeout(() => {
58
+ ref.current?.registry.dispose()
59
+ ref.current = null
60
+ }, 500)
61
+ }
62
+ }, [ref])
63
+ return React.createElement(RegistryContext.Provider, { value: ref.current.registry }, options?.children)
64
+ }
package/src/index.ts ADDED
@@ -0,0 +1,45 @@
1
+ /**
2
+ * @since 1.0.0
3
+ */
4
+
5
+ /**
6
+ * @since 1.0.0
7
+ * @category re-exports
8
+ */
9
+ export * as Atom from "@effect-atom/atom/Atom"
10
+
11
+ /**
12
+ * @since 1.0.0
13
+ * @category re-exports
14
+ */
15
+ export * as Registry from "@effect-atom/atom/Registry"
16
+
17
+ /**
18
+ * @since 1.0.0
19
+ * @category re-exports
20
+ */
21
+ export * as Result from "@effect-atom/atom/Result"
22
+
23
+ /**
24
+ * @since 1.0.0
25
+ * @category re-exports
26
+ */
27
+ export * as AtomRef from "@effect-atom/atom/AtomRef"
28
+
29
+ /**
30
+ * @since 1.0.0
31
+ * @category re-exports
32
+ */
33
+ export * as Hydration from "@effect-atom/atom/Hydration"
34
+
35
+ /**
36
+ * @since 1.0.0
37
+ * @category hooks
38
+ */
39
+ export * from "./Hooks.js"
40
+
41
+ /**
42
+ * @since 1.0.0
43
+ * @category context
44
+ */
45
+ export * from "./RegistryContext.js"