@effect-rx/rx-react 0.38.1 → 0.38.2

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,285 @@
1
+ /**
2
+ * @since 1.0.0
3
+ */
4
+ "use client"
5
+ import type * as Registry from "@effect-rx/rx/Registry"
6
+ import * as Result from "@effect-rx/rx/Result"
7
+ import * as Rx from "@effect-rx/rx/Rx"
8
+ import type * as RxRef from "@effect-rx/rx/RxRef"
9
+ import * as Cause from "effect/Cause"
10
+ import type * as Exit from "effect/Exit"
11
+ import { globalValue } from "effect/GlobalValue"
12
+ import * as React from "react"
13
+ import { RegistryContext } from "./RegistryContext.js"
14
+
15
+ interface RxStore<A> {
16
+ readonly subscribe: (f: () => void) => () => void
17
+ readonly snapshot: () => A
18
+ }
19
+
20
+ const storeRegistry = globalValue(
21
+ "@effect-rx/rx-react/storeRegistry",
22
+ () => new WeakMap<Registry.Registry, WeakMap<Rx.Rx<any>, RxStore<any>>>()
23
+ )
24
+
25
+ function makeStore<A>(registry: Registry.Registry, rx: Rx.Rx<A>): RxStore<A> {
26
+ let stores = storeRegistry.get(registry)
27
+ if (stores === undefined) {
28
+ stores = new WeakMap()
29
+ storeRegistry.set(registry, stores)
30
+ }
31
+ const store = stores.get(rx)
32
+ if (store !== undefined) {
33
+ return store
34
+ }
35
+ const newStore: RxStore<A> = {
36
+ subscribe(f) {
37
+ return registry.subscribe(rx, f)
38
+ },
39
+ snapshot() {
40
+ return registry.get(rx)
41
+ }
42
+ }
43
+ stores.set(rx, newStore)
44
+ return newStore
45
+ }
46
+
47
+ function useStore<A>(registry: Registry.Registry, rx: Rx.Rx<A>): A {
48
+ const store = makeStore(registry, rx)
49
+ return React.useSyncExternalStore(store.subscribe, store.snapshot, store.snapshot)
50
+ }
51
+
52
+ const initialValuesSet = globalValue(
53
+ "@effect-rx/rx-react/initialValuesSet",
54
+ () => new WeakMap<Registry.Registry, WeakSet<Rx.Rx<any>>>()
55
+ )
56
+
57
+ /**
58
+ * @since 1.0.0
59
+ * @category hooks
60
+ */
61
+ export const useRxInitialValues = (initialValues: Iterable<readonly [Rx.Rx<any>, any]>): void => {
62
+ const registry = React.useContext(RegistryContext)
63
+ let set = initialValuesSet.get(registry)
64
+ if (set === undefined) {
65
+ set = new WeakSet()
66
+ initialValuesSet.set(registry, set)
67
+ }
68
+ for (const [rx, value] of initialValues) {
69
+ if (!set.has(rx)) {
70
+ set.add(rx)
71
+ ;(registry as any).ensureNode(rx).setValue(value)
72
+ }
73
+ }
74
+ }
75
+
76
+ /**
77
+ * @since 1.0.0
78
+ * @category hooks
79
+ */
80
+ export const useRxValue: {
81
+ <A>(rx: Rx.Rx<A>): A
82
+ <A, B>(rx: Rx.Rx<A>, f: (_: A) => B): B
83
+ } = <A>(rx: Rx.Rx<A>, f?: (_: A) => A): A => {
84
+ const registry = React.useContext(RegistryContext)
85
+ if (f) {
86
+ const rxB = React.useMemo(() => Rx.map(rx, f), [rx, f])
87
+ return useStore(registry, rxB)
88
+ }
89
+ return useStore(registry, rx)
90
+ }
91
+
92
+ function mountRx<A>(registry: Registry.Registry, rx: Rx.Rx<A>): void {
93
+ React.useEffect(() => registry.mount(rx), [rx, registry])
94
+ }
95
+
96
+ function setRx<R, W>(registry: Registry.Registry, rx: Rx.Writable<R, W>): (_: W | ((_: R) => W)) => void {
97
+ return React.useCallback((value) => {
98
+ if (typeof value === "function") {
99
+ registry.set(rx, (value as any)(registry.get(rx)))
100
+ return
101
+ } else {
102
+ registry.set(rx, value)
103
+ }
104
+ }, [registry, rx])
105
+ }
106
+
107
+ /**
108
+ * @since 1.0.0
109
+ * @category hooks
110
+ */
111
+ export const useRxMount = <A>(rx: Rx.Rx<A>): void => {
112
+ const registry = React.useContext(RegistryContext)
113
+ mountRx(registry, rx)
114
+ }
115
+
116
+ /**
117
+ * @since 1.0.0
118
+ * @category hooks
119
+ */
120
+ export const useRxSet = <R, W>(rx: Rx.Writable<R, W>): (_: W | ((_: R) => W)) => void => {
121
+ const registry = React.useContext(RegistryContext)
122
+ mountRx(registry, rx)
123
+ return setRx(registry, rx)
124
+ }
125
+
126
+ /**
127
+ * @since 1.0.0
128
+ * @category hooks
129
+ */
130
+ export const useRxSetPromise = <E, A, W>(
131
+ rx: Rx.Writable<Result.Result<A, E>, W>
132
+ ): (_: W) => Promise<Exit.Exit<A, E>> => {
133
+ const registry = React.useContext(RegistryContext)
134
+ const resolves = React.useMemo(() => new Set<(result: Exit.Exit<A, E>) => void>(), [])
135
+ React.useEffect(() =>
136
+ registry.subscribe(rx, (result) => {
137
+ if (result.waiting || result._tag === "Initial") return
138
+ const fns = Array.from(resolves)
139
+ resolves.clear()
140
+ const exit = Result.toExit(result)
141
+ fns.forEach((resolve) => resolve(exit as any))
142
+ }, { immediate: true }), [registry, rx, resolves])
143
+ return React.useCallback((value) =>
144
+ new Promise((resolve) => {
145
+ resolves.add(resolve)
146
+ registry.set(rx, value)
147
+ }), [registry, rx, resolves])
148
+ }
149
+
150
+ /**
151
+ * @since 1.0.0
152
+ * @category hooks
153
+ */
154
+ export const useRxRefresh = <A>(rx: Rx.Rx<A>): () => void => {
155
+ const registry = React.useContext(RegistryContext)
156
+ mountRx(registry, rx)
157
+ return React.useCallback(() => {
158
+ registry.refresh(rx)
159
+ }, [registry, rx])
160
+ }
161
+
162
+ /**
163
+ * @since 1.0.0
164
+ * @category hooks
165
+ */
166
+ export const useRx = <R, W>(
167
+ rx: Rx.Writable<R, W>
168
+ ): readonly [value: R, setOrUpdate: (_: W | ((_: R) => W)) => void] => {
169
+ const registry = React.useContext(RegistryContext)
170
+ return [
171
+ useStore(registry, rx),
172
+ setRx(registry, rx)
173
+ ] as const
174
+ }
175
+
176
+ const rxPromiseMap = globalValue(
177
+ "@effect-rx/rx-react/rxPromiseMap",
178
+ () => ({
179
+ suspendOnWaiting: new Map<Rx.Rx<any>, Promise<void>>(),
180
+ default: new Map<Rx.Rx<any>, Promise<void>>()
181
+ })
182
+ )
183
+
184
+ function rxToPromise<A, E>(
185
+ registry: Registry.Registry,
186
+ rx: Rx.Rx<Result.Result<A, E>>,
187
+ suspendOnWaiting: boolean
188
+ ) {
189
+ const map = suspendOnWaiting ? rxPromiseMap.suspendOnWaiting : rxPromiseMap.default
190
+ let promise = map.get(rx)
191
+ if (promise !== undefined) {
192
+ return promise
193
+ }
194
+ promise = new Promise<void>((resolve) => {
195
+ const dispose = registry.subscribe(rx, (result) => {
196
+ if (result._tag === "Initial" || (suspendOnWaiting && result.waiting)) {
197
+ return
198
+ }
199
+ setTimeout(dispose, 1000)
200
+ resolve()
201
+ map.delete(rx)
202
+ })
203
+ })
204
+ map.set(rx, promise)
205
+ return promise
206
+ }
207
+
208
+ function rxResultOrSuspend<A, E>(
209
+ registry: Registry.Registry,
210
+ rx: Rx.Rx<Result.Result<A, E>>,
211
+ suspendOnWaiting: boolean
212
+ ) {
213
+ const value = useStore(registry, rx)
214
+ if (value._tag === "Initial" || (suspendOnWaiting && value.waiting)) {
215
+ throw rxToPromise(registry, rx, suspendOnWaiting)
216
+ }
217
+ return value
218
+ }
219
+
220
+ /**
221
+ * @since 1.0.0
222
+ * @category hooks
223
+ */
224
+ export const useRxSuspense = <A, E>(
225
+ rx: Rx.Rx<Result.Result<A, E>>,
226
+ options?: { readonly suspendOnWaiting?: boolean }
227
+ ): Result.Success<A, E> | Result.Failure<A, E> => {
228
+ const registry = React.useContext(RegistryContext)
229
+ return rxResultOrSuspend(registry, rx, options?.suspendOnWaiting ?? false)
230
+ }
231
+
232
+ /**
233
+ * @since 1.0.0
234
+ * @category hooks
235
+ */
236
+ export const useRxSuspenseSuccess = <A, E>(
237
+ rx: Rx.Rx<Result.Result<A, E>>,
238
+ options?: { readonly suspendOnWaiting?: boolean }
239
+ ): Result.Success<A, E> => {
240
+ const result = useRxSuspense(rx, options)
241
+ if (result._tag === "Failure") {
242
+ throw Cause.squash(result.cause)
243
+ }
244
+ return result
245
+ }
246
+
247
+ /**
248
+ * @since 1.0.0
249
+ * @category hooks
250
+ */
251
+ export const useRxSubscribe = <A>(
252
+ rx: Rx.Rx<A>,
253
+ f: (_: A) => void,
254
+ options?: { readonly immediate?: boolean }
255
+ ): void => {
256
+ const registry = React.useContext(RegistryContext)
257
+ React.useEffect(
258
+ () => registry.subscribe(rx, f, options),
259
+ [registry, rx, f, options?.immediate]
260
+ )
261
+ }
262
+
263
+ /**
264
+ * @since 1.0.0
265
+ * @category hooks
266
+ */
267
+ export const useRxRef = <A>(ref: RxRef.ReadonlyRef<A>): A => {
268
+ const [, setValue] = React.useState(ref.value)
269
+ React.useEffect(() => ref.subscribe(setValue), [ref])
270
+ return ref.value
271
+ }
272
+
273
+ /**
274
+ * @since 1.0.0
275
+ * @category hooks
276
+ */
277
+ export const useRxRefProp = <A, K extends keyof A>(ref: RxRef.RxRef<A>, prop: K): RxRef.RxRef<A[K]> =>
278
+ React.useMemo(() => ref.prop(prop), [ref, prop])
279
+
280
+ /**
281
+ * @since 1.0.0
282
+ * @category hooks
283
+ */
284
+ export const useRxRefPropValue = <A, K extends keyof A>(ref: RxRef.RxRef<A>, prop: K): A[K] =>
285
+ useRxRef(useRxRefProp(ref, prop))
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @since 1.0.0
3
+ */
4
+ "use client"
5
+ import * as Hydration from "@effect-rx/rx/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 const HydrationBoundary: React.FC<{
14
+ state: Iterable<Hydration.DehydratedRx>
15
+ children?: React.ReactNode
16
+ }> = ({ children, state }) => {
17
+ const registry = React.useContext(RegistryContext)
18
+ React.useEffect(() => {
19
+ Hydration.hydrate(registry, state)
20
+ }, [registry, state])
21
+ return React.createElement(React.Fragment, {}, children)
22
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * @since 1.0.0
3
+ */
4
+ "use client"
5
+ import * as Registry from "@effect-rx/rx/Registry"
6
+ import type * as Rx from "@effect-rx/rx/Rx"
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 [Rx.Rx<any>, any]> | undefined
34
+ readonly scheduleTask?: ((f: () => void) => void) | undefined
35
+ readonly timeoutResolution?: number | undefined
36
+ readonly defaultIdleTTL?: number | undefined
37
+ }) => {
38
+ const registry = React.useMemo(() =>
39
+ Registry.make({
40
+ scheduleTask,
41
+ defaultIdleTTL: 400,
42
+ ...options
43
+ }), [])
44
+ React.useEffect(() => () => {
45
+ registry.dispose()
46
+ }, [registry])
47
+ return React.createElement(RegistryContext.Provider, {
48
+ value: Registry.make({
49
+ scheduleTask,
50
+ defaultIdleTTL: 400,
51
+ ...options
52
+ })
53
+ }, options?.children)
54
+ }