From 35463d5607a699db40820dd9ec40091243598bfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Thu, 24 Jul 2025 02:26:53 +0200 Subject: [PATCH 01/90] Fix --- packages/effect-fc/src/types/SubscriptionSubRef.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/effect-fc/src/types/SubscriptionSubRef.ts b/packages/effect-fc/src/types/SubscriptionSubRef.ts index 407d7e3..fead780 100644 --- a/packages/effect-fc/src/types/SubscriptionSubRef.ts +++ b/packages/effect-fc/src/types/SubscriptionSubRef.ts @@ -75,7 +75,7 @@ class SubscriptionSubRefImpl extends Effectable.Class imp modifyEffect(f: (a: A) => Effect.Effect): Effect.Effect { return Effect.Do.pipe( - Effect.bind("b", () => Ref.get(this.parent)), + Effect.bind("b", () => this.parent), Effect.bind("ca", ({ b }) => f(this.getter(b))), Effect.tap(({ b, ca: [, a] }) => Ref.set(this.parent, this.setter(b, a))), Effect.map(({ ca: [c] }) => c), -- 2.49.1 From 051226ebd46d70758069aecfd2c3f14ef26ec80e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Sat, 26 Jul 2025 01:35:02 +0200 Subject: [PATCH 02/90] SubscriptionSubRef refactoring --- .../effect-fc/src/types/SubscriptionSubRef.ts | 55 +++++++++++-------- packages/example/src/routes/dev/memo.tsx | 6 +- 2 files changed, 36 insertions(+), 25 deletions(-) diff --git a/packages/effect-fc/src/types/SubscriptionSubRef.ts b/packages/effect-fc/src/types/SubscriptionSubRef.ts index fead780..88baee8 100644 --- a/packages/effect-fc/src/types/SubscriptionSubRef.ts +++ b/packages/effect-fc/src/types/SubscriptionSubRef.ts @@ -5,7 +5,8 @@ import * as PropertyPath from "./PropertyPath.js" export const SubscriptionSubRefTypeId: unique symbol = Symbol.for("effect-fc/types/SubscriptionSubRef") export type SubscriptionSubRefTypeId = typeof SubscriptionSubRefTypeId -export interface SubscriptionSubRef extends SubscriptionSubRef.Variance, SubscriptionRef.SubscriptionRef { +export interface SubscriptionSubRef> +extends SubscriptionSubRef.Variance, SubscriptionRef.SubscriptionRef { readonly parent: SubscriptionRef.SubscriptionRef readonly [Unify.typeSymbol]?: unknown @@ -36,7 +37,8 @@ const synchronizedRefVariance = { _A: (_: any) => _ } const subscriptionRefVariance = { _A: (_: any) => _ } const subscriptionSubRefVariance = { _A: (_: any) => _, _B: (_: any) => _ } -class SubscriptionSubRefImpl extends Effectable.Class implements SubscriptionSubRef { +class SubscriptionSubRefImpl> +extends Effectable.Class implements SubscriptionSubRef { readonly [Readable.TypeId]: Readable.TypeId = Readable.TypeId readonly [Subscribable.TypeId]: Subscribable.TypeId = Subscribable.TypeId readonly [Ref.RefTypeId] = refVariance @@ -47,9 +49,9 @@ class SubscriptionSubRefImpl extends Effectable.Class imp readonly get: Effect.Effect constructor( - readonly parent: SubscriptionRef.SubscriptionRef, - readonly getter: (parentValue: B) => A, - readonly setter: (parentValue: B, value: A) => B, + readonly parent: B, + readonly getter: (parentValue: Effect.Effect.Success) => A, + readonly setter: (parentValue: Effect.Effect.Success, value: A) => Effect.Effect.Success, ) { super() this.get = Effect.map(this.parent, this.getter) @@ -60,12 +62,11 @@ class SubscriptionSubRefImpl extends Effectable.Class imp } get changes(): Stream.Stream { - return this.get.pipe( - Effect.map(a => this.parent.changes.pipe( - Stream.map(this.getter), - s => Stream.concat(Stream.make(a), s), - )), - Stream.unwrap, + return Stream.unwrap( + Effect.map(this.get, a => Stream.concat( + Stream.make(a), + Stream.map(this.parent.changes, this.getter), + )) ) } @@ -75,7 +76,7 @@ class SubscriptionSubRefImpl extends Effectable.Class imp modifyEffect(f: (a: A) => Effect.Effect): Effect.Effect { return Effect.Do.pipe( - Effect.bind("b", () => this.parent), + Effect.bind("b", (): Effect.Effect> => this.parent), Effect.bind("ca", ({ b }) => f(this.getter(b))), Effect.tap(({ b, ca: [, a] }) => Ref.set(this.parent, this.setter(b, a))), Effect.map(({ ca: [c] }) => c), @@ -84,28 +85,34 @@ class SubscriptionSubRefImpl extends Effectable.Class imp } -export const makeFromGetSet = ( - parent: SubscriptionRef.SubscriptionRef, +export const makeFromGetSet = >( + parent: B, options: { - readonly get: (parentValue: B) => A - readonly set: (parentValue: B, value: A) => B + readonly get: (parentValue: Effect.Effect.Success) => A + readonly set: (parentValue: Effect.Effect.Success, value: A) => Effect.Effect.Success }, ): SubscriptionSubRef => new SubscriptionSubRefImpl(parent, options.get, options.set) -export const makeFromPath = >( - parent: SubscriptionRef.SubscriptionRef, +export const makeFromPath = < + B extends SubscriptionRef.SubscriptionRef, + const P extends PropertyPath.Paths>, +>( + parent: B, path: P, -): SubscriptionSubRef, B> => new SubscriptionSubRefImpl( +): SubscriptionSubRef, P>, B> => new SubscriptionSubRefImpl( parent, parentValue => Option.getOrThrow(PropertyPath.get(parentValue, path)), (parentValue, value) => Option.getOrThrow(PropertyPath.immutableSet(parentValue, path, value)), ) -export const makeFromChunkRef = ( - parent: SubscriptionRef.SubscriptionRef>, +export const makeFromChunkRef = | Chunk.NonEmptyChunk>>( + parent: B, index: number, -): SubscriptionSubRef> => new SubscriptionSubRefImpl( - parent, +): SubscriptionSubRef< + Effect.Effect.Success extends Chunk.Chunk ? A : never, + B +> => new SubscriptionSubRefImpl( + parent as SubscriptionRef.SubscriptionRef>, parentValue => Chunk.unsafeGet(parentValue, index), (parentValue, value) => Chunk.replace(parentValue, index, value), -) +) as any diff --git a/packages/example/src/routes/dev/memo.tsx b/packages/example/src/routes/dev/memo.tsx index 42f2f41..4acd3cb 100644 --- a/packages/example/src/routes/dev/memo.tsx +++ b/packages/example/src/routes/dev/memo.tsx @@ -2,13 +2,17 @@ import { runtime } from "@/runtime" import { Flex, Text, TextField } from "@radix-ui/themes" import { createFileRoute } from "@tanstack/react-router" import { GetRandomValues, makeUuid4 } from "@typed/id" -import { Effect } from "effect" +import { Chunk, Effect, SubscriptionRef } from "effect" import { Component, Memoized } from "effect-fc" +import { SubscriptionSubRef } from "effect-fc/types" import * as React from "react" const RouteComponent = Component.make(function* RouteComponent() { const [value, setValue] = React.useState("") + const myRef = yield* SubscriptionRef.make(Chunk.make({ name: "person 1" } as const)) + // const myRef = yield* SubscriptionRef.make(Chunk.empty<{ readonly name: "person 1" }>()) + const mySubRef = SubscriptionSubRef.makeFromChunkRef(myRef, 0) return ( -- 2.49.1 From 1c1659e82c150fd2bf24eac64e61a496657cd04a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Sun, 27 Jul 2025 18:09:07 +0200 Subject: [PATCH 03/90] Fix --- .../effect-fc/src/types/SubscriptionSubRef.ts | 26 ++++++++++++++----- packages/example/src/routes/dev/memo.tsx | 4 +-- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/packages/effect-fc/src/types/SubscriptionSubRef.ts b/packages/effect-fc/src/types/SubscriptionSubRef.ts index 88baee8..ead45f1 100644 --- a/packages/effect-fc/src/types/SubscriptionSubRef.ts +++ b/packages/effect-fc/src/types/SubscriptionSubRef.ts @@ -105,14 +105,26 @@ export const makeFromPath = < (parentValue, value) => Option.getOrThrow(PropertyPath.immutableSet(parentValue, path, value)), ) -export const makeFromChunkRef = | Chunk.NonEmptyChunk>>( - parent: B, +export const makeFromChunkRef: { + >>( + parent: B, + index: number, + ): SubscriptionSubRef< + Effect.Effect.Success extends Chunk.NonEmptyChunk ? A : never, + B + > + >>( + parent: B, + index: number, + ): SubscriptionSubRef< + Effect.Effect.Success extends Chunk.Chunk ? A : never, + B + > +} = ( + parent: SubscriptionRef.SubscriptionRef>, index: number, -): SubscriptionSubRef< - Effect.Effect.Success extends Chunk.Chunk ? A : never, - B -> => new SubscriptionSubRefImpl( - parent as SubscriptionRef.SubscriptionRef>, +) => new SubscriptionSubRefImpl( + parent, parentValue => Chunk.unsafeGet(parentValue, index), (parentValue, value) => Chunk.replace(parentValue, index, value), ) as any diff --git a/packages/example/src/routes/dev/memo.tsx b/packages/example/src/routes/dev/memo.tsx index 4acd3cb..5b10906 100644 --- a/packages/example/src/routes/dev/memo.tsx +++ b/packages/example/src/routes/dev/memo.tsx @@ -10,8 +10,8 @@ import * as React from "react" const RouteComponent = Component.make(function* RouteComponent() { const [value, setValue] = React.useState("") - const myRef = yield* SubscriptionRef.make(Chunk.make({ name: "person 1" } as const)) - // const myRef = yield* SubscriptionRef.make(Chunk.empty<{ readonly name: "person 1" }>()) + // const myRef = yield* SubscriptionRef.make(Chunk.make({ name: "person 1" } as const)) + const myRef = yield* SubscriptionRef.make(Chunk.empty<{ readonly name: "person 1" }>()) const mySubRef = SubscriptionSubRef.makeFromChunkRef(myRef, 0) return ( -- 2.49.1 From 956a532195278138ec703a6afc8d93f4efe87746 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Sun, 27 Jul 2025 19:08:16 +0200 Subject: [PATCH 04/90] Fix --- packages/example/src/routes/dev/memo.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/example/src/routes/dev/memo.tsx b/packages/example/src/routes/dev/memo.tsx index 5b10906..42f2f41 100644 --- a/packages/example/src/routes/dev/memo.tsx +++ b/packages/example/src/routes/dev/memo.tsx @@ -2,17 +2,13 @@ import { runtime } from "@/runtime" import { Flex, Text, TextField } from "@radix-ui/themes" import { createFileRoute } from "@tanstack/react-router" import { GetRandomValues, makeUuid4 } from "@typed/id" -import { Chunk, Effect, SubscriptionRef } from "effect" +import { Effect } from "effect" import { Component, Memoized } from "effect-fc" -import { SubscriptionSubRef } from "effect-fc/types" import * as React from "react" const RouteComponent = Component.make(function* RouteComponent() { const [value, setValue] = React.useState("") - // const myRef = yield* SubscriptionRef.make(Chunk.make({ name: "person 1" } as const)) - const myRef = yield* SubscriptionRef.make(Chunk.empty<{ readonly name: "person 1" }>()) - const mySubRef = SubscriptionSubRef.makeFromChunkRef(myRef, 0) return ( -- 2.49.1 From 09ed773b96f0f35f6bc321dc09dee30625c2db40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Mon, 28 Jul 2025 01:42:09 +0200 Subject: [PATCH 05/90] Refactoring --- packages/effect-fc/src/Component.ts | 2 +- packages/effect-fc/src/Hook.ts | 302 ------------------ packages/effect-fc/src/Hook/ScopeOptions.ts | 7 + packages/effect-fc/src/Hook/index.ts | 14 + packages/effect-fc/src/Hook/internal.ts | 18 ++ .../effect-fc/src/Hook/useCallbackPromise.ts | 16 + .../effect-fc/src/Hook/useCallbackSync.ts | 16 + packages/effect-fc/src/Hook/useContext.ts | 23 ++ packages/effect-fc/src/Hook/useEffect.ts | 28 ++ packages/effect-fc/src/Hook/useFork.ts | 31 ++ .../effect-fc/src/Hook/useLayoutEffect.ts | 28 ++ packages/effect-fc/src/Hook/useMemo.ts | 16 + packages/effect-fc/src/Hook/useOnce.ts | 11 + .../src/Hook/useRefFromReactiveValue.ts | 12 + packages/effect-fc/src/Hook/useRefState.ts | 28 ++ packages/effect-fc/src/Hook/useScope.ts | 36 +++ .../src/Hook/useStreamFromReactiveValues.ts | 30 ++ .../effect-fc/src/Hook/useSubscribeRefs.ts | 27 ++ .../effect-fc/src/Hook/useSubscribeStream.ts | 31 ++ packages/effect-fc/src/index.ts | 2 +- 20 files changed, 374 insertions(+), 304 deletions(-) delete mode 100644 packages/effect-fc/src/Hook.ts create mode 100644 packages/effect-fc/src/Hook/ScopeOptions.ts create mode 100644 packages/effect-fc/src/Hook/index.ts create mode 100644 packages/effect-fc/src/Hook/internal.ts create mode 100644 packages/effect-fc/src/Hook/useCallbackPromise.ts create mode 100644 packages/effect-fc/src/Hook/useCallbackSync.ts create mode 100644 packages/effect-fc/src/Hook/useContext.ts create mode 100644 packages/effect-fc/src/Hook/useEffect.ts create mode 100644 packages/effect-fc/src/Hook/useFork.ts create mode 100644 packages/effect-fc/src/Hook/useLayoutEffect.ts create mode 100644 packages/effect-fc/src/Hook/useMemo.ts create mode 100644 packages/effect-fc/src/Hook/useOnce.ts create mode 100644 packages/effect-fc/src/Hook/useRefFromReactiveValue.ts create mode 100644 packages/effect-fc/src/Hook/useRefState.ts create mode 100644 packages/effect-fc/src/Hook/useScope.ts create mode 100644 packages/effect-fc/src/Hook/useStreamFromReactiveValues.ts create mode 100644 packages/effect-fc/src/Hook/useSubscribeRefs.ts create mode 100644 packages/effect-fc/src/Hook/useSubscribeStream.ts diff --git a/packages/effect-fc/src/Component.ts b/packages/effect-fc/src/Component.ts index a2f0d21..98089ee 100644 --- a/packages/effect-fc/src/Component.ts +++ b/packages/effect-fc/src/Component.ts @@ -1,6 +1,6 @@ import { Context, Effect, Effectable, ExecutionStrategy, Function, Predicate, Runtime, Scope, String, Tracer, type Utils } from "effect" import * as React from "react" -import * as Hook from "./Hook.js" +import * as Hook from "./Hook/index.js" import * as Memoized from "./Memoized.js" diff --git a/packages/effect-fc/src/Hook.ts b/packages/effect-fc/src/Hook.ts deleted file mode 100644 index ee766b3..0000000 --- a/packages/effect-fc/src/Hook.ts +++ /dev/null @@ -1,302 +0,0 @@ -import { type Context, Effect, Equivalence, ExecutionStrategy, Exit, type Layer, Option, pipe, PubSub, Ref, Runtime, Scope, Stream, SubscriptionRef } from "effect" -import * as React from "react" -import { SetStateAction } from "./types/index.js" - - -export interface ScopeOptions { - readonly finalizerExecutionMode?: "sync" | "fork" - readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy -} - - -export const useScope: { - ( - deps: React.DependencyList, - options?: ScopeOptions, - ): Effect.Effect -} = Effect.fn("useScope")(function*(deps, options) { - const runtime = yield* Effect.runtime() - - const [isInitialRun, initialScope] = React.useMemo(() => Runtime.runSync(runtime)(Effect.all([ - Ref.make(true), - Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential), - ])), []) - const [scope, setScope] = React.useState(initialScope) - - React.useEffect(() => Runtime.runSync(runtime)( - Effect.if(isInitialRun, { - onTrue: () => Effect.as( - Ref.set(isInitialRun, false), - () => closeScope(scope, runtime, options), - ), - - onFalse: () => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential).pipe( - Effect.tap(scope => Effect.sync(() => setScope(scope))), - Effect.map(scope => () => closeScope(scope, runtime, options)), - ), - }) - ), deps) - - return scope -}) - -const closeScope = ( - scope: Scope.CloseableScope, - runtime: Runtime.Runtime, - options?: ScopeOptions, -) => { - switch (options?.finalizerExecutionMode ?? "sync") { - case "sync": - Runtime.runSync(runtime)(Scope.close(scope, Exit.void)) - break - case "fork": - Runtime.runFork(runtime)(Scope.close(scope, Exit.void)) - break - } -} - - -export const useCallbackSync: { - ( - callback: (...args: Args) => Effect.Effect, - deps: React.DependencyList, - ): Effect.Effect<(...args: Args) => A, never, R> -} = Effect.fn("useCallbackSync")(function* ( - callback: (...args: Args) => Effect.Effect, - deps: React.DependencyList, -) { - const runtime = yield* Effect.runtime() - return React.useCallback((...args: Args) => Runtime.runSync(runtime)(callback(...args)), deps) -}) - -export const useCallbackPromise: { - ( - callback: (...args: Args) => Effect.Effect, - deps: React.DependencyList, - ): Effect.Effect<(...args: Args) => Promise, never, R> -} = Effect.fn("useCallbackPromise")(function* ( - callback: (...args: Args) => Effect.Effect, - deps: React.DependencyList, -) { - const runtime = yield* Effect.runtime() - return React.useCallback((...args: Args) => Runtime.runPromise(runtime)(callback(...args)), deps) -}) - - -export const useMemo: { - ( - factory: () => Effect.Effect, - deps: React.DependencyList, - ): Effect.Effect -} = Effect.fn("useMemo")(function* ( - factory: () => Effect.Effect, - deps: React.DependencyList, -) { - const runtime = yield* Effect.runtime() - return yield* React.useMemo(() => Runtime.runSync(runtime)(Effect.cached(factory())), deps) -}) - -export const useOnce: { - (factory: () => Effect.Effect): Effect.Effect -} = Effect.fn("useOnce")(function* ( - factory: () => Effect.Effect -) { - return yield* useMemo(factory, []) -}) - - -export const useEffect: { - ( - effect: () => Effect.Effect, - deps?: React.DependencyList, - options?: ScopeOptions, - ): Effect.Effect> -} = Effect.fn("useEffect")(function* ( - effect: () => Effect.Effect, - deps?: React.DependencyList, - options?: ScopeOptions, -) { - const runtime = yield* Effect.runtime>() - - React.useEffect(() => Effect.Do.pipe( - Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)), - Effect.bind("exit", ({ scope }) => Effect.exit(Effect.provideService(effect(), Scope.Scope, scope))), - Effect.map(({ scope }) => - () => closeScope(scope, runtime, options) - ), - Runtime.runSync(runtime), - ), deps) -}) - -export const useLayoutEffect: { - ( - effect: () => Effect.Effect, - deps?: React.DependencyList, - options?: ScopeOptions, - ): Effect.Effect> -} = Effect.fn("useLayoutEffect")(function* ( - effect: () => Effect.Effect, - deps?: React.DependencyList, - options?: ScopeOptions, -) { - const runtime = yield* Effect.runtime>() - - React.useLayoutEffect(() => Effect.Do.pipe( - Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)), - Effect.bind("exit", ({ scope }) => Effect.exit(Effect.provideService(effect(), Scope.Scope, scope))), - Effect.map(({ scope }) => - () => closeScope(scope, runtime, options) - ), - Runtime.runSync(runtime), - ), deps) -}) - -export const useFork: { - ( - effect: () => Effect.Effect, - deps?: React.DependencyList, - options?: Runtime.RunForkOptions & ScopeOptions, - ): Effect.Effect> -} = Effect.fn("useFork")(function* ( - effect: () => Effect.Effect, - deps?: React.DependencyList, - options?: Runtime.RunForkOptions & ScopeOptions, -) { - const runtime = yield* Effect.runtime>() - - React.useEffect(() => { - const scope = Runtime.runSync(runtime)(options?.scope - ? Scope.fork(options.scope, options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential) - : Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential) - ) - Runtime.runFork(runtime)(Effect.provideService(effect(), Scope.Scope, scope), { ...options, scope }) - return () => closeScope(scope, runtime, { - ...options, - finalizerExecutionMode: options?.finalizerExecutionMode ?? "fork", - }) - }, deps) -}) - - -export const useContext: { - ( - layer: Layer.Layer, - options?: ScopeOptions, - ): Effect.Effect, E, Exclude> -} = Effect.fn("useContext")(function* ( - layer: Layer.Layer, - options?: ScopeOptions, -) { - const scope = yield* useScope([layer], options) - - return yield* useMemo(() => Effect.provideService( - Effect.provide(Effect.context(), layer), - Scope.Scope, - scope, - ), [scope]) -}) - - -export const useRefFromReactiveValue: { - (value: A): Effect.Effect> -} = Effect.fn("useRefFromReactiveValue")(function*(value) { - const ref = yield* useOnce(() => SubscriptionRef.make(value)) - yield* useEffect(() => Ref.set(ref, value), [value]) - return ref -}) - -export const useSubscribeRefs: { - []>( - ...refs: Refs - ): Effect.Effect<{ [K in keyof Refs]: Effect.Effect.Success }> -} = Effect.fn("useSubscribeRefs")(function* []>( - ...refs: Refs -) { - const [reactStateValue, setReactStateValue] = React.useState(yield* useOnce(() => - Effect.all(refs as readonly SubscriptionRef.SubscriptionRef[]) - )) - - yield* useFork(() => pipe( - refs.map(ref => Stream.changesWith(ref.changes, Equivalence.strict())), - streams => Stream.zipLatestAll(...streams), - Stream.runForEach(v => - Effect.sync(() => setReactStateValue(v)) - ), - ), refs) - - return reactStateValue as any -}) - -export const useRefState: { - ( - ref: SubscriptionRef.SubscriptionRef - ): Effect.Effect>]> -} = Effect.fn("useRefState")(function* (ref: SubscriptionRef.SubscriptionRef) { - const [reactStateValue, setReactStateValue] = React.useState(yield* useOnce(() => ref)) - - yield* useFork(() => Stream.runForEach( - Stream.changesWith(ref.changes, Equivalence.strict()), - v => Effect.sync(() => setReactStateValue(v)), - ), [ref]) - - const setValue = yield* useCallbackSync((setStateAction: React.SetStateAction) => - Ref.update(ref, prevState => - SetStateAction.value(setStateAction, prevState) - ), - [ref]) - - return [reactStateValue, setValue] -}) - - -export const useStreamFromReactiveValues: { - ( - values: A - ): Effect.Effect, never, Scope.Scope> -} = Effect.fn("useStreamFromReactiveValues")(function* (values: A) { - const { latest, pubsub, stream } = yield* useOnce(() => Effect.Do.pipe( - Effect.bind("latest", () => Ref.make(values)), - Effect.bind("pubsub", () => Effect.acquireRelease(PubSub.unbounded(), PubSub.shutdown)), - Effect.let("stream", ({ latest, pubsub }) => latest.pipe( - Effect.flatMap(a => Effect.map( - Stream.fromPubSub(pubsub, { scoped: true }), - s => Stream.concat(Stream.make(a), s), - )), - Stream.unwrapScoped, - )), - )) - - yield* useEffect(() => Ref.set(latest, values).pipe( - Effect.andThen(PubSub.publish(pubsub, values)), - Effect.unlessEffect(PubSub.isShutdown(pubsub)), - ), values) - - return stream -}) - -export const useSubscribeStream: { - ( - stream: Stream.Stream - ): Effect.Effect, never, R> - , E, R>( - stream: Stream.Stream, - initialValue: A, - ): Effect.Effect, never, R> -} = Effect.fn("useSubscribeStream")(function* , E, R>( - stream: Stream.Stream, - initialValue?: A, -) { - const [reactStateValue, setReactStateValue] = React.useState( - React.useMemo(() => initialValue - ? Option.some(initialValue) - : Option.none(), - []) - ) - - yield* useFork(() => Stream.runForEach( - Stream.changesWith(stream, Equivalence.strict()), - v => Effect.sync(() => setReactStateValue(Option.some(v))), - ), [stream]) - - return reactStateValue as Option.Some -}) diff --git a/packages/effect-fc/src/Hook/ScopeOptions.ts b/packages/effect-fc/src/Hook/ScopeOptions.ts new file mode 100644 index 0000000..c2654e8 --- /dev/null +++ b/packages/effect-fc/src/Hook/ScopeOptions.ts @@ -0,0 +1,7 @@ +import type { ExecutionStrategy } from "effect" + + +export interface ScopeOptions { + readonly finalizerExecutionMode?: "sync" | "fork" + readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy +} diff --git a/packages/effect-fc/src/Hook/index.ts b/packages/effect-fc/src/Hook/index.ts new file mode 100644 index 0000000..5d0e295 --- /dev/null +++ b/packages/effect-fc/src/Hook/index.ts @@ -0,0 +1,14 @@ +export * from "./ScopeOptions.js" +export * from "./useCallbackPromise.js" +export * from "./useCallbackSync.js" +export * from "./useContext.js" +export * from "./useEffect.js" +export * from "./useLayoutEffect.js" +export * from "./useMemo.js" +export * from "./useOnce.js" +export * from "./useRefFromReactiveValue.js" +export * from "./useRefState.js" +export * from "./useScope.js" +export * from "./useStreamFromReactiveValues.js" +export * from "./useSubscribeRefs.js" +export * from "./useSubscribeStream.js" diff --git a/packages/effect-fc/src/Hook/internal.ts b/packages/effect-fc/src/Hook/internal.ts new file mode 100644 index 0000000..4c9a35e --- /dev/null +++ b/packages/effect-fc/src/Hook/internal.ts @@ -0,0 +1,18 @@ +import { Exit, Runtime, Scope } from "effect" +import type { ScopeOptions } from "./ScopeOptions.js" + + +export const closeScope = ( + scope: Scope.CloseableScope, + runtime: Runtime.Runtime, + options?: ScopeOptions, +) => { + switch (options?.finalizerExecutionMode ?? "sync") { + case "sync": + Runtime.runSync(runtime)(Scope.close(scope, Exit.void)) + break + case "fork": + Runtime.runFork(runtime)(Scope.close(scope, Exit.void)) + break + } +} diff --git a/packages/effect-fc/src/Hook/useCallbackPromise.ts b/packages/effect-fc/src/Hook/useCallbackPromise.ts new file mode 100644 index 0000000..5e093ef --- /dev/null +++ b/packages/effect-fc/src/Hook/useCallbackPromise.ts @@ -0,0 +1,16 @@ +import { Effect, Runtime } from "effect" +import * as React from "react" + + +export const useCallbackPromise: { + ( + callback: (...args: Args) => Effect.Effect, + deps: React.DependencyList, + ): Effect.Effect<(...args: Args) => Promise, never, R> +} = Effect.fn("useCallbackPromise")(function* ( + callback: (...args: Args) => Effect.Effect, + deps: React.DependencyList, +) { + const runtime = yield* Effect.runtime() + return React.useCallback((...args: Args) => Runtime.runPromise(runtime)(callback(...args)), deps) +}) diff --git a/packages/effect-fc/src/Hook/useCallbackSync.ts b/packages/effect-fc/src/Hook/useCallbackSync.ts new file mode 100644 index 0000000..07ffd84 --- /dev/null +++ b/packages/effect-fc/src/Hook/useCallbackSync.ts @@ -0,0 +1,16 @@ +import { Effect, Runtime } from "effect" +import * as React from "react" + + +export const useCallbackSync: { + ( + callback: (...args: Args) => Effect.Effect, + deps: React.DependencyList, + ): Effect.Effect<(...args: Args) => A, never, R> +} = Effect.fn("useCallbackSync")(function* ( + callback: (...args: Args) => Effect.Effect, + deps: React.DependencyList, +) { + const runtime = yield* Effect.runtime() + return React.useCallback((...args: Args) => Runtime.runSync(runtime)(callback(...args)), deps) +}) diff --git a/packages/effect-fc/src/Hook/useContext.ts b/packages/effect-fc/src/Hook/useContext.ts new file mode 100644 index 0000000..fbb9d85 --- /dev/null +++ b/packages/effect-fc/src/Hook/useContext.ts @@ -0,0 +1,23 @@ +import { type Context, Effect, type Layer, Scope } from "effect" +import type { ScopeOptions } from "./ScopeOptions.js" +import { useMemo } from "./useMemo.js" +import { useScope } from "./useScope.js" + + +export const useContext: { + ( + layer: Layer.Layer, + options?: ScopeOptions, + ): Effect.Effect, E, Exclude> +} = Effect.fn("useContext")(function* ( + layer: Layer.Layer, + options?: ScopeOptions, +) { + const scope = yield* useScope([layer], options) + + return yield* useMemo(() => Effect.provideService( + Effect.provide(Effect.context(), layer), + Scope.Scope, + scope, + ), [scope]) +}) diff --git a/packages/effect-fc/src/Hook/useEffect.ts b/packages/effect-fc/src/Hook/useEffect.ts new file mode 100644 index 0000000..798d39f --- /dev/null +++ b/packages/effect-fc/src/Hook/useEffect.ts @@ -0,0 +1,28 @@ +import { Effect, ExecutionStrategy, Runtime, Scope } from "effect" +import * as React from "react" +import type { ScopeOptions } from "./ScopeOptions.js" +import { closeScope } from "./internal.js" + + +export const useEffect: { + ( + effect: () => Effect.Effect, + deps?: React.DependencyList, + options?: ScopeOptions, + ): Effect.Effect> +} = Effect.fn("useEffect")(function* ( + effect: () => Effect.Effect, + deps?: React.DependencyList, + options?: ScopeOptions, +) { + const runtime = yield* Effect.runtime>() + + React.useEffect(() => Effect.Do.pipe( + Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)), + Effect.bind("exit", ({ scope }) => Effect.exit(Effect.provideService(effect(), Scope.Scope, scope))), + Effect.map(({ scope }) => + () => closeScope(scope, runtime, options) + ), + Runtime.runSync(runtime), + ), deps) +}) diff --git a/packages/effect-fc/src/Hook/useFork.ts b/packages/effect-fc/src/Hook/useFork.ts new file mode 100644 index 0000000..ec17db6 --- /dev/null +++ b/packages/effect-fc/src/Hook/useFork.ts @@ -0,0 +1,31 @@ +import { Effect, ExecutionStrategy, Runtime, Scope } from "effect" +import * as React from "react" +import { closeScope } from "./internal.js" +import type { ScopeOptions } from "./ScopeOptions.js" + + +export const useFork: { + ( + effect: () => Effect.Effect, + deps?: React.DependencyList, + options?: Runtime.RunForkOptions & ScopeOptions, + ): Effect.Effect> +} = Effect.fn("useFork")(function* ( + effect: () => Effect.Effect, + deps?: React.DependencyList, + options?: Runtime.RunForkOptions & ScopeOptions, +) { + const runtime = yield* Effect.runtime>() + + React.useEffect(() => { + const scope = Runtime.runSync(runtime)(options?.scope + ? Scope.fork(options.scope, options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential) + : Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential) + ) + Runtime.runFork(runtime)(Effect.provideService(effect(), Scope.Scope, scope), { ...options, scope }) + return () => closeScope(scope, runtime, { + ...options, + finalizerExecutionMode: options?.finalizerExecutionMode ?? "fork", + }) + }, deps) +}) diff --git a/packages/effect-fc/src/Hook/useLayoutEffect.ts b/packages/effect-fc/src/Hook/useLayoutEffect.ts new file mode 100644 index 0000000..988fd14 --- /dev/null +++ b/packages/effect-fc/src/Hook/useLayoutEffect.ts @@ -0,0 +1,28 @@ +import { Effect, ExecutionStrategy, Runtime, Scope } from "effect" +import * as React from "react" +import type { ScopeOptions } from "./ScopeOptions.js" +import { closeScope } from "./internal.js" + + +export const useLayoutEffect: { + ( + effect: () => Effect.Effect, + deps?: React.DependencyList, + options?: ScopeOptions, + ): Effect.Effect> +} = Effect.fn("useLayoutEffect")(function* ( + effect: () => Effect.Effect, + deps?: React.DependencyList, + options?: ScopeOptions, +) { + const runtime = yield* Effect.runtime>() + + React.useLayoutEffect(() => Effect.Do.pipe( + Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)), + Effect.bind("exit", ({ scope }) => Effect.exit(Effect.provideService(effect(), Scope.Scope, scope))), + Effect.map(({ scope }) => + () => closeScope(scope, runtime, options) + ), + Runtime.runSync(runtime), + ), deps) +}) diff --git a/packages/effect-fc/src/Hook/useMemo.ts b/packages/effect-fc/src/Hook/useMemo.ts new file mode 100644 index 0000000..53b1aa8 --- /dev/null +++ b/packages/effect-fc/src/Hook/useMemo.ts @@ -0,0 +1,16 @@ +import { Effect, Runtime } from "effect" +import * as React from "react" + + +export const useMemo: { + ( + factory: () => Effect.Effect, + deps: React.DependencyList, + ): Effect.Effect +} = Effect.fn("useMemo")(function* ( + factory: () => Effect.Effect, + deps: React.DependencyList, +) { + const runtime = yield* Effect.runtime() + return yield* React.useMemo(() => Runtime.runSync(runtime)(Effect.cached(factory())), deps) +}) diff --git a/packages/effect-fc/src/Hook/useOnce.ts b/packages/effect-fc/src/Hook/useOnce.ts new file mode 100644 index 0000000..3598d56 --- /dev/null +++ b/packages/effect-fc/src/Hook/useOnce.ts @@ -0,0 +1,11 @@ +import { Effect } from "effect" +import { useMemo } from "./useMemo.js" + + +export const useOnce: { + (factory: () => Effect.Effect): Effect.Effect +} = Effect.fn("useOnce")(function* ( + factory: () => Effect.Effect +) { + return yield* useMemo(factory, []) +}) diff --git a/packages/effect-fc/src/Hook/useRefFromReactiveValue.ts b/packages/effect-fc/src/Hook/useRefFromReactiveValue.ts new file mode 100644 index 0000000..9ff1e6f --- /dev/null +++ b/packages/effect-fc/src/Hook/useRefFromReactiveValue.ts @@ -0,0 +1,12 @@ +import { Effect, Ref, SubscriptionRef } from "effect" +import { useEffect } from "./useEffect.js" +import { useOnce } from "./useOnce.js" + + +export const useRefFromReactiveValue: { + (value: A): Effect.Effect> +} = Effect.fn("useRefFromReactiveValue")(function*(value) { + const ref = yield* useOnce(() => SubscriptionRef.make(value)) + yield* useEffect(() => Ref.set(ref, value), [value]) + return ref +}) diff --git a/packages/effect-fc/src/Hook/useRefState.ts b/packages/effect-fc/src/Hook/useRefState.ts new file mode 100644 index 0000000..d1fa07f --- /dev/null +++ b/packages/effect-fc/src/Hook/useRefState.ts @@ -0,0 +1,28 @@ +import { Effect, Equivalence, Ref, Stream, SubscriptionRef } from "effect" +import * as React from "react" +import { SetStateAction } from "../types/index.js" +import { useCallbackSync } from "./useCallbackSync.js" +import { useFork } from "./useFork.js" +import { useOnce } from "./useOnce.js" + + +export const useRefState: { + ( + ref: SubscriptionRef.SubscriptionRef + ): Effect.Effect>]> +} = Effect.fn("useRefState")(function* (ref: SubscriptionRef.SubscriptionRef) { + const [reactStateValue, setReactStateValue] = React.useState(yield* useOnce(() => ref)) + + yield* useFork(() => Stream.runForEach( + Stream.changesWith(ref.changes, Equivalence.strict()), + v => Effect.sync(() => setReactStateValue(v)), + ), [ref]) + + const setValue = yield* useCallbackSync((setStateAction: React.SetStateAction) => + Ref.update(ref, prevState => + SetStateAction.value(setStateAction, prevState) + ), + [ref]) + + return [reactStateValue, setValue] +}) diff --git a/packages/effect-fc/src/Hook/useScope.ts b/packages/effect-fc/src/Hook/useScope.ts new file mode 100644 index 0000000..aae4a49 --- /dev/null +++ b/packages/effect-fc/src/Hook/useScope.ts @@ -0,0 +1,36 @@ +import { Effect, ExecutionStrategy, Ref, Runtime, Scope } from "effect" +import * as React from "react" +import type { ScopeOptions } from "./ScopeOptions.js" +import { closeScope } from "./internal.js" + + +export const useScope: { + ( + deps: React.DependencyList, + options?: ScopeOptions, + ): Effect.Effect +} = Effect.fn("useScope")(function*(deps, options) { + const runtime = yield* Effect.runtime() + + const [isInitialRun, initialScope] = React.useMemo(() => Runtime.runSync(runtime)(Effect.all([ + Ref.make(true), + Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential), + ])), []) + const [scope, setScope] = React.useState(initialScope) + + React.useEffect(() => Runtime.runSync(runtime)( + Effect.if(isInitialRun, { + onTrue: () => Effect.as( + Ref.set(isInitialRun, false), + () => closeScope(scope, runtime, options), + ), + + onFalse: () => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential).pipe( + Effect.tap(scope => Effect.sync(() => setScope(scope))), + Effect.map(scope => () => closeScope(scope, runtime, options)), + ), + }) + ), deps) + + return scope +}) diff --git a/packages/effect-fc/src/Hook/useStreamFromReactiveValues.ts b/packages/effect-fc/src/Hook/useStreamFromReactiveValues.ts new file mode 100644 index 0000000..58e3840 --- /dev/null +++ b/packages/effect-fc/src/Hook/useStreamFromReactiveValues.ts @@ -0,0 +1,30 @@ +import { Effect, PubSub, Ref, Scope, Stream } from "effect" +import * as React from "react" +import { useEffect } from "./useEffect.js" +import { useOnce } from "./useOnce.js" + + +export const useStreamFromReactiveValues: { + ( + values: A + ): Effect.Effect, never, Scope.Scope> +} = Effect.fn("useStreamFromReactiveValues")(function* (values: A) { + const { latest, pubsub, stream } = yield* useOnce(() => Effect.Do.pipe( + Effect.bind("latest", () => Ref.make(values)), + Effect.bind("pubsub", () => Effect.acquireRelease(PubSub.unbounded(), PubSub.shutdown)), + Effect.let("stream", ({ latest, pubsub }) => latest.pipe( + Effect.flatMap(a => Effect.map( + Stream.fromPubSub(pubsub, { scoped: true }), + s => Stream.concat(Stream.make(a), s), + )), + Stream.unwrapScoped, + )), + )) + + yield* useEffect(() => Ref.set(latest, values).pipe( + Effect.andThen(PubSub.publish(pubsub, values)), + Effect.unlessEffect(PubSub.isShutdown(pubsub)), + ), values) + + return stream +}) diff --git a/packages/effect-fc/src/Hook/useSubscribeRefs.ts b/packages/effect-fc/src/Hook/useSubscribeRefs.ts new file mode 100644 index 0000000..4f18976 --- /dev/null +++ b/packages/effect-fc/src/Hook/useSubscribeRefs.ts @@ -0,0 +1,27 @@ +import { Effect, Equivalence, pipe, Stream, SubscriptionRef } from "effect" +import * as React from "react" +import { useFork } from "./useFork.js" +import { useOnce } from "./useOnce.js" + + +export const useSubscribeRefs: { + []>( + ...refs: Refs + ): Effect.Effect<{ [K in keyof Refs]: Effect.Effect.Success }> +} = Effect.fn("useSubscribeRefs")(function* []>( + ...refs: Refs +) { + const [reactStateValue, setReactStateValue] = React.useState(yield* useOnce(() => + Effect.all(refs as readonly SubscriptionRef.SubscriptionRef[]) + )) + + yield* useFork(() => pipe( + refs.map(ref => Stream.changesWith(ref.changes, Equivalence.strict())), + streams => Stream.zipLatestAll(...streams), + Stream.runForEach(v => + Effect.sync(() => setReactStateValue(v)) + ), + ), refs) + + return reactStateValue as any +}) diff --git a/packages/effect-fc/src/Hook/useSubscribeStream.ts b/packages/effect-fc/src/Hook/useSubscribeStream.ts new file mode 100644 index 0000000..d157111 --- /dev/null +++ b/packages/effect-fc/src/Hook/useSubscribeStream.ts @@ -0,0 +1,31 @@ +import { Effect, Equivalence, Option, Stream } from "effect" +import * as React from "react" +import { useFork } from "./useFork.js" + + +export const useSubscribeStream: { + ( + stream: Stream.Stream + ): Effect.Effect, never, R> + , E, R>( + stream: Stream.Stream, + initialValue: A, + ): Effect.Effect, never, R> +} = Effect.fn("useSubscribeStream")(function* , E, R>( + stream: Stream.Stream, + initialValue?: A, +) { + const [reactStateValue, setReactStateValue] = React.useState( + React.useMemo(() => initialValue + ? Option.some(initialValue) + : Option.none(), + []) + ) + + yield* useFork(() => Stream.runForEach( + Stream.changesWith(stream, Equivalence.strict()), + v => Effect.sync(() => setReactStateValue(Option.some(v))), + ), [stream]) + + return reactStateValue as Option.Some +}) diff --git a/packages/effect-fc/src/index.ts b/packages/effect-fc/src/index.ts index fd1ade9..6d643c6 100644 --- a/packages/effect-fc/src/index.ts +++ b/packages/effect-fc/src/index.ts @@ -1,5 +1,5 @@ export * as Component from "./Component.js" -export * as Hook from "./Hook.js" +export * as Hook from "./Hook/index.js" export * as Memoized from "./Memoized.js" export * as ReactManagedRuntime from "./ReactManagedRuntime.js" export * as Suspense from "./Suspense.js" -- 2.49.1 From bada57a591d9c8bc5ed330d0a9aed45dd16379d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Mon, 28 Jul 2025 04:02:55 +0200 Subject: [PATCH 06/90] Refactoring --- packages/effect-fc/package.json | 4 ++++ packages/effect-fc/src/Component.ts | 4 ++-- packages/effect-fc/src/{Hook/index.ts => hooks/Hooks.ts} | 0 packages/effect-fc/src/{Hook => hooks}/ScopeOptions.ts | 0 packages/effect-fc/src/hooks/index.ts | 2 ++ packages/effect-fc/src/{Hook => hooks}/internal.ts | 0 packages/effect-fc/src/{Hook => hooks}/useCallbackPromise.ts | 0 packages/effect-fc/src/{Hook => hooks}/useCallbackSync.ts | 0 packages/effect-fc/src/{Hook => hooks}/useContext.ts | 0 packages/effect-fc/src/{Hook => hooks}/useEffect.ts | 0 packages/effect-fc/src/{Hook => hooks}/useFork.ts | 0 packages/effect-fc/src/{Hook => hooks}/useLayoutEffect.ts | 0 packages/effect-fc/src/{Hook => hooks}/useMemo.ts | 0 packages/effect-fc/src/{Hook => hooks}/useOnce.ts | 0 .../effect-fc/src/{Hook => hooks}/useRefFromReactiveValue.ts | 0 packages/effect-fc/src/{Hook => hooks}/useRefState.ts | 0 packages/effect-fc/src/{Hook => hooks}/useScope.ts | 0 .../src/{Hook => hooks}/useStreamFromReactiveValues.ts | 0 packages/effect-fc/src/{Hook => hooks}/useSubscribeRefs.ts | 0 packages/effect-fc/src/{Hook => hooks}/useSubscribeStream.ts | 0 packages/effect-fc/src/index.ts | 1 - 21 files changed, 8 insertions(+), 3 deletions(-) rename packages/effect-fc/src/{Hook/index.ts => hooks/Hooks.ts} (100%) rename packages/effect-fc/src/{Hook => hooks}/ScopeOptions.ts (100%) create mode 100644 packages/effect-fc/src/hooks/index.ts rename packages/effect-fc/src/{Hook => hooks}/internal.ts (100%) rename packages/effect-fc/src/{Hook => hooks}/useCallbackPromise.ts (100%) rename packages/effect-fc/src/{Hook => hooks}/useCallbackSync.ts (100%) rename packages/effect-fc/src/{Hook => hooks}/useContext.ts (100%) rename packages/effect-fc/src/{Hook => hooks}/useEffect.ts (100%) rename packages/effect-fc/src/{Hook => hooks}/useFork.ts (100%) rename packages/effect-fc/src/{Hook => hooks}/useLayoutEffect.ts (100%) rename packages/effect-fc/src/{Hook => hooks}/useMemo.ts (100%) rename packages/effect-fc/src/{Hook => hooks}/useOnce.ts (100%) rename packages/effect-fc/src/{Hook => hooks}/useRefFromReactiveValue.ts (100%) rename packages/effect-fc/src/{Hook => hooks}/useRefState.ts (100%) rename packages/effect-fc/src/{Hook => hooks}/useScope.ts (100%) rename packages/effect-fc/src/{Hook => hooks}/useStreamFromReactiveValues.ts (100%) rename packages/effect-fc/src/{Hook => hooks}/useSubscribeRefs.ts (100%) rename packages/effect-fc/src/{Hook => hooks}/useSubscribeStream.ts (100%) diff --git a/packages/effect-fc/package.json b/packages/effect-fc/package.json index 8994f33..27e9ca9 100644 --- a/packages/effect-fc/package.json +++ b/packages/effect-fc/package.json @@ -17,6 +17,10 @@ "types": "./dist/index.d.ts", "default": "./dist/index.js" }, + "./hooks": { + "types": "./dist/types/hooks.d.ts", + "default": "./dist/types/hooks.js" + }, "./types": { "types": "./dist/types/index.d.ts", "default": "./dist/types/index.js" diff --git a/packages/effect-fc/src/Component.ts b/packages/effect-fc/src/Component.ts index 98089ee..6cf7585 100644 --- a/packages/effect-fc/src/Component.ts +++ b/packages/effect-fc/src/Component.ts @@ -1,6 +1,6 @@ import { Context, Effect, Effectable, ExecutionStrategy, Function, Predicate, Runtime, Scope, String, Tracer, type Utils } from "effect" import * as React from "react" -import * as Hook from "./Hook/index.js" +import { Hooks } from "./hooks/index.js" import * as Memoized from "./Memoized.js" @@ -40,7 +40,7 @@ const ComponentProto = Object.freeze({ runtimeRef.current = yield* Effect.runtime>() return React.useCallback(function ScopeProvider(props: P) { - const scope = Runtime.runSync(runtimeRef.current)(Hook.useScope( + const scope = Runtime.runSync(runtimeRef.current)(Hooks.useScope( Array.from( Context.omit(...nonReactiveTags)(runtimeRef.current.context).unsafeMap.values() ), diff --git a/packages/effect-fc/src/Hook/index.ts b/packages/effect-fc/src/hooks/Hooks.ts similarity index 100% rename from packages/effect-fc/src/Hook/index.ts rename to packages/effect-fc/src/hooks/Hooks.ts diff --git a/packages/effect-fc/src/Hook/ScopeOptions.ts b/packages/effect-fc/src/hooks/ScopeOptions.ts similarity index 100% rename from packages/effect-fc/src/Hook/ScopeOptions.ts rename to packages/effect-fc/src/hooks/ScopeOptions.ts diff --git a/packages/effect-fc/src/hooks/index.ts b/packages/effect-fc/src/hooks/index.ts new file mode 100644 index 0000000..e1811a5 --- /dev/null +++ b/packages/effect-fc/src/hooks/index.ts @@ -0,0 +1,2 @@ +export * from "./Hooks.js" +export * as Hooks from "./Hooks.js" diff --git a/packages/effect-fc/src/Hook/internal.ts b/packages/effect-fc/src/hooks/internal.ts similarity index 100% rename from packages/effect-fc/src/Hook/internal.ts rename to packages/effect-fc/src/hooks/internal.ts diff --git a/packages/effect-fc/src/Hook/useCallbackPromise.ts b/packages/effect-fc/src/hooks/useCallbackPromise.ts similarity index 100% rename from packages/effect-fc/src/Hook/useCallbackPromise.ts rename to packages/effect-fc/src/hooks/useCallbackPromise.ts diff --git a/packages/effect-fc/src/Hook/useCallbackSync.ts b/packages/effect-fc/src/hooks/useCallbackSync.ts similarity index 100% rename from packages/effect-fc/src/Hook/useCallbackSync.ts rename to packages/effect-fc/src/hooks/useCallbackSync.ts diff --git a/packages/effect-fc/src/Hook/useContext.ts b/packages/effect-fc/src/hooks/useContext.ts similarity index 100% rename from packages/effect-fc/src/Hook/useContext.ts rename to packages/effect-fc/src/hooks/useContext.ts diff --git a/packages/effect-fc/src/Hook/useEffect.ts b/packages/effect-fc/src/hooks/useEffect.ts similarity index 100% rename from packages/effect-fc/src/Hook/useEffect.ts rename to packages/effect-fc/src/hooks/useEffect.ts diff --git a/packages/effect-fc/src/Hook/useFork.ts b/packages/effect-fc/src/hooks/useFork.ts similarity index 100% rename from packages/effect-fc/src/Hook/useFork.ts rename to packages/effect-fc/src/hooks/useFork.ts diff --git a/packages/effect-fc/src/Hook/useLayoutEffect.ts b/packages/effect-fc/src/hooks/useLayoutEffect.ts similarity index 100% rename from packages/effect-fc/src/Hook/useLayoutEffect.ts rename to packages/effect-fc/src/hooks/useLayoutEffect.ts diff --git a/packages/effect-fc/src/Hook/useMemo.ts b/packages/effect-fc/src/hooks/useMemo.ts similarity index 100% rename from packages/effect-fc/src/Hook/useMemo.ts rename to packages/effect-fc/src/hooks/useMemo.ts diff --git a/packages/effect-fc/src/Hook/useOnce.ts b/packages/effect-fc/src/hooks/useOnce.ts similarity index 100% rename from packages/effect-fc/src/Hook/useOnce.ts rename to packages/effect-fc/src/hooks/useOnce.ts diff --git a/packages/effect-fc/src/Hook/useRefFromReactiveValue.ts b/packages/effect-fc/src/hooks/useRefFromReactiveValue.ts similarity index 100% rename from packages/effect-fc/src/Hook/useRefFromReactiveValue.ts rename to packages/effect-fc/src/hooks/useRefFromReactiveValue.ts diff --git a/packages/effect-fc/src/Hook/useRefState.ts b/packages/effect-fc/src/hooks/useRefState.ts similarity index 100% rename from packages/effect-fc/src/Hook/useRefState.ts rename to packages/effect-fc/src/hooks/useRefState.ts diff --git a/packages/effect-fc/src/Hook/useScope.ts b/packages/effect-fc/src/hooks/useScope.ts similarity index 100% rename from packages/effect-fc/src/Hook/useScope.ts rename to packages/effect-fc/src/hooks/useScope.ts diff --git a/packages/effect-fc/src/Hook/useStreamFromReactiveValues.ts b/packages/effect-fc/src/hooks/useStreamFromReactiveValues.ts similarity index 100% rename from packages/effect-fc/src/Hook/useStreamFromReactiveValues.ts rename to packages/effect-fc/src/hooks/useStreamFromReactiveValues.ts diff --git a/packages/effect-fc/src/Hook/useSubscribeRefs.ts b/packages/effect-fc/src/hooks/useSubscribeRefs.ts similarity index 100% rename from packages/effect-fc/src/Hook/useSubscribeRefs.ts rename to packages/effect-fc/src/hooks/useSubscribeRefs.ts diff --git a/packages/effect-fc/src/Hook/useSubscribeStream.ts b/packages/effect-fc/src/hooks/useSubscribeStream.ts similarity index 100% rename from packages/effect-fc/src/Hook/useSubscribeStream.ts rename to packages/effect-fc/src/hooks/useSubscribeStream.ts diff --git a/packages/effect-fc/src/index.ts b/packages/effect-fc/src/index.ts index 6d643c6..9676bf1 100644 --- a/packages/effect-fc/src/index.ts +++ b/packages/effect-fc/src/index.ts @@ -1,5 +1,4 @@ export * as Component from "./Component.js" -export * as Hook from "./Hook/index.js" export * as Memoized from "./Memoized.js" export * as ReactManagedRuntime from "./ReactManagedRuntime.js" export * as Suspense from "./Suspense.js" -- 2.49.1 From 6b39671d60ea134671933c523795e50d3b174cf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Mon, 28 Jul 2025 04:20:04 +0200 Subject: [PATCH 07/90] Fix --- packages/effect-fc/src/Component.ts | 2 +- packages/effect-fc/src/hooks/useCallbackPromise.ts | 6 ++++-- packages/effect-fc/src/hooks/useCallbackSync.ts | 6 ++++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/effect-fc/src/Component.ts b/packages/effect-fc/src/Component.ts index 6cf7585..36205bf 100644 --- a/packages/effect-fc/src/Component.ts +++ b/packages/effect-fc/src/Component.ts @@ -34,7 +34,7 @@ const ComponentProto = Object.freeze({ ...Effectable.CommitPrototype, [TypeId]: TypeId, - commit: Effect.fn("Component")(function*

(this: Component) { + commit: Effect.fnUntraced(function*

(this: Component) { const self = this const runtimeRef = React.useRef>>(null!) runtimeRef.current = yield* Effect.runtime>() diff --git a/packages/effect-fc/src/hooks/useCallbackPromise.ts b/packages/effect-fc/src/hooks/useCallbackPromise.ts index 5e093ef..be075ea 100644 --- a/packages/effect-fc/src/hooks/useCallbackPromise.ts +++ b/packages/effect-fc/src/hooks/useCallbackPromise.ts @@ -11,6 +11,8 @@ export const useCallbackPromise: { callback: (...args: Args) => Effect.Effect, deps: React.DependencyList, ) { - const runtime = yield* Effect.runtime() - return React.useCallback((...args: Args) => Runtime.runPromise(runtime)(callback(...args)), deps) + const runtimeRef = React.useRef>(null!) + runtimeRef.current = yield* Effect.runtime() + + return React.useCallback((...args: Args) => Runtime.runPromise(runtimeRef.current)(callback(...args)), deps) }) diff --git a/packages/effect-fc/src/hooks/useCallbackSync.ts b/packages/effect-fc/src/hooks/useCallbackSync.ts index 07ffd84..44a41cc 100644 --- a/packages/effect-fc/src/hooks/useCallbackSync.ts +++ b/packages/effect-fc/src/hooks/useCallbackSync.ts @@ -11,6 +11,8 @@ export const useCallbackSync: { callback: (...args: Args) => Effect.Effect, deps: React.DependencyList, ) { - const runtime = yield* Effect.runtime() - return React.useCallback((...args: Args) => Runtime.runSync(runtime)(callback(...args)), deps) + const runtimeRef = React.useRef>(null!) + runtimeRef.current = yield* Effect.runtime() + + return React.useCallback((...args: Args) => Runtime.runSync(runtimeRef.current)(callback(...args)), deps) }) -- 2.49.1 From 55ca8a0dd48c9845c8a4cfcd5b94e6486bdd696a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Mon, 28 Jul 2025 04:25:41 +0200 Subject: [PATCH 08/90] Refactoring --- packages/effect-fc/src/hooks/{ => Hooks}/ScopeOptions.ts | 0 packages/effect-fc/src/hooks/{Hooks.ts => Hooks/index.ts} | 0 packages/effect-fc/src/hooks/{ => Hooks}/internal.ts | 0 .../effect-fc/src/hooks/{ => Hooks}/useCallbackPromise.ts | 0 packages/effect-fc/src/hooks/{ => Hooks}/useCallbackSync.ts | 0 packages/effect-fc/src/hooks/{ => Hooks}/useContext.ts | 0 packages/effect-fc/src/hooks/{ => Hooks}/useEffect.ts | 0 packages/effect-fc/src/hooks/{ => Hooks}/useFork.ts | 0 packages/effect-fc/src/hooks/{ => Hooks}/useLayoutEffect.ts | 0 packages/effect-fc/src/hooks/{ => Hooks}/useMemo.ts | 0 packages/effect-fc/src/hooks/{ => Hooks}/useOnce.ts | 0 .../src/hooks/{ => Hooks}/useRefFromReactiveValue.ts | 0 packages/effect-fc/src/hooks/{ => Hooks}/useRefState.ts | 2 +- packages/effect-fc/src/hooks/{ => Hooks}/useScope.ts | 0 .../src/hooks/{ => Hooks}/useStreamFromReactiveValues.ts | 0 packages/effect-fc/src/hooks/{ => Hooks}/useSubscribeRefs.ts | 0 .../effect-fc/src/hooks/{ => Hooks}/useSubscribeStream.ts | 0 packages/effect-fc/src/hooks/index.ts | 4 ++-- 18 files changed, 3 insertions(+), 3 deletions(-) rename packages/effect-fc/src/hooks/{ => Hooks}/ScopeOptions.ts (100%) rename packages/effect-fc/src/hooks/{Hooks.ts => Hooks/index.ts} (100%) rename packages/effect-fc/src/hooks/{ => Hooks}/internal.ts (100%) rename packages/effect-fc/src/hooks/{ => Hooks}/useCallbackPromise.ts (100%) rename packages/effect-fc/src/hooks/{ => Hooks}/useCallbackSync.ts (100%) rename packages/effect-fc/src/hooks/{ => Hooks}/useContext.ts (100%) rename packages/effect-fc/src/hooks/{ => Hooks}/useEffect.ts (100%) rename packages/effect-fc/src/hooks/{ => Hooks}/useFork.ts (100%) rename packages/effect-fc/src/hooks/{ => Hooks}/useLayoutEffect.ts (100%) rename packages/effect-fc/src/hooks/{ => Hooks}/useMemo.ts (100%) rename packages/effect-fc/src/hooks/{ => Hooks}/useOnce.ts (100%) rename packages/effect-fc/src/hooks/{ => Hooks}/useRefFromReactiveValue.ts (100%) rename packages/effect-fc/src/hooks/{ => Hooks}/useRefState.ts (94%) rename packages/effect-fc/src/hooks/{ => Hooks}/useScope.ts (100%) rename packages/effect-fc/src/hooks/{ => Hooks}/useStreamFromReactiveValues.ts (100%) rename packages/effect-fc/src/hooks/{ => Hooks}/useSubscribeRefs.ts (100%) rename packages/effect-fc/src/hooks/{ => Hooks}/useSubscribeStream.ts (100%) diff --git a/packages/effect-fc/src/hooks/ScopeOptions.ts b/packages/effect-fc/src/hooks/Hooks/ScopeOptions.ts similarity index 100% rename from packages/effect-fc/src/hooks/ScopeOptions.ts rename to packages/effect-fc/src/hooks/Hooks/ScopeOptions.ts diff --git a/packages/effect-fc/src/hooks/Hooks.ts b/packages/effect-fc/src/hooks/Hooks/index.ts similarity index 100% rename from packages/effect-fc/src/hooks/Hooks.ts rename to packages/effect-fc/src/hooks/Hooks/index.ts diff --git a/packages/effect-fc/src/hooks/internal.ts b/packages/effect-fc/src/hooks/Hooks/internal.ts similarity index 100% rename from packages/effect-fc/src/hooks/internal.ts rename to packages/effect-fc/src/hooks/Hooks/internal.ts diff --git a/packages/effect-fc/src/hooks/useCallbackPromise.ts b/packages/effect-fc/src/hooks/Hooks/useCallbackPromise.ts similarity index 100% rename from packages/effect-fc/src/hooks/useCallbackPromise.ts rename to packages/effect-fc/src/hooks/Hooks/useCallbackPromise.ts diff --git a/packages/effect-fc/src/hooks/useCallbackSync.ts b/packages/effect-fc/src/hooks/Hooks/useCallbackSync.ts similarity index 100% rename from packages/effect-fc/src/hooks/useCallbackSync.ts rename to packages/effect-fc/src/hooks/Hooks/useCallbackSync.ts diff --git a/packages/effect-fc/src/hooks/useContext.ts b/packages/effect-fc/src/hooks/Hooks/useContext.ts similarity index 100% rename from packages/effect-fc/src/hooks/useContext.ts rename to packages/effect-fc/src/hooks/Hooks/useContext.ts diff --git a/packages/effect-fc/src/hooks/useEffect.ts b/packages/effect-fc/src/hooks/Hooks/useEffect.ts similarity index 100% rename from packages/effect-fc/src/hooks/useEffect.ts rename to packages/effect-fc/src/hooks/Hooks/useEffect.ts diff --git a/packages/effect-fc/src/hooks/useFork.ts b/packages/effect-fc/src/hooks/Hooks/useFork.ts similarity index 100% rename from packages/effect-fc/src/hooks/useFork.ts rename to packages/effect-fc/src/hooks/Hooks/useFork.ts diff --git a/packages/effect-fc/src/hooks/useLayoutEffect.ts b/packages/effect-fc/src/hooks/Hooks/useLayoutEffect.ts similarity index 100% rename from packages/effect-fc/src/hooks/useLayoutEffect.ts rename to packages/effect-fc/src/hooks/Hooks/useLayoutEffect.ts diff --git a/packages/effect-fc/src/hooks/useMemo.ts b/packages/effect-fc/src/hooks/Hooks/useMemo.ts similarity index 100% rename from packages/effect-fc/src/hooks/useMemo.ts rename to packages/effect-fc/src/hooks/Hooks/useMemo.ts diff --git a/packages/effect-fc/src/hooks/useOnce.ts b/packages/effect-fc/src/hooks/Hooks/useOnce.ts similarity index 100% rename from packages/effect-fc/src/hooks/useOnce.ts rename to packages/effect-fc/src/hooks/Hooks/useOnce.ts diff --git a/packages/effect-fc/src/hooks/useRefFromReactiveValue.ts b/packages/effect-fc/src/hooks/Hooks/useRefFromReactiveValue.ts similarity index 100% rename from packages/effect-fc/src/hooks/useRefFromReactiveValue.ts rename to packages/effect-fc/src/hooks/Hooks/useRefFromReactiveValue.ts diff --git a/packages/effect-fc/src/hooks/useRefState.ts b/packages/effect-fc/src/hooks/Hooks/useRefState.ts similarity index 94% rename from packages/effect-fc/src/hooks/useRefState.ts rename to packages/effect-fc/src/hooks/Hooks/useRefState.ts index d1fa07f..9698a73 100644 --- a/packages/effect-fc/src/hooks/useRefState.ts +++ b/packages/effect-fc/src/hooks/Hooks/useRefState.ts @@ -1,6 +1,6 @@ import { Effect, Equivalence, Ref, Stream, SubscriptionRef } from "effect" import * as React from "react" -import { SetStateAction } from "../types/index.js" +import { SetStateAction } from "../../types/index.js" import { useCallbackSync } from "./useCallbackSync.js" import { useFork } from "./useFork.js" import { useOnce } from "./useOnce.js" diff --git a/packages/effect-fc/src/hooks/useScope.ts b/packages/effect-fc/src/hooks/Hooks/useScope.ts similarity index 100% rename from packages/effect-fc/src/hooks/useScope.ts rename to packages/effect-fc/src/hooks/Hooks/useScope.ts diff --git a/packages/effect-fc/src/hooks/useStreamFromReactiveValues.ts b/packages/effect-fc/src/hooks/Hooks/useStreamFromReactiveValues.ts similarity index 100% rename from packages/effect-fc/src/hooks/useStreamFromReactiveValues.ts rename to packages/effect-fc/src/hooks/Hooks/useStreamFromReactiveValues.ts diff --git a/packages/effect-fc/src/hooks/useSubscribeRefs.ts b/packages/effect-fc/src/hooks/Hooks/useSubscribeRefs.ts similarity index 100% rename from packages/effect-fc/src/hooks/useSubscribeRefs.ts rename to packages/effect-fc/src/hooks/Hooks/useSubscribeRefs.ts diff --git a/packages/effect-fc/src/hooks/useSubscribeStream.ts b/packages/effect-fc/src/hooks/Hooks/useSubscribeStream.ts similarity index 100% rename from packages/effect-fc/src/hooks/useSubscribeStream.ts rename to packages/effect-fc/src/hooks/Hooks/useSubscribeStream.ts diff --git a/packages/effect-fc/src/hooks/index.ts b/packages/effect-fc/src/hooks/index.ts index e1811a5..6192316 100644 --- a/packages/effect-fc/src/hooks/index.ts +++ b/packages/effect-fc/src/hooks/index.ts @@ -1,2 +1,2 @@ -export * from "./Hooks.js" -export * as Hooks from "./Hooks.js" +export * from "./Hooks/index.js" +export * as Hooks from "./Hooks/index.js" -- 2.49.1 From ec8f9f2ddb327e7be566430f154e2f537d1a3c59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Mon, 28 Jul 2025 04:32:08 +0200 Subject: [PATCH 09/90] Fix --- packages/effect-fc/package.json | 4 ++-- packages/example/src/routes/dev/async-rendering.tsx | 5 +++-- packages/example/src/routes/index.tsx | 5 +++-- packages/example/src/todo/Todo.tsx | 7 ++++--- packages/example/src/todo/Todos.tsx | 7 ++++--- 5 files changed, 16 insertions(+), 12 deletions(-) diff --git a/packages/effect-fc/package.json b/packages/effect-fc/package.json index 27e9ca9..874edc5 100644 --- a/packages/effect-fc/package.json +++ b/packages/effect-fc/package.json @@ -18,8 +18,8 @@ "default": "./dist/index.js" }, "./hooks": { - "types": "./dist/types/hooks.d.ts", - "default": "./dist/types/hooks.js" + "types": "./dist/hooks/index.d.ts", + "default": "./dist/hooks/index.js" }, "./types": { "types": "./dist/types/index.d.ts", diff --git a/packages/example/src/routes/dev/async-rendering.tsx b/packages/example/src/routes/dev/async-rendering.tsx index 52b9fb1..4bc8eaa 100644 --- a/packages/example/src/routes/dev/async-rendering.tsx +++ b/packages/example/src/routes/dev/async-rendering.tsx @@ -3,7 +3,8 @@ import { Flex, Text, TextField } from "@radix-ui/themes" import { createFileRoute } from "@tanstack/react-router" import { GetRandomValues, makeUuid4 } from "@typed/id" import { Effect } from "effect" -import { Component, Hook, Memoized, Suspense } from "effect-fc" +import { Component, Memoized, Suspense } from "effect-fc" +import { Hooks } from "effect-fc/hooks" import * as React from "react" @@ -69,7 +70,7 @@ class AsyncComponent extends Component.make(function* AsyncComponent() { class MemoizedAsyncComponent extends Memoized.memo(AsyncComponent) {} class SubComponent extends Component.make(function* SubComponent() { - const [state] = React.useState(yield* Hook.useOnce(() => Effect.provide(makeUuid4, GetRandomValues.CryptoRandom))) + const [state] = React.useState(yield* Hooks.useOnce(() => Effect.provide(makeUuid4, GetRandomValues.CryptoRandom))) return {state} }) {} diff --git a/packages/example/src/routes/index.tsx b/packages/example/src/routes/index.tsx index 8c82d1b..143b18c 100644 --- a/packages/example/src/routes/index.tsx +++ b/packages/example/src/routes/index.tsx @@ -3,7 +3,8 @@ import { Todos } from "@/todo/Todos" import { TodosState } from "@/todo/TodosState.service" import { createFileRoute } from "@tanstack/react-router" import { Effect } from "effect" -import { Component, Hook } from "effect-fc" +import { Component } from "effect-fc" +import { Hooks } from "effect-fc/hooks" const TodosStateLive = TodosState.Default("todos") @@ -12,7 +13,7 @@ export const Route = createFileRoute("/")({ component: Component.make(function* Index() { return yield* Todos.pipe( Effect.map(FC => ), - Effect.provide(yield* Hook.useContext(TodosStateLive, { finalizerExecutionMode: "fork" })), + Effect.provide(yield* Hooks.useContext(TodosStateLive, { finalizerExecutionMode: "fork" })), ) }).pipe( Component.withRuntime(runtime.context) diff --git a/packages/example/src/todo/Todo.tsx b/packages/example/src/todo/Todo.tsx index 69b82c8..44c0b48 100644 --- a/packages/example/src/todo/Todo.tsx +++ b/packages/example/src/todo/Todo.tsx @@ -2,7 +2,8 @@ import * as Domain from "@/domain" import { Box, Button, Flex, IconButton, TextArea } from "@radix-ui/themes" import { GetRandomValues, makeUuid4 } from "@typed/id" import { Chunk, Effect, Match, Option, Ref, Runtime, SubscriptionRef } from "effect" -import { Component, Hook, Memoized } from "effect-fc" +import { Component, Memoized } from "effect-fc" +import { Hooks } from "effect-fc/hooks" import { SubscriptionSubRef } from "effect-fc/types" import { FaArrowDown, FaArrowUp } from "react-icons/fa" import { FaDeleteLeft } from "react-icons/fa6" @@ -28,7 +29,7 @@ export class Todo extends Component.make(function* Todo(props: TodoProps) { const runtime = yield* Effect.runtime() const state = yield* TodosState - const [ref, contentRef] = yield* Hook.useMemo(() => Match.value(props).pipe( + const [ref, contentRef] = yield* Hooks.useMemo(() => Match.value(props).pipe( Match.tag("new", () => Effect.andThen(makeTodo, SubscriptionRef.make)), Match.tag("edit", ({ index }) => Effect.succeed(SubscriptionSubRef.makeFromChunkRef(state.ref, index))), Match.exhaustive, @@ -39,7 +40,7 @@ export class Todo extends Component.make(function* Todo(props: TodoProps) { ] as const), ), [props._tag, props.index]) - const [content, size] = yield* Hook.useSubscribeRefs(contentRef, state.sizeRef) + const [content, size] = yield* Hooks.useSubscribeRefs(contentRef, state.sizeRef) return ( diff --git a/packages/example/src/todo/Todos.tsx b/packages/example/src/todo/Todos.tsx index 857f303..b6af608 100644 --- a/packages/example/src/todo/Todos.tsx +++ b/packages/example/src/todo/Todos.tsx @@ -1,15 +1,16 @@ import { Container, Flex, Heading } from "@radix-ui/themes" import { Chunk, Console, Effect } from "effect" -import { Component, Hook } from "effect-fc" +import { Component } from "effect-fc" +import { Hooks } from "effect-fc/hooks" import { Todo } from "./Todo" import { TodosState } from "./TodosState.service" export class Todos extends Component.make(function* Todos() { const state = yield* TodosState - const [todos] = yield* Hook.useSubscribeRefs(state.ref) + const [todos] = yield* Hooks.useSubscribeRefs(state.ref) - yield* Hook.useOnce(() => Effect.andThen( + yield* Hooks.useOnce(() => Effect.andThen( Console.log("Todos mounted"), Effect.addFinalizer(() => Console.log("Todos unmounted")), )) -- 2.49.1 From b2b002852cb2a29db3779fd3259ba8d63bbfcd25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Tue, 29 Jul 2025 02:57:18 +0200 Subject: [PATCH 10/90] useInput --- packages/effect-fc/src/hooks/Hooks/index.ts | 1 + .../effect-fc/src/hooks/Hooks/useInput.ts | 59 +++++++++++++++++++ packages/example/src/todo/Todo.tsx | 21 +++++-- 3 files changed, 76 insertions(+), 5 deletions(-) create mode 100644 packages/effect-fc/src/hooks/Hooks/useInput.ts diff --git a/packages/effect-fc/src/hooks/Hooks/index.ts b/packages/effect-fc/src/hooks/Hooks/index.ts index 5d0e295..6b3c65d 100644 --- a/packages/effect-fc/src/hooks/Hooks/index.ts +++ b/packages/effect-fc/src/hooks/Hooks/index.ts @@ -3,6 +3,7 @@ export * from "./useCallbackPromise.js" export * from "./useCallbackSync.js" export * from "./useContext.js" export * from "./useEffect.js" +export * from "./useInput.js" export * from "./useLayoutEffect.js" export * from "./useMemo.js" export * from "./useOnce.js" diff --git a/packages/effect-fc/src/hooks/Hooks/useInput.ts b/packages/effect-fc/src/hooks/Hooks/useInput.ts new file mode 100644 index 0000000..4d32d7b --- /dev/null +++ b/packages/effect-fc/src/hooks/Hooks/useInput.ts @@ -0,0 +1,59 @@ +import { type Duration, Effect, flow, Option, ParseResult, Ref, Schema, Stream, SubscriptionRef, Types } from "effect" +import * as React from "react" +import { useCallbackSync } from "./useCallbackSync.js" +import { useFork } from "./useFork.js" +import { useOnce } from "./useOnce.js" +import { useSubscribeRefs } from "./useSubscribeRefs.js" + + +export namespace useInput { + export interface Options { + readonly ref: SubscriptionRef.SubscriptionRef + readonly schema: Schema.Schema, string, R> + readonly debounce?: Duration.Duration + } + + export interface Result { + readonly value: string + readonly onChange: React.ChangeEventHandler + readonly error: Option.Option + } +} + +export const useInput: { + (options: useInput.Options): Effect.Effect +} = Effect.fnUntraced(function* (options: useInput.Options) { + const internalRef = yield* useOnce(() => options.ref.pipe( + Effect.andThen(Schema.encode(options.schema)), + Effect.andThen(SubscriptionRef.make), + )) + const [error, setError] = React.useState(Option.none()) + + yield* useFork(() => Effect.all([ + Stream.runForEach(options.ref, upstreamValue => + Effect.andThen(internalRef, internalValue => + upstreamValue !== internalValue + ? Effect.andThen(Schema.encode(options.schema)(upstreamValue), v => Ref.set(internalRef, v)) + : Effect.void + ) + ), + + Stream.runForEach( + options.debounce ? Stream.debounce(internalRef, options.debounce) : internalRef, + flow( + Schema.decode(options.schema), + Effect.andThen(v => Ref.set(options.ref, v)), + Effect.andThen(() => setError(Option.none())), + Effect.catchTag("ParseError", e => Effect.sync(() => setError(Option.some(e)))), + ), + ), + ], { concurrency: "unbounded" }), [options.ref, options.schema, options.debounce, internalRef]) + + const [value] = yield* useSubscribeRefs(internalRef) + const onChange = yield* useCallbackSync((e: React.ChangeEvent) => Ref.set( + internalRef, + e.target.value, + ), [internalRef]) + + return { value, onChange, error } +}) diff --git a/packages/example/src/todo/Todo.tsx b/packages/example/src/todo/Todo.tsx index 44c0b48..1cf0aed 100644 --- a/packages/example/src/todo/Todo.tsx +++ b/packages/example/src/todo/Todo.tsx @@ -1,7 +1,7 @@ import * as Domain from "@/domain" -import { Box, Button, Flex, IconButton, TextArea } from "@radix-ui/themes" +import { Box, Button, Callout, Flex, IconButton, Text, TextArea } from "@radix-ui/themes" import { GetRandomValues, makeUuid4 } from "@typed/id" -import { Chunk, Effect, Match, Option, Ref, Runtime, SubscriptionRef } from "effect" +import { Chunk, Effect, Match, Option, ParseResult, Ref, Runtime, Schema, SubscriptionRef } from "effect" import { Component, Memoized } from "effect-fc" import { Hooks } from "effect-fc/hooks" import { SubscriptionSubRef } from "effect-fc/types" @@ -40,15 +40,26 @@ export class Todo extends Component.make(function* Todo(props: TodoProps) { ] as const), ), [props._tag, props.index]) - const [content, size] = yield* Hooks.useSubscribeRefs(contentRef, state.sizeRef) + const [size] = yield* Hooks.useSubscribeRefs(state.sizeRef) + const contentInput = yield* Hooks.useInput({ ref: contentRef, schema: Schema.Any }) return ( + {Option.isSome(contentInput.error) && + + + {ParseResult.ArrayFormatter.formatErrorSync(contentInput.error.value).map(e => <> + • {e.message}
+ )} +
+
+ } +