From a432993ac313785185a42ebee4f170f2c205956d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Fri, 24 Oct 2025 01:36:27 +0200 Subject: [PATCH] 0.2.0 (#18) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Renovate Bot Co-authored-by: Julien Valverdé Reviewed-on: https://git.valverde.cloud/Thilawyn/effect-fc/pulls/18 --- bun.lock | 6 +- package.json | 2 +- packages/effect-fc/package.json | 2 +- packages/effect-fc/src/Async.ts | 10 +- packages/effect-fc/src/Component.ts | 286 ++++++++++++++++-- packages/effect-fc/src/Form.ts | 185 ++++++----- packages/effect-fc/src/Hooks/ScopeOptions.ts | 7 - packages/effect-fc/src/Hooks/index.ts | 16 - packages/effect-fc/src/Hooks/input/index.ts | 2 - .../effect-fc/src/Hooks/input/useInput.ts | 67 ---- .../src/Hooks/input/useOptionalInput.ts | 107 ------- packages/effect-fc/src/Hooks/internal.ts | 18 -- .../effect-fc/src/Hooks/useCallbackPromise.ts | 20 -- .../effect-fc/src/Hooks/useCallbackSync.ts | 20 -- packages/effect-fc/src/Hooks/useContext.ts | 25 -- packages/effect-fc/src/Hooks/useEffect.ts | 29 -- packages/effect-fc/src/Hooks/useFork.ts | 32 -- .../effect-fc/src/Hooks/useLayoutEffect.ts | 29 -- packages/effect-fc/src/Hooks/useMemo.ts | 17 -- packages/effect-fc/src/Hooks/useOnce.ts | 11 - .../effect-fc/src/Hooks/useRefFromState.ts | 20 -- packages/effect-fc/src/Hooks/useRefState.ts | 29 -- packages/effect-fc/src/Hooks/useScope.ts | 38 --- .../src/Hooks/useStreamFromReactiveValues.ts | 30 -- .../effect-fc/src/Hooks/useSubscribables.ts | 31 -- .../effect-fc/src/Hooks/useSubscribeStream.ts | 32 -- packages/effect-fc/src/Memoized.ts | 2 +- packages/effect-fc/src/ReactRuntime.ts | 12 +- packages/effect-fc/src/Stream.ts | 58 ++++ packages/effect-fc/src/Subscribable.ts | 44 ++- packages/effect-fc/src/SubscriptionRef.ts | 48 +++ packages/effect-fc/src/SubscriptionSubRef.ts | 2 +- packages/effect-fc/src/index.ts | 3 +- packages/example/src/domain/Todo.ts | 2 +- .../src/lib/form/TextFieldFormInput.tsx | 102 +++---- .../example/src/lib/input/TextAreaInput.tsx | 41 --- .../example/src/lib/input/TextFieldInput.tsx | 69 ----- packages/example/src/routeTree.gen.ts | 34 +-- .../src/routes/dev/async-rendering.tsx | 4 +- packages/example/src/routes/dev/context.tsx | 42 +++ packages/example/src/routes/dev/input.tsx | 41 --- packages/example/src/routes/form.tsx | 11 +- packages/example/src/routes/index.tsx | 4 +- packages/example/src/todo/Todo.tsx | 99 +++--- packages/example/src/todo/Todos.tsx | 6 +- 45 files changed, 701 insertions(+), 994 deletions(-) delete mode 100644 packages/effect-fc/src/Hooks/ScopeOptions.ts delete mode 100644 packages/effect-fc/src/Hooks/index.ts delete mode 100644 packages/effect-fc/src/Hooks/input/index.ts delete mode 100644 packages/effect-fc/src/Hooks/input/useInput.ts delete mode 100644 packages/effect-fc/src/Hooks/input/useOptionalInput.ts delete mode 100644 packages/effect-fc/src/Hooks/internal.ts delete mode 100644 packages/effect-fc/src/Hooks/useCallbackPromise.ts delete mode 100644 packages/effect-fc/src/Hooks/useCallbackSync.ts delete mode 100644 packages/effect-fc/src/Hooks/useContext.ts delete mode 100644 packages/effect-fc/src/Hooks/useEffect.ts delete mode 100644 packages/effect-fc/src/Hooks/useFork.ts delete mode 100644 packages/effect-fc/src/Hooks/useLayoutEffect.ts delete mode 100644 packages/effect-fc/src/Hooks/useMemo.ts delete mode 100644 packages/effect-fc/src/Hooks/useOnce.ts delete mode 100644 packages/effect-fc/src/Hooks/useRefFromState.ts delete mode 100644 packages/effect-fc/src/Hooks/useRefState.ts delete mode 100644 packages/effect-fc/src/Hooks/useScope.ts delete mode 100644 packages/effect-fc/src/Hooks/useStreamFromReactiveValues.ts delete mode 100644 packages/effect-fc/src/Hooks/useSubscribables.ts delete mode 100644 packages/effect-fc/src/Hooks/useSubscribeStream.ts create mode 100644 packages/effect-fc/src/Stream.ts create mode 100644 packages/effect-fc/src/SubscriptionRef.ts delete mode 100644 packages/example/src/lib/input/TextAreaInput.tsx delete mode 100644 packages/example/src/lib/input/TextFieldInput.tsx create mode 100644 packages/example/src/routes/dev/context.tsx delete mode 100644 packages/example/src/routes/dev/input.tsx diff --git a/bun.lock b/bun.lock index 6079b3a..4caaad6 100644 --- a/bun.lock +++ b/bun.lock @@ -5,7 +5,7 @@ "name": "@effect-fc/monorepo", "devDependencies": { "@biomejs/biome": "^2.2.5", - "@effect/language-service": "^0.45.0", + "@effect/language-service": "^0.48.0", "@types/bun": "^1.2.23", "npm-check-updates": "^19.0.0", "npm-sort": "^0.0.4", @@ -15,7 +15,7 @@ }, "packages/effect-fc": { "name": "effect-fc", - "version": "0.1.4", + "version": "0.1.5", "dependencies": { "@typed/async-data": "^0.13.1", }, @@ -135,7 +135,7 @@ "@effect-fc/example": ["@effect-fc/example@workspace:packages/example"], - "@effect/language-service": ["@effect/language-service@0.45.1", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-SEZ9TaVCpRKYumTQJPApg3os9O94bN2lCYQLgZbyK/xD+NSfYPPJZQ+6T5LkpcNgW8BRk1ACI7S1W2/noxm7Qg=="], + "@effect/language-service": ["@effect/language-service@0.48.0", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-u7DTPoGFFeDGSdomjY5C2nCGNWSisxpYSqHp3dlSG8kCZh5cay+166bveHRYvuJSJS5yomdkPTJwjwrqMmT7Og=="], "@effect/platform": ["@effect/platform@0.92.1", "", { "dependencies": { "find-my-way-ts": "^0.1.6", "msgpackr": "^1.11.4", "multipasta": "^0.2.7" }, "peerDependencies": { "effect": "^3.18.1" } }, "sha512-XXWCBVwyhaKZISN7aM1fv/3fWDGyxr84ObywnUrL8aHvJLoIeskWFAP/fqw3c5MFCrJ3ZV97RWLbv6JiBQugdg=="], diff --git a/package.json b/package.json index 76911e6..0496090 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ }, "devDependencies": { "@biomejs/biome": "^2.2.5", - "@effect/language-service": "^0.45.0", + "@effect/language-service": "^0.48.0", "@types/bun": "^1.2.23", "npm-check-updates": "^19.0.0", "npm-sort": "^0.0.4", diff --git a/packages/effect-fc/package.json b/packages/effect-fc/package.json index d87a362..bc322de 100644 --- a/packages/effect-fc/package.json +++ b/packages/effect-fc/package.json @@ -1,7 +1,7 @@ { "name": "effect-fc", "description": "Write React function components with Effect", - "version": "0.1.5", + "version": "0.2.0", "type": "module", "files": [ "./README.md", diff --git a/packages/effect-fc/src/Async.ts b/packages/effect-fc/src/Async.ts index 19c1912..a0dba11 100644 --- a/packages/effect-fc/src/Async.ts +++ b/packages/effect-fc/src/Async.ts @@ -1,10 +1,10 @@ /** biome-ignore-all lint/complexity/useArrowFunction: necessary for class prototypes */ import { Effect, Function, Predicate, Runtime, Scope } from "effect" import * as React from "react" -import type * as Component from "./Component.js" +import * as Component from "./Component.js" -export const TypeId: unique symbol = Symbol.for("effect-fc/Async") +export const TypeId: unique symbol = Symbol.for("@effect-fc/Async/Async") export type TypeId = typeof TypeId export interface Async extends Async.Options { @@ -26,13 +26,15 @@ const SuspenseProto = Object.freeze({ makeFunctionComponent

( this: Component.Component & Async, runtimeRef: React.RefObject>>, - scope: Scope.Scope, ) { const SuspenseInner = (props: { readonly promise: Promise }) => React.use(props.promise) return ({ fallback, name, ...props }: Async.Props) => { const promise = Runtime.runPromise(runtimeRef.current)( - Effect.provideService(this.body(props as P), Scope.Scope, scope) + Effect.andThen( + Component.useScope([], this), + scope => Effect.provideService(this.body(props as P), Scope.Scope, scope), + ) ) return React.createElement( diff --git a/packages/effect-fc/src/Component.ts b/packages/effect-fc/src/Component.ts index dd1524f..69a24f4 100644 --- a/packages/effect-fc/src/Component.ts +++ b/packages/effect-fc/src/Component.ts @@ -1,12 +1,11 @@ /** biome-ignore-all lint/complexity/noBannedTypes: {} is the default type for React props */ /** biome-ignore-all lint/complexity/useArrowFunction: necessary for class prototypes */ -import { Context, Effect, Effectable, ExecutionStrategy, Function, Predicate, Runtime, Scope, Tracer, type Types, type Utils } from "effect" +import { Context, type Duration, Effect, Effectable, Equivalence, ExecutionStrategy, Exit, Fiber, Function, HashMap, Layer, ManagedRuntime, Option, Predicate, Ref, Runtime, Scope, Tracer, type Types, type Utils } from "effect" import * as React from "react" -import * as Hooks from "./Hooks/index.js" import { Memoized } from "./index.js" -export const TypeId: unique symbol = Symbol.for("effect-fc/Component") +export const TypeId: unique symbol = Symbol.for("@effect-fc/Component/Component") export type TypeId = typeof TypeId export interface Component

@@ -26,8 +25,7 @@ extends /** @internal */ makeFunctionComponent( - runtimeRef: React.Ref>>, - scope: Scope.Scope, + runtimeRef: React.Ref>> ): (props: P) => A } @@ -41,8 +39,8 @@ export namespace Component { export interface Options { readonly displayName?: string - readonly finalizerExecutionMode: "sync" | "fork" readonly finalizerExecutionStrategy: ExecutionStrategy.ExecutionStrategy + readonly finalizerExecutionDebounce: Duration.DurationInput } } @@ -54,46 +52,41 @@ const ComponentProto = Object.freeze({ commit: Effect.fnUntraced(function*

( this: Component ) { - const self = this // biome-ignore lint/style/noNonNullAssertion: React ref initialization const runtimeRef = React.useRef>>(null!) runtimeRef.current = yield* Effect.runtime>() - return React.useRef(function ScopeProvider(props: P) { - const scope = Runtime.runSync(runtimeRef.current)(Hooks.useScope( - Array.from( - Context.omit(...nonReactiveTags)(runtimeRef.current.context).unsafeMap.values() - ), - self, - )) - - const FC = React.useMemo(() => { - const f: React.FC

= self.makeFunctionComponent(runtimeRef, scope) - f.displayName = self.displayName ?? "Anonymous" - return Memoized.isMemoized(self) - ? React.memo(f, self.propsAreEqual) + return yield* React.useState(() => Runtime.runSync(runtimeRef.current)(Effect.cachedFunction( + (_services: readonly any[]) => Effect.sync(() => { + const f: React.FC

= this.makeFunctionComponent(runtimeRef) + f.displayName = this.displayName ?? "Anonymous" + return Memoized.isMemoized(this) + ? React.memo(f, this.propsAreEqual) : f - }, [scope]) - - return React.createElement(FC, props) - }).current + }), + Equivalence.array(Equivalence.strict()), + )))[0](Array.from( + Context.omit(...nonReactiveTags)(runtimeRef.current.context).unsafeMap.values() + )) }), makeFunctionComponent

( this: Component, runtimeRef: React.RefObject>>, - scope: Scope.Scope, ) { return (props: P) => Runtime.runSync(runtimeRef.current)( - Effect.provideService(this.body(props), Scope.Scope, scope) + Effect.andThen( + useScope([], this), + scope => Effect.provideService(this.body(props), Scope.Scope, scope), + ) ) }, } as const) -const defaultOptions = { - finalizerExecutionMode: "sync", +const defaultOptions: Component.Options = { finalizerExecutionStrategy: ExecutionStrategy.sequential, -} as const + finalizerExecutionDebounce: "100 millis", +} const nonReactiveTags = [Tracer.ParentSpan] as const @@ -407,3 +400,238 @@ export const withRuntime: { props, ) }) + + +export class ScopeMap extends Effect.Service()("effect-fc/Component/ScopeMap", { + effect: Effect.bind(Effect.Do, "ref", () => Ref.make(HashMap.empty())) +}) {} + +export namespace ScopeMap { + export interface Entry { + readonly scope: Scope.CloseableScope + readonly closeFiber: Option.Option> + } +} + + +export namespace useScope { + export interface Options { + readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy + readonly finalizerExecutionDebounce?: Duration.DurationInput + } +} + +export const useScope: { + ( + deps: React.DependencyList, + options?: useScope.Options, + ): Effect.Effect +} = Effect.fnUntraced(function*(deps, options) { + // biome-ignore lint/style/noNonNullAssertion: context initialization + const runtimeRef = React.useRef>(null!) + runtimeRef.current = yield* Effect.runtime() + + const scopeMap = yield* ScopeMap as unknown as Effect.Effect + + const [key, scope] = React.useMemo(() => Runtime.runSync(runtimeRef.current)(Effect.andThen( + Effect.all([Effect.succeed({}), scopeMap.ref]), + ([key, map]) => Effect.andThen( + Option.match(HashMap.get(map, key), { + onSome: entry => Effect.succeed(entry.scope), + onNone: () => Effect.tap( + Scope.make(options?.finalizerExecutionStrategy ?? defaultOptions.finalizerExecutionStrategy), + scope => Ref.update(scopeMap.ref, HashMap.set(key, { + scope, + closeFiber: Option.none(), + })), + ), + }), + scope => [key, scope] as const, + ), + // biome-ignore lint/correctness/useExhaustiveDependencies: use of React.DependencyList + )), deps) + + // biome-ignore lint/correctness/useExhaustiveDependencies: only reactive on "key" + React.useEffect(() => Runtime.runSync(runtimeRef.current)(scopeMap.ref.pipe( + Effect.andThen(HashMap.get(key)), + Effect.tap(entry => Option.match(entry.closeFiber, { + onSome: fiber => Effect.andThen( + Ref.update(scopeMap.ref, HashMap.set(key, { ...entry, closeFiber: Option.none() })), + Fiber.interruptFork(fiber), + ), + onNone: () => Effect.void, + })), + Effect.map(({ scope }) => + () => Runtime.runSync(runtimeRef.current)(Effect.andThen( + Effect.forkDaemon(Effect.sleep(options?.finalizerExecutionDebounce ?? defaultOptions.finalizerExecutionDebounce).pipe( + Effect.andThen(Scope.close(scope, Exit.void)), + Effect.andThen(Ref.update(scopeMap.ref, HashMap.remove(key))), + )), + fiber => Ref.update(scopeMap.ref, HashMap.set(key, { + scope, + closeFiber: Option.some(fiber), + })), + )) + ), + )), [key]) + + return scope +}) + +export const useOnMount: { + ( + f: () => Effect.Effect + ): Effect.Effect +} = Effect.fnUntraced(function* ( + f: () => Effect.Effect +) { + const runtime = yield* Effect.runtime() + return yield* React.useState(() => Runtime.runSync(runtime)(Effect.cached(f())))[0] +}) + +export namespace useOnChange { + export type Options = useScope.Options +} + +export const useOnChange: { + ( + f: () => Effect.Effect, + deps: React.DependencyList, + options?: useOnChange.Options, + ): Effect.Effect> +} = Effect.fnUntraced(function* ( + f: () => Effect.Effect, + deps: React.DependencyList, + options?: useOnChange.Options, +) { + const runtime = yield* Effect.runtime>() + const scope = yield* useScope(deps, options) + + // biome-ignore lint/correctness/useExhaustiveDependencies: only reactive on "scope" + return yield* React.useMemo(() => Runtime.runSync(runtime)( + Effect.cached(Effect.provideService(f(), Scope.Scope, scope)) + ), [scope]) +}) + +export namespace useReactEffect { + export interface Options { + readonly finalizerExecutionMode?: "sync" | "fork" + readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy + } +} + +export const useReactEffect: { + ( + f: () => Effect.Effect, + deps?: React.DependencyList, + options?: useReactEffect.Options, + ): Effect.Effect> +} = Effect.fnUntraced(function* ( + f: () => Effect.Effect, + deps?: React.DependencyList, + options?: useReactEffect.Options, +) { + const runtime = yield* Effect.runtime>() + // biome-ignore lint/correctness/useExhaustiveDependencies: use of React.DependencyList + React.useEffect(() => runReactEffect(runtime, f, options), deps) +}) + +const runReactEffect = ( + runtime: Runtime.Runtime>, + f: () => Effect.Effect, + options?: useReactEffect.Options, +) => Effect.Do.pipe( + Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy ?? defaultOptions.finalizerExecutionStrategy)), + Effect.bind("exit", ({ scope }) => Effect.exit(Effect.provideService(f(), Scope.Scope, scope))), + Effect.map(({ scope }) => + () => { + switch (options?.finalizerExecutionMode ?? "fork") { + case "sync": + Runtime.runSync(runtime)(Scope.close(scope, Exit.void)) + break + case "fork": + Runtime.runFork(runtime)(Scope.close(scope, Exit.void)) + break + } + } + ), + Runtime.runSync(runtime), +) + +export namespace useReactLayoutEffect { + export type Options = useReactEffect.Options +} + +export const useReactLayoutEffect: { + ( + f: () => Effect.Effect, + deps?: React.DependencyList, + options?: useReactLayoutEffect.Options, + ): Effect.Effect> +} = Effect.fnUntraced(function* ( + f: () => Effect.Effect, + deps?: React.DependencyList, + options?: useReactLayoutEffect.Options, +) { + const runtime = yield* Effect.runtime>() + // biome-ignore lint/correctness/useExhaustiveDependencies: use of React.DependencyList + React.useLayoutEffect(() => runReactEffect(runtime, f, options), deps) +}) + +export const useCallbackSync: { + ( + f: (...args: Args) => Effect.Effect, + deps: React.DependencyList, + ): Effect.Effect<(...args: Args) => A, never, R> +} = Effect.fnUntraced(function* ( + f: (...args: Args) => Effect.Effect, + deps: React.DependencyList, +) { + // biome-ignore lint/style/noNonNullAssertion: context initialization + const runtimeRef = React.useRef>(null!) + runtimeRef.current = yield* Effect.runtime() + + // biome-ignore lint/correctness/useExhaustiveDependencies: use of React.DependencyList + return React.useCallback((...args: Args) => Runtime.runSync(runtimeRef.current)(f(...args)), deps) +}) + +export const useCallbackPromise: { + ( + f: (...args: Args) => Effect.Effect, + deps: React.DependencyList, + ): Effect.Effect<(...args: Args) => Promise, never, R> +} = Effect.fnUntraced(function* ( + f: (...args: Args) => Effect.Effect, + deps: React.DependencyList, +) { + // biome-ignore lint/style/noNonNullAssertion: context initialization + const runtimeRef = React.useRef>(null!) + runtimeRef.current = yield* Effect.runtime() + + // biome-ignore lint/correctness/useExhaustiveDependencies: use of React.DependencyList + return React.useCallback((...args: Args) => Runtime.runPromise(runtimeRef.current)(f(...args)), deps) +}) + +export namespace useContext { + export type Options = useScope.Options +} + +export const useContext: { + ( + layer: Layer.Layer, + options?: useContext.Options, + ): Effect.Effect, E, RIn> +} = Effect.fnUntraced(function* ( + layer: Layer.Layer, + options?: useContext.Options, +) { + const scope = yield* useScope([layer], options) + + return yield* useOnChange(() => Effect.context().pipe( + Effect.map(context => ManagedRuntime.make(Layer.provide(layer, Layer.succeedContext(context)))), + Effect.tap(runtime => Effect.addFinalizer(() => runtime.disposeEffect)), + Effect.andThen(runtime => runtime.runtimeEffect), + Effect.andThen(runtime => runtime.context), + Effect.provideService(Scope.Scope, scope), + ), [scope]) +}) diff --git a/packages/effect-fc/src/Form.ts b/packages/effect-fc/src/Form.ts index e829a11..b78d296 100644 --- a/packages/effect-fc/src/Form.ts +++ b/packages/effect-fc/src/Form.ts @@ -1,14 +1,15 @@ import * as AsyncData from "@typed/async-data" -import { Array, Cause, Chunk, type Duration, Effect, Equal, Exit, Fiber, flow, identity, Option, ParseResult, Pipeable, Predicate, Ref, Schema, type Scope, Stream, SubscriptionRef } from "effect" +import { Array, Cause, Chunk, type Duration, Effect, Equal, Exit, Fiber, flow, identity, Option, ParseResult, Pipeable, Predicate, Ref, Schema, type Scope, Stream } from "effect" import type { NoSuchElementException } from "effect/Cause" import * as React from "react" -import * as Hooks from "./Hooks/index.js" +import * as Component from "./Component.js" import * as PropertyPath from "./PropertyPath.js" import * as Subscribable from "./Subscribable.js" +import * as SubscriptionRef from "./SubscriptionRef.js" import * as SubscriptionSubRef from "./SubscriptionSubRef.js" -export const FormTypeId: unique symbol = Symbol.for("effect-fc/Form") +export const FormTypeId: unique symbol = Symbol.for("@effect-fc/Form/Form") export type FormTypeId = typeof FormTypeId export interface Form @@ -17,6 +18,7 @@ extends Pipeable.Pipeable { readonly schema: Schema.Schema readonly onSubmit: (value: NoInfer) => Effect.Effect + readonly autosubmit: boolean readonly debounce: Option.Option readonly valueRef: SubscriptionRef.SubscriptionRef> @@ -35,6 +37,7 @@ extends Pipeable.Class() implements Form { constructor( readonly schema: Schema.Schema, readonly onSubmit: (value: NoInfer) => Effect.Effect, + readonly autosubmit: boolean, readonly debounce: Option.Option, readonly valueRef: SubscriptionRef.SubscriptionRef>, @@ -52,11 +55,15 @@ extends Pipeable.Class() implements Form { export const isForm = (u: unknown): u is Form => Predicate.hasProperty(u, FormTypeId) export namespace make { - export interface Options { + export interface Options { readonly schema: Schema.Schema readonly initialEncodedValue: NoInfer - readonly onSubmit: (value: NoInfer) => Effect.Effect, - readonly debounce?: Duration.DurationInput, + readonly onSubmit: ( + this: Form, NoInfer, NoInfer, unknown, unknown, unknown>, + value: NoInfer, + ) => Effect.Effect + readonly autosubmit?: boolean + readonly debounce?: Duration.DurationInput } } @@ -75,6 +82,7 @@ export const make: { return new FormImpl( options.schema, options.onSubmit, + options.autosubmit ?? false, Option.fromNullable(options.debounce), valueRef, @@ -97,7 +105,7 @@ export const make: { export const run = ( self: Form -): Effect.Effect => Stream.runForEach( +): Effect.Effect => Stream.runForEach( self.encodedValueRef.changes.pipe( Option.isSome(self.debounce) ? Stream.debounce(self.debounce.value) : identity ), @@ -108,30 +116,35 @@ export const run = ( onNone: () => Effect.void, })), Effect.andThen( - Effect.addFinalizer(() => SubscriptionRef.set(self.validationFiberRef, Option.none())).pipe( + Effect.addFinalizer(() => Ref.set(self.validationFiberRef, Option.none())).pipe( Effect.andThen(Schema.decode(self.schema, { errors: "all" })(encodedValue)), Effect.exit, Effect.andThen(flow( Exit.matchEffect({ - onSuccess: v => Effect.andThen( - SubscriptionRef.set(self.valueRef, Option.some(v)), - SubscriptionRef.set(self.errorRef, Option.none()), + onSuccess: v => Ref.set(self.valueRef, Option.some(v)).pipe( + Effect.andThen(Ref.set(self.errorRef, Option.none())), + Effect.as(Option.some(v)), ), - onFailure: c => Option.match( - Chunk.findFirst(Cause.failures(c), e => e._tag === "ParseError"), - { - onSome: e => SubscriptionRef.set(self.errorRef, Option.some(e)), + onFailure: c => Chunk.findFirst(Cause.failures(c), e => e._tag === "ParseError").pipe( + Option.match({ + onSome: e => Ref.set(self.errorRef, Option.some(e)), onNone: () => Effect.void, - }, + }), + Effect.as(Option.none()), ), }), Effect.uninterruptible, )), Effect.scoped, + + Effect.andThen(value => Option.isSome(value) && self.autosubmit + ? Effect.asVoid(Effect.forkScoped(submit(self))) + : Effect.void + ), Effect.forkScoped, ) ), - Effect.andThen(fiber => SubscriptionRef.set(self.validationFiberRef, Option.some(fiber))) + Effect.andThen(fiber => Ref.set(self.validationFiberRef, Option.some(fiber))) ), ) @@ -157,13 +170,13 @@ export const submit = ( ) export namespace service { - export interface Options + export interface Options extends make.Options {} } export const service = ( options: service.Options -): Effect.Effect, never, R | Scope.Scope> => Effect.tap( +): Effect.Effect, never, Scope.Scope | R | SR> => Effect.tap( make(options), form => Effect.forkScoped(run(form)), ) @@ -220,24 +233,6 @@ extends Pipeable.Class() implements FormField { export const isFormField = (u: unknown): u is FormField => Predicate.hasProperty(u, FormFieldTypeId) -export namespace useForm { - export interface Options - extends make.Options {} -} - -export const useForm: { - ( - options: make.Options, - deps: React.DependencyList, - ): Effect.Effect, never, R> -} = Effect.fnUntraced(function* ( - options: make.Options, - deps: React.DependencyList, -) { - const form = yield* Hooks.useMemo(() => make(options), [options.debounce, ...deps]) - yield* Hooks.useFork(() => run(form), [form]) - return form -}) export const useSubmit = ( self: Form @@ -245,7 +240,7 @@ export const useSubmit = ( () => Promise>>, never, SR -> => Hooks.useCallbackPromise(() => submit(self), [self]) +> => Component.useCallbackPromise(() => submit(self), [self]) export const useField = >>( self: Form, @@ -271,33 +266,34 @@ export const useInput: { ( field: FormField, options?: useInput.Options, - ): Effect.Effect, NoSuchElementException> + ): Effect.Effect, NoSuchElementException, Scope.Scope> } = Effect.fnUntraced(function* ( field: FormField, options?: useInput.Options, ) { - const internalValueRef = yield* Hooks.useMemo(() => Effect.andThen(field.encodedValueRef, SubscriptionRef.make), [field]) - const [value, setValue] = yield* Hooks.useRefState(internalValueRef) - - yield* Hooks.useFork(() => Effect.all([ - Stream.runForEach( - Stream.drop(field.encodedValueRef, 1), - upstreamEncodedValue => Effect.whenEffect( - Ref.set(internalValueRef, upstreamEncodedValue), - Effect.andThen(internalValueRef, internalValue => !Equal.equals(upstreamEncodedValue, internalValue)), + const internalValueRef = yield* Component.useOnChange(() => Effect.tap( + Effect.andThen(field.encodedValueRef, SubscriptionRef.make), + internalValueRef => Effect.forkScoped(Effect.all([ + Stream.runForEach( + Stream.drop(field.encodedValueRef, 1), + upstreamEncodedValue => Effect.whenEffect( + Ref.set(internalValueRef, upstreamEncodedValue), + Effect.andThen(internalValueRef, internalValue => !Equal.equals(upstreamEncodedValue, internalValue)), + ), ), - ), - Stream.runForEach( - internalValueRef.changes.pipe( - Stream.drop(1), - Stream.changesWith(Equal.equivalence()), - options?.debounce ? Stream.debounce(options.debounce) : identity, + Stream.runForEach( + internalValueRef.changes.pipe( + Stream.drop(1), + Stream.changesWith(Equal.equivalence()), + options?.debounce ? Stream.debounce(options.debounce) : identity, + ), + internalValue => Ref.set(field.encodedValueRef, internalValue), ), - internalValue => Ref.set(field.encodedValueRef, internalValue), - ), - ], { concurrency: "unbounded" }), [field, internalValueRef, options?.debounce]) + ], { concurrency: "unbounded" })), + ), [field, options?.debounce]) + const [value, setValue] = yield* SubscriptionRef.useSubscriptionRefState(internalValueRef) return { value, setValue } }) @@ -316,55 +312,56 @@ export const useOptionalInput: { ( field: FormField>, options: useOptionalInput.Options, - ): Effect.Effect, NoSuchElementException> + ): Effect.Effect, NoSuchElementException, Scope.Scope> } = Effect.fnUntraced(function* ( field: FormField>, options: useOptionalInput.Options, ) { - const [enabledRef, internalValueRef] = yield* Hooks.useMemo(() => Effect.andThen( - field.encodedValueRef, - Option.match({ - onSome: v => Effect.all([SubscriptionRef.make(true), SubscriptionRef.make(v)]), - onNone: () => Effect.all([SubscriptionRef.make(false), SubscriptionRef.make(options.defaultValue)]), - }), - ), [field]) + const [enabledRef, internalValueRef] = yield* Component.useOnChange(() => Effect.tap( + Effect.andThen( + field.encodedValueRef, + Option.match({ + onSome: v => Effect.all([SubscriptionRef.make(true), SubscriptionRef.make(v)]), + onNone: () => Effect.all([SubscriptionRef.make(false), SubscriptionRef.make(options.defaultValue)]), + }), + ), - const [enabled, setEnabled] = yield* Hooks.useRefState(enabledRef) - const [value, setValue] = yield* Hooks.useRefState(internalValueRef) + ([enabledRef, internalValueRef]) => Effect.forkScoped(Effect.all([ + Stream.runForEach( + Stream.drop(field.encodedValueRef, 1), - yield* Hooks.useFork(() => Effect.all([ - Stream.runForEach( - Stream.drop(field.encodedValueRef, 1), + upstreamEncodedValue => Effect.whenEffect( + Option.match(upstreamEncodedValue, { + onSome: v => Effect.andThen( + Ref.set(enabledRef, true), + Ref.set(internalValueRef, v), + ), + onNone: () => Effect.andThen( + Ref.set(enabledRef, false), + Ref.set(internalValueRef, options.defaultValue), + ), + }), - upstreamEncodedValue => Effect.whenEffect( - Option.match(upstreamEncodedValue, { - onSome: v => Effect.andThen( - Ref.set(enabledRef, true), - Ref.set(internalValueRef, v), + Effect.andThen( + Effect.all([enabledRef, internalValueRef]), + ([enabled, internalValue]) => !Equal.equals(upstreamEncodedValue, enabled ? Option.some(internalValue) : Option.none()), ), - onNone: () => Effect.andThen( - Ref.set(enabledRef, false), - Ref.set(internalValueRef, options.defaultValue), - ), - }), - - Effect.andThen( - Effect.all([enabledRef, internalValueRef]), - ([enabled, internalValue]) => !Equal.equals(upstreamEncodedValue, enabled ? Option.some(internalValue) : Option.none()), ), ), - ), - Stream.runForEach( - enabledRef.changes.pipe( - Stream.zipLatest(internalValueRef.changes), - Stream.drop(1), - Stream.changesWith(Equal.equivalence()), - options?.debounce ? Stream.debounce(options.debounce) : identity, + Stream.runForEach( + enabledRef.changes.pipe( + Stream.zipLatest(internalValueRef.changes), + Stream.drop(1), + Stream.changesWith(Equal.equivalence()), + options?.debounce ? Stream.debounce(options.debounce) : identity, + ), + ([enabled, internalValue]) => Ref.set(field.encodedValueRef, enabled ? Option.some(internalValue) : Option.none()), ), - ([enabled, internalValue]) => Ref.set(field.encodedValueRef, enabled ? Option.some(internalValue) : Option.none()), - ), - ], { concurrency: "unbounded" }), [field, enabledRef, internalValueRef, options.debounce]) + ], { concurrency: "unbounded" })), + ), [field, options.debounce]) + const [enabled, setEnabled] = yield* SubscriptionRef.useSubscriptionRefState(enabledRef) + const [value, setValue] = yield* SubscriptionRef.useSubscriptionRefState(internalValueRef) return { enabled, setEnabled, value, setValue } }) diff --git a/packages/effect-fc/src/Hooks/ScopeOptions.ts b/packages/effect-fc/src/Hooks/ScopeOptions.ts deleted file mode 100644 index c2654e8..0000000 --- a/packages/effect-fc/src/Hooks/ScopeOptions.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { ExecutionStrategy } from "effect" - - -export interface ScopeOptions { - readonly finalizerExecutionMode?: "sync" | "fork" - readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy -} diff --git a/packages/effect-fc/src/Hooks/index.ts b/packages/effect-fc/src/Hooks/index.ts deleted file mode 100644 index 0f99a97..0000000 --- a/packages/effect-fc/src/Hooks/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -export * from "./input/index.js" -export * from "./ScopeOptions.js" -export * from "./useCallbackPromise.js" -export * from "./useCallbackSync.js" -export * from "./useContext.js" -export * from "./useEffect.js" -export * from "./useFork.js" -export * from "./useLayoutEffect.js" -export * from "./useMemo.js" -export * from "./useOnce.js" -export * from "./useRefFromState.js" -export * from "./useRefState.js" -export * from "./useScope.js" -export * from "./useStreamFromReactiveValues.js" -export * from "./useSubscribables.js" -export * from "./useSubscribeStream.js" diff --git a/packages/effect-fc/src/Hooks/input/index.ts b/packages/effect-fc/src/Hooks/input/index.ts deleted file mode 100644 index bc1c4dc..0000000 --- a/packages/effect-fc/src/Hooks/input/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./useInput.js" -export * from "./useOptionalInput.js" diff --git a/packages/effect-fc/src/Hooks/input/useInput.ts b/packages/effect-fc/src/Hooks/input/useInput.ts deleted file mode 100644 index 215eac6..0000000 --- a/packages/effect-fc/src/Hooks/input/useInput.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { type Duration, Effect, Equal, Equivalence, flow, identity, Option, type ParseResult, Ref, Schema, Stream, SubscriptionRef } from "effect" -import * as React from "react" -import { useFork } from "../useFork.js" -import { useOnce } from "../useOnce.js" -import { useRefState } from "../useRefState.js" - - -export namespace useInput { - export interface Options { - readonly schema: Schema.Schema - readonly equivalence?: Equivalence.Equivalence - readonly ref: SubscriptionRef.SubscriptionRef - readonly debounce?: Duration.DurationInput - } - - export interface Result { - readonly value: string - readonly setValue: React.Dispatch> - 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([ - // Sync the upstream state with the internal state - // Only mutate the internal state if the upstream value is actually different. This avoids infinite re-render loops. - Stream.runForEach(Stream.changesWith(options.ref.changes, Equivalence.strict()), upstreamValue => - Effect.whenEffect( - Effect.andThen( - Schema.encode(options.schema)(upstreamValue), - encodedUpstreamValue => Ref.set(internalRef, encodedUpstreamValue), - ), - internalRef.pipe( - Effect.andThen(Schema.decode(options.schema)), - Effect.andThen(decodedInternalValue => !(options.equivalence ?? Equal.equals)(upstreamValue, decodedInternalValue)), - Effect.catchTag("ParseError", () => Effect.succeed(false)), - ), - ) - ), - - // Sync all changes to the internal state with upstream - Stream.runForEach( - internalRef.changes.pipe( - Stream.changesWith(Equivalence.strict()), - options.debounce ? Stream.debounce(options.debounce) : identity, - Stream.drop(1), - ), - 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.schema, options.equivalence, options.ref, options.debounce, internalRef]) - - const [value, setValue] = yield* useRefState(internalRef) - return { value, setValue, error } -}) diff --git a/packages/effect-fc/src/Hooks/input/useOptionalInput.ts b/packages/effect-fc/src/Hooks/input/useOptionalInput.ts deleted file mode 100644 index 0f519aa..0000000 --- a/packages/effect-fc/src/Hooks/input/useOptionalInput.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { type Duration, Effect, Equal, Equivalence, flow, identity, Option, type ParseResult, Ref, Schema, Stream, SubscriptionRef } from "effect" -import * as React from "react" -import * as SetStateAction from "../../SetStateAction.js" -import { useCallbackSync } from "../useCallbackSync.js" -import { useFork } from "../useFork.js" -import { useOnce } from "../useOnce.js" -import { useRefState } from "../useRefState.js" -import { useSubscribables } from "../useSubscribables.js" - - -export namespace useOptionalInput { - export interface Options { - readonly schema: Schema.Schema - readonly defaultValue?: A - readonly equivalence?: Equivalence.Equivalence - readonly ref: SubscriptionRef.SubscriptionRef> - readonly debounce?: Duration.DurationInput - } - - export interface Result { - readonly value: string - readonly setValue: React.Dispatch> - readonly enabled: boolean - readonly setEnabled: React.Dispatch> - readonly error: Option.Option - } -} - -export const useOptionalInput: { - (options: useOptionalInput.Options): Effect.Effect -} = Effect.fnUntraced(function* (options: useOptionalInput.Options) { - const [internalRef, enabledRef] = yield* useOnce(() => Effect.andThen(options.ref, upstreamValue => - Effect.all([ - Effect.andThen( - Option.match(upstreamValue, { - onSome: Schema.encode(options.schema), - onNone: () => options.defaultValue - ? Schema.encode(options.schema)(options.defaultValue) - : Effect.succeed(""), - }), - SubscriptionRef.make, - ), - - SubscriptionRef.make(Option.isSome(upstreamValue)), - ]) - )) - - const [error, setError] = React.useState(Option.none()) - - yield* useFork(() => Effect.all([ - // Sync the upstream state with the internal state - // Only mutate the internal state if the upstream value is actually different. This avoids infinite re-render loops. - Stream.runForEach(Stream.changesWith(options.ref.changes, Equivalence.strict()), Option.match({ - onSome: upstreamValue => Effect.andThen( - Ref.set(enabledRef, true), - - Effect.whenEffect( - Effect.andThen( - Schema.encode(options.schema)(upstreamValue), - encodedUpstreamValue => Ref.set(internalRef, encodedUpstreamValue), - ), - internalRef.pipe( - Effect.andThen(Schema.decode(options.schema)), - Effect.andThen(decodedInternalValue => !(options.equivalence ?? Equal.equals)(upstreamValue, decodedInternalValue)), - Effect.catchTag("ParseError", () => Effect.succeed(false)), - ), - ), - ), - - onNone: () => Ref.set(enabledRef, false), - })), - - // Sync all changes to the internal state with upstream - Stream.runForEach( - internalRef.changes.pipe( - Stream.changesWith(Equivalence.strict()), - options.debounce ? Stream.debounce(options.debounce) : identity, - Stream.drop(1), - ), - flow( - Schema.decode(options.schema), - Effect.andThen(v => Ref.set(options.ref, Option.some(v))), - Effect.andThen(() => setError(Option.none())), - Effect.catchTag("ParseError", e => Effect.sync(() => setError(Option.some(e)))), - ), - ), - ], { concurrency: "unbounded" }), [options.schema, options.equivalence, options.ref, options.debounce, internalRef]) - - const setEnabled = yield* useCallbackSync( - (setStateAction: React.SetStateAction) => Effect.andThen( - Ref.updateAndGet(enabledRef, prevState => SetStateAction.value(setStateAction, prevState)), - enabled => enabled - ? internalRef.pipe( - Effect.andThen(Schema.decode(options.schema)), - Effect.andThen(v => Ref.set(options.ref, Option.some(v))), - Effect.andThen(() => setError(Option.none())), - Effect.catchTag("ParseError", e => Effect.sync(() => setError(Option.some(e)))), - ) - : Ref.set(options.ref, Option.none()), - ), - [options.schema, options.ref, internalRef, enabledRef], - ) - - const [enabled] = yield* useSubscribables(enabledRef) - const [value, setValue] = yield* useRefState(internalRef) - return { value, setValue, enabled, setEnabled, error } -}) diff --git a/packages/effect-fc/src/Hooks/internal.ts b/packages/effect-fc/src/Hooks/internal.ts deleted file mode 100644 index 4c9a35e..0000000 --- a/packages/effect-fc/src/Hooks/internal.ts +++ /dev/null @@ -1,18 +0,0 @@ -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/Hooks/useCallbackPromise.ts b/packages/effect-fc/src/Hooks/useCallbackPromise.ts deleted file mode 100644 index 0e2ce10..0000000 --- a/packages/effect-fc/src/Hooks/useCallbackPromise.ts +++ /dev/null @@ -1,20 +0,0 @@ -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.fnUntraced(function* ( - callback: (...args: Args) => Effect.Effect, - deps: React.DependencyList, -) { - // biome-ignore lint/style/noNonNullAssertion: context initialization - const runtimeRef = React.useRef>(null!) - runtimeRef.current = yield* Effect.runtime() - - // biome-ignore lint/correctness/useExhaustiveDependencies: use of React.DependencyList - 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 deleted file mode 100644 index 3f96641..0000000 --- a/packages/effect-fc/src/Hooks/useCallbackSync.ts +++ /dev/null @@ -1,20 +0,0 @@ -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.fnUntraced(function* ( - callback: (...args: Args) => Effect.Effect, - deps: React.DependencyList, -) { - // biome-ignore lint/style/noNonNullAssertion: context initialization - const runtimeRef = React.useRef>(null!) - runtimeRef.current = yield* Effect.runtime() - - // biome-ignore lint/correctness/useExhaustiveDependencies: use of React.DependencyList - return React.useCallback((...args: Args) => Runtime.runSync(runtimeRef.current)(callback(...args)), deps) -}) diff --git a/packages/effect-fc/src/Hooks/useContext.ts b/packages/effect-fc/src/Hooks/useContext.ts deleted file mode 100644 index 16516de..0000000 --- a/packages/effect-fc/src/Hooks/useContext.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { type Context, Effect, Layer, ManagedRuntime, 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, RIn> -} = Effect.fnUntraced(function* ( - layer: Layer.Layer, - options?: ScopeOptions, -) { - const scope = yield* useScope([layer], options) - - return yield* useMemo(() => Effect.context().pipe( - Effect.map(context => ManagedRuntime.make(Layer.provide(layer, Layer.succeedContext(context)))), - Effect.tap(runtime => Effect.addFinalizer(() => runtime.disposeEffect)), - Effect.andThen(runtime => runtime.runtimeEffect), - Effect.andThen(runtime => runtime.context), - Effect.provideService(Scope.Scope, scope), - ), [scope]) -}) diff --git a/packages/effect-fc/src/Hooks/useEffect.ts b/packages/effect-fc/src/Hooks/useEffect.ts deleted file mode 100644 index b325479..0000000 --- a/packages/effect-fc/src/Hooks/useEffect.ts +++ /dev/null @@ -1,29 +0,0 @@ -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 useEffect: { - ( - effect: () => Effect.Effect, - deps?: React.DependencyList, - options?: ScopeOptions, - ): Effect.Effect> -} = Effect.fnUntraced(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), - // biome-ignore lint/correctness/useExhaustiveDependencies: use of React.DependencyList - ), deps) -}) diff --git a/packages/effect-fc/src/Hooks/useFork.ts b/packages/effect-fc/src/Hooks/useFork.ts deleted file mode 100644 index 0ce8a5c..0000000 --- a/packages/effect-fc/src/Hooks/useFork.ts +++ /dev/null @@ -1,32 +0,0 @@ -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.fnUntraced(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", - }) - // biome-ignore lint/correctness/useExhaustiveDependencies: use of React.DependencyList - }, deps) -}) diff --git a/packages/effect-fc/src/Hooks/useLayoutEffect.ts b/packages/effect-fc/src/Hooks/useLayoutEffect.ts deleted file mode 100644 index 6f8c438..0000000 --- a/packages/effect-fc/src/Hooks/useLayoutEffect.ts +++ /dev/null @@ -1,29 +0,0 @@ -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 useLayoutEffect: { - ( - effect: () => Effect.Effect, - deps?: React.DependencyList, - options?: ScopeOptions, - ): Effect.Effect> -} = Effect.fnUntraced(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), - // biome-ignore lint/correctness/useExhaustiveDependencies: use of React.DependencyList - ), deps) -}) diff --git a/packages/effect-fc/src/Hooks/useMemo.ts b/packages/effect-fc/src/Hooks/useMemo.ts deleted file mode 100644 index 43d4449..0000000 --- a/packages/effect-fc/src/Hooks/useMemo.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Effect, Runtime } from "effect" -import * as React from "react" - - -export const useMemo: { - ( - factory: () => Effect.Effect, - deps: React.DependencyList, - ): Effect.Effect -} = Effect.fnUntraced(function* ( - factory: () => Effect.Effect, - deps: React.DependencyList, -) { - const runtime = yield* Effect.runtime() - // biome-ignore lint/correctness/useExhaustiveDependencies: use of React.DependencyList - return yield* React.useMemo(() => Runtime.runSync(runtime)(Effect.cached(factory())), deps) -}) diff --git a/packages/effect-fc/src/Hooks/useOnce.ts b/packages/effect-fc/src/Hooks/useOnce.ts deleted file mode 100644 index f617611..0000000 --- a/packages/effect-fc/src/Hooks/useOnce.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Effect } from "effect" -import { useMemo } from "./useMemo.js" - - -export const useOnce: { - (factory: () => Effect.Effect): Effect.Effect -} = Effect.fnUntraced(function* ( - factory: () => Effect.Effect -) { - return yield* useMemo(factory, []) -}) diff --git a/packages/effect-fc/src/Hooks/useRefFromState.ts b/packages/effect-fc/src/Hooks/useRefFromState.ts deleted file mode 100644 index a6138f8..0000000 --- a/packages/effect-fc/src/Hooks/useRefFromState.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Effect, Equivalence, Ref, Stream, SubscriptionRef } from "effect" -import type * as React from "react" -import { useEffect } from "./useEffect.js" -import { useFork } from "./useFork.js" -import { useOnce } from "./useOnce.js" - - -export const useRefFromState: { - (state: readonly [A, React.Dispatch>]): Effect.Effect> -} = Effect.fnUntraced(function*([value, setValue]) { - const ref = yield* useOnce(() => SubscriptionRef.make(value)) - - yield* useEffect(() => Ref.set(ref, value), [value]) - yield* useFork(() => Stream.runForEach( - Stream.changesWith(ref.changes, Equivalence.strict()), - v => Effect.sync(() => setValue(v)), - ), [setValue]) - - return ref -}) diff --git a/packages/effect-fc/src/Hooks/useRefState.ts b/packages/effect-fc/src/Hooks/useRefState.ts deleted file mode 100644 index 9e55fbf..0000000 --- a/packages/effect-fc/src/Hooks/useRefState.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Effect, Equivalence, Ref, Stream, type SubscriptionRef } from "effect" -import * as React from "react" -import * as SetStateAction from "../SetStateAction.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.fnUntraced(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) => - Effect.andThen( - Ref.updateAndGet(ref, prevState => SetStateAction.value(setStateAction, prevState)), - v => setReactStateValue(v), - ), - [ref]) - - return [reactStateValue, setValue] -}) diff --git a/packages/effect-fc/src/Hooks/useScope.ts b/packages/effect-fc/src/Hooks/useScope.ts deleted file mode 100644 index 93212a0..0000000 --- a/packages/effect-fc/src/Hooks/useScope.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Effect, ExecutionStrategy, Ref, Runtime, Scope } from "effect" -import * as React from "react" -import { closeScope } from "./internal.js" -import type { ScopeOptions } from "./ScopeOptions.js" - - -export const useScope: { - ( - deps: React.DependencyList, - options?: ScopeOptions, - ): Effect.Effect -} = Effect.fnUntraced(function*(deps, options) { - const runtime = yield* Effect.runtime() - - // biome-ignore lint/correctness/useExhaustiveDependencies: no reactivity needed - 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)), - ), - }) - // biome-ignore lint/correctness/useExhaustiveDependencies: use of React.DependencyList - ), deps) - - return scope -}) diff --git a/packages/effect-fc/src/Hooks/useStreamFromReactiveValues.ts b/packages/effect-fc/src/Hooks/useStreamFromReactiveValues.ts deleted file mode 100644 index a05ea28..0000000 --- a/packages/effect-fc/src/Hooks/useStreamFromReactiveValues.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Effect, PubSub, Ref, type Scope, Stream } from "effect" -import type * 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.fnUntraced(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/Hooks/useSubscribables.ts b/packages/effect-fc/src/Hooks/useSubscribables.ts deleted file mode 100644 index 9d96fee..0000000 --- a/packages/effect-fc/src/Hooks/useSubscribables.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Effect, Equivalence, pipe, Stream, type Subscribable } from "effect" -import * as React from "react" -import { useFork } from "./useFork.js" -import { useOnce } from "./useOnce.js" - - -export const useSubscribables: { - []>( - ...elements: T - ): Effect.Effect< - { [K in keyof T]: Effect.Effect.Success | Stream.Stream.Success }, - Effect.Effect.Error | Stream.Stream.Error, - Effect.Effect.Context | Stream.Stream.Context - > -} = Effect.fnUntraced(function* []>( - ...elements: T -) { - const [reactStateValue, setReactStateValue] = React.useState(yield* useOnce(() => - Effect.all(elements.map(v => v.get)) - )) - - yield* useFork(() => pipe( - elements.map(ref => Stream.changesWith(ref.changes, Equivalence.strict())), - streams => Stream.zipLatestAll(...streams), - Stream.runForEach(v => - Effect.sync(() => setReactStateValue(v)) - ), - ), elements) - - return reactStateValue as any -}) diff --git a/packages/effect-fc/src/Hooks/useSubscribeStream.ts b/packages/effect-fc/src/Hooks/useSubscribeStream.ts deleted file mode 100644 index 252054f..0000000 --- a/packages/effect-fc/src/Hooks/useSubscribeStream.ts +++ /dev/null @@ -1,32 +0,0 @@ -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.fnUntraced(function* , E, R>( - stream: Stream.Stream, - initialValue?: A, -) { - const [reactStateValue, setReactStateValue] = React.useState( - // biome-ignore lint/correctness/useExhaustiveDependencies: no reactivity needed - 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/Memoized.ts b/packages/effect-fc/src/Memoized.ts index 7ed205f..61cea91 100644 --- a/packages/effect-fc/src/Memoized.ts +++ b/packages/effect-fc/src/Memoized.ts @@ -3,7 +3,7 @@ import { type Equivalence, Function, Predicate } from "effect" import type * as Component from "./Component.js" -export const TypeId: unique symbol = Symbol.for("effect-fc/Memoized") +export const TypeId: unique symbol = Symbol.for("@effect-fc/Memoized/Memoized") export type TypeId = typeof TypeId export interface Memoized

extends Memoized.Options

{ diff --git a/packages/effect-fc/src/ReactRuntime.ts b/packages/effect-fc/src/ReactRuntime.ts index 64f1af9..cdbe329 100644 --- a/packages/effect-fc/src/ReactRuntime.ts +++ b/packages/effect-fc/src/ReactRuntime.ts @@ -1,9 +1,10 @@ /** biome-ignore-all lint/complexity/useArrowFunction: necessary for class prototypes */ -import { Effect, type Layer, ManagedRuntime, Predicate, type Runtime } from "effect" +import { Effect, Layer, ManagedRuntime, Predicate, type Runtime } from "effect" import * as React from "react" +import * as Component from "./Component.js" -export const TypeId: unique symbol = Symbol.for("effect-fc/ReactRuntime") +export const TypeId: unique symbol = Symbol.for("@effect-fc/ReactRuntime/ReactRuntime") export type TypeId = typeof TypeId export interface ReactRuntime { @@ -21,9 +22,12 @@ export const isReactRuntime = (u: unknown): u is ReactRuntime export const make = ( layer: Layer.Layer, memoMap?: Layer.MemoMap, -): ReactRuntime => Object.setPrototypeOf( +): ReactRuntime => Object.setPrototypeOf( Object.assign(function() {}, { - runtime: ManagedRuntime.make(layer, memoMap), + runtime: ManagedRuntime.make( + Layer.merge(layer, Component.ScopeMap.Default), + memoMap, + ), // biome-ignore lint/style/noNonNullAssertion: context initialization context: React.createContext>(null!), }), diff --git a/packages/effect-fc/src/Stream.ts b/packages/effect-fc/src/Stream.ts new file mode 100644 index 0000000..726b7be --- /dev/null +++ b/packages/effect-fc/src/Stream.ts @@ -0,0 +1,58 @@ +import { Effect, Equivalence, Option, PubSub, Ref, type Scope, Stream } from "effect" +import * as React from "react" +import * as Component from "./Component.js" + + +export const useStream: { + ( + stream: Stream.Stream + ): Effect.Effect, never, R> + , E, R>( + stream: Stream.Stream, + initialValue: A, + ): Effect.Effect, never, R> +} = Effect.fnUntraced(function* , E, R>( + stream: Stream.Stream, + initialValue?: A, +) { + const [reactStateValue, setReactStateValue] = React.useState(() => initialValue + ? Option.some(initialValue) + : Option.none() + ) + + yield* Component.useReactEffect(() => Effect.forkScoped( + Stream.runForEach( + Stream.changesWith(stream, Equivalence.strict()), + v => Effect.sync(() => setReactStateValue(Option.some(v))), + ) + ), [stream]) + + return reactStateValue as Option.Some +}) + +export const useStreamFromReactiveValues: { + ( + values: A + ): Effect.Effect, never, Scope.Scope> +} = Effect.fnUntraced(function* (values: A) { + const { latest, pubsub, stream } = yield* Component.useOnMount(() => 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* Component.useReactEffect(() => Ref.set(latest, values).pipe( + Effect.andThen(PubSub.publish(pubsub, values)), + Effect.unlessEffect(PubSub.isShutdown(pubsub)), + ), values) + + return stream +}) + +export * from "effect/Stream" diff --git a/packages/effect-fc/src/Subscribable.ts b/packages/effect-fc/src/Subscribable.ts index 9ebba36..22f5dbe 100644 --- a/packages/effect-fc/src/Subscribable.ts +++ b/packages/effect-fc/src/Subscribable.ts @@ -1,17 +1,47 @@ -import { Effect, Stream, Subscribable } from "effect" +import { Effect, Equivalence, pipe, type Scope, Stream, Subscribable } from "effect" +import * as React from "react" +import * as Component from "./Component.js" -export const zipLatestAll = >>( - ...subscribables: T +export const zipLatestAll = []>( + ...elements: T ): Subscribable.Subscribable< [T[number]] extends [never] ? never : { [K in keyof T]: T[K] extends Subscribable.Subscribable ? A : never }, - [T[number]] extends [never] ? never : T[number] extends Subscribable.Subscribable ? _E : never, - [T[number]] extends [never] ? never : T[number] extends Subscribable.Subscribable ? _R : never + [T[number]] extends [never] ? never : T[number] extends Subscribable.Subscribable ? E : never, + [T[number]] extends [never] ? never : T[number] extends Subscribable.Subscribable ? R : never > => Subscribable.make({ - get: Effect.all(subscribables.map(v => v.get)), - changes: Stream.zipLatestAll(...subscribables.map(v => v.changes)), + get: Effect.all(elements.map(v => v.get)), + changes: Stream.zipLatestAll(...elements.map(v => v.changes)), }) as any +export const useSubscribables: { + []>( + ...elements: T + ): Effect.Effect< + [T[number]] extends [never] + ? never + : { [K in keyof T]: T[K] extends Subscribable.Subscribable ? A : never }, + [T[number]] extends [never] ? never : T[number] extends Subscribable.Subscribable ? E : never, + ([T[number]] extends [never] ? never : T[number] extends Subscribable.Subscribable ? R : never) | Scope.Scope + > +} = Effect.fnUntraced(function* []>( + ...elements: T +) { + const [reactStateValue, setReactStateValue] = React.useState( + yield* Component.useOnMount(() => Effect.all(elements.map(v => v.get))) + ) + + yield* Component.useReactEffect(() => Effect.forkScoped(pipe( + elements.map(ref => Stream.changesWith(ref.changes, Equivalence.strict())), + streams => Stream.zipLatestAll(...streams), + Stream.runForEach(v => + Effect.sync(() => setReactStateValue(v)) + ), + )), elements) + + return reactStateValue as any +}) + export * from "effect/Subscribable" diff --git a/packages/effect-fc/src/SubscriptionRef.ts b/packages/effect-fc/src/SubscriptionRef.ts new file mode 100644 index 0000000..eb7e9b8 --- /dev/null +++ b/packages/effect-fc/src/SubscriptionRef.ts @@ -0,0 +1,48 @@ +import { Effect, Equivalence, Ref, type Scope, Stream, SubscriptionRef } from "effect" +import * as React from "react" +import * as Component from "./Component.js" +import * as SetStateAction from "./SetStateAction.js" + + +export const useSubscriptionRefState: { + ( + ref: SubscriptionRef.SubscriptionRef + ): Effect.Effect>], never, Scope.Scope> +} = Effect.fnUntraced(function* (ref: SubscriptionRef.SubscriptionRef) { + const [reactStateValue, setReactStateValue] = React.useState(yield* Component.useOnMount(() => ref)) + + yield* Component.useReactEffect(() => Effect.forkScoped( + Stream.runForEach( + Stream.changesWith(ref.changes, Equivalence.strict()), + v => Effect.sync(() => setReactStateValue(v)), + ) + ), [ref]) + + const setValue = yield* Component.useCallbackSync((setStateAction: React.SetStateAction) => + Effect.andThen( + Ref.updateAndGet(ref, prevState => SetStateAction.value(setStateAction, prevState)), + v => setReactStateValue(v), + ), + [ref]) + + return [reactStateValue, setValue] +}) + +export const useSubscriptionRefFromState: { + (state: readonly [A, React.Dispatch>]): Effect.Effect, never, Scope.Scope> +} = Effect.fnUntraced(function*([value, setValue]) { + const ref = yield* Component.useOnChange(() => Effect.tap( + SubscriptionRef.make(value), + ref => Effect.forkScoped( + Stream.runForEach( + Stream.changesWith(ref.changes, Equivalence.strict()), + v => Effect.sync(() => setValue(v)), + ) + ), + ), [setValue]) + + yield* Component.useReactEffect(() => Ref.set(ref, value), [value]) + return ref +}) + +export * from "effect/SubscriptionRef" diff --git a/packages/effect-fc/src/SubscriptionSubRef.ts b/packages/effect-fc/src/SubscriptionSubRef.ts index 8533999..7980a29 100644 --- a/packages/effect-fc/src/SubscriptionSubRef.ts +++ b/packages/effect-fc/src/SubscriptionSubRef.ts @@ -2,7 +2,7 @@ import { Chunk, Effect, Effectable, Option, Predicate, Readable, Ref, Stream, Su import * as PropertyPath from "./PropertyPath.js" -export const SubscriptionSubRefTypeId: unique symbol = Symbol.for("effect-fc/SubscriptionSubRef") +export const SubscriptionSubRefTypeId: unique symbol = Symbol.for("effect-fc/SubscriptionSubRef/SubscriptionSubRef") export type SubscriptionSubRefTypeId = typeof SubscriptionSubRefTypeId export interface SubscriptionSubRef> diff --git a/packages/effect-fc/src/index.ts b/packages/effect-fc/src/index.ts index b2227e8..f63afb4 100644 --- a/packages/effect-fc/src/index.ts +++ b/packages/effect-fc/src/index.ts @@ -1,10 +1,11 @@ export * as Async from "./Async.js" export * as Component from "./Component.js" export * as Form from "./Form.js" -export * as Hooks from "./Hooks/index.js" export * as Memoized from "./Memoized.js" export * as PropertyPath from "./PropertyPath.js" export * as ReactRuntime from "./ReactRuntime.js" export * as SetStateAction from "./SetStateAction.js" +export * as Stream from "./Stream.js" export * as Subscribable from "./Subscribable.js" +export * as SubscriptionRef from "./SubscriptionRef.js" export * as SubscriptionSubRef from "./SubscriptionSubRef.js" diff --git a/packages/example/src/domain/Todo.ts b/packages/example/src/domain/Todo.ts index f4fe24f..500faab 100644 --- a/packages/example/src/domain/Todo.ts +++ b/packages/example/src/domain/Todo.ts @@ -1,5 +1,5 @@ -import { assertEncodedJsonifiable } from "@/lib/schema" import { Schema } from "effect" +import { assertEncodedJsonifiable } from "@/lib/schema" export class Todo extends Schema.Class("Todo")({ diff --git a/packages/example/src/lib/form/TextFieldFormInput.tsx b/packages/example/src/lib/form/TextFieldFormInput.tsx index 567afdb..5e69a4a 100644 --- a/packages/example/src/lib/form/TextFieldFormInput.tsx +++ b/packages/example/src/lib/form/TextFieldFormInput.tsx @@ -1,6 +1,6 @@ import { Callout, Flex, Spinner, Switch, TextField } from "@radix-ui/themes" import { Array, Option, Struct } from "effect" -import { Component, Form, Hooks } from "effect-fc" +import { Component, Form, Subscribable } from "effect-fc" interface Props @@ -18,60 +18,58 @@ extends Omit, Form.useOptional export type TextFieldFormInputProps = Props | OptionalProps -export class TextFieldFormInput extends Component.makeUntraced("TextFieldFormInput")( - function*(props: TextFieldFormInputProps) { - const input: ( - | { readonly optional: true } & Form.useOptionalInput.Result - | { readonly optional: false } & Form.useInput.Result - ) = props.optional - // biome-ignore lint/correctness/useHookAtTopLevel: "optional" reactivity not supported - ? { optional: true, ...yield* Form.useOptionalInput(props.field, props) } - // biome-ignore lint/correctness/useHookAtTopLevel: "optional" reactivity not supported - : { optional: false, ...yield* Form.useInput(props.field, props) } +export class TextFieldFormInput extends Component.makeUntraced("TextFieldFormInput")(function*(props: TextFieldFormInputProps) { + const input: ( + | { readonly optional: true } & Form.useOptionalInput.Result + | { readonly optional: false } & Form.useInput.Result + ) = props.optional + // biome-ignore lint/correctness/useHookAtTopLevel: "optional" reactivity not supported + ? { optional: true, ...yield* Form.useOptionalInput(props.field, props) } + // biome-ignore lint/correctness/useHookAtTopLevel: "optional" reactivity not supported + : { optional: false, ...yield* Form.useInput(props.field, props) } - const [issues, isValidating, isSubmitting] = yield* Hooks.useSubscribables( - props.field.issuesSubscribable, - props.field.isValidatingSubscribable, - props.field.isSubmittingSubscribable, - ) + const [issues, isValidating, isSubmitting] = yield* Subscribable.useSubscribables( + props.field.issuesSubscribable, + props.field.isValidatingSubscribable, + props.field.isSubmittingSubscribable, + ) - return ( - - input.setValue(e.target.value)} - disabled={(input.optional && !input.enabled) || isSubmitting} - {...Struct.omit(props, "optional", "defaultValue")} - > - {input.optional && - - - - } + return ( + + input.setValue(e.target.value)} + disabled={(input.optional && !input.enabled) || isSubmitting} + {...Struct.omit(props, "optional", "defaultValue")} + > + {input.optional && + + + + } - {isValidating && - - - - } + {isValidating && + + + + } - {props.children} - + {props.children} + - {Option.match(Array.head(issues), { - onSome: issue => ( - - {issue.message} - - ), + {Option.match(Array.head(issues), { + onSome: issue => ( + + {issue.message} + + ), - onNone: () => <>, - })} - - ) - } -) {} + onNone: () => <>, + })} + + ) +}) {} diff --git a/packages/example/src/lib/input/TextAreaInput.tsx b/packages/example/src/lib/input/TextAreaInput.tsx deleted file mode 100644 index 03d4359..0000000 --- a/packages/example/src/lib/input/TextAreaInput.tsx +++ /dev/null @@ -1,41 +0,0 @@ -/** biome-ignore-all lint/correctness/useHookAtTopLevel: effect-fc HOC */ -import { Callout, Flex, TextArea, type TextAreaProps } from "@radix-ui/themes" -import { Array, type Equivalence, Option, ParseResult, type Schema, Struct } from "effect" -import { Component } from "effect-fc" -import { useInput } from "effect-fc/Hooks" -import * as React from "react" - - -export type TextAreaInputProps = Omit, "schema" | "equivalence"> & Omit - -export const TextAreaInput = (options: { - readonly schema: Schema.Schema - readonly equivalence?: Equivalence.Equivalence -}): Component.Component< - TextAreaInputProps, - React.JSX.Element, - ParseResult.ParseError, - R -> => Component.makeUntraced("TextFieldInput")(function*(props) { - const input = yield* useInput({ ...options, ...props }) - const issue = React.useMemo(() => input.error.pipe( - Option.map(ParseResult.ArrayFormatter.formatErrorSync), - Option.flatMap(Array.head), - ), [input.error]) - - return ( - -