diff --git a/packages/effect-fc/src/hooks/Hooks/useInput.ts b/packages/effect-fc/src/hooks/Hooks/useInput.ts index e5444ea..d975edc 100644 --- a/packages/effect-fc/src/hooks/Hooks/useInput.ts +++ b/packages/effect-fc/src/hooks/Hooks/useInput.ts @@ -1,4 +1,4 @@ -import { type Duration, Effect, Equal, flow, identity, Option, type ParseResult, Ref, Schema, Stream, SubscriptionRef } from "effect" +import { type Duration, Effect, Equal, type 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" @@ -10,6 +10,7 @@ export namespace useInput { readonly schema: Schema.Schema readonly ref: SubscriptionRef.SubscriptionRef readonly debounce?: Duration.DurationInput + readonly equivalence?: Equivalence.Equivalence } export interface Result { @@ -30,7 +31,7 @@ export const useInput: { yield* useFork(() => Effect.all([ // Sync the upstream state with the internal state - // Only mutate the internal state if the upstream encoded value is actually different. This avoids infinite re-render loops. + // Only mutate the internal state if the upstream value is actually different. This avoids infinite re-render loops. Stream.runForEach(options.ref.changes, upstreamValue => Effect.whenEffect( Effect.andThen( @@ -39,7 +40,7 @@ export const useInput: { ), internalRef.pipe( Effect.andThen(Schema.decode(options.schema)), - Effect.andThen(decodedInternalValue => !Equal.equals(upstreamValue, decodedInternalValue)), + Effect.andThen(decodedInternalValue => !(options.equivalence ?? Equal.equals)(upstreamValue, decodedInternalValue)), ), ) ), @@ -54,7 +55,7 @@ export const useInput: { Effect.catchTag("ParseError", e => Effect.sync(() => setError(Option.some(e)))), ), ), - ], { concurrency: "unbounded" }), [options.ref, options.schema, options.debounce, internalRef]) + ], { concurrency: "unbounded" }), [options.ref, options.schema, options.debounce, options.equivalence, internalRef]) const [value, setValue] = yield* useRefState(internalRef) const onChange = React.useCallback((e: React.ChangeEvent) => setValue(e.target.value), [setValue]) diff --git a/packages/effect-fc/src/hooks/Hooks/useOptionalInput.ts b/packages/effect-fc/src/hooks/Hooks/useOptionalInput.ts index 320a58e..ea04ee1 100644 --- a/packages/effect-fc/src/hooks/Hooks/useOptionalInput.ts +++ b/packages/effect-fc/src/hooks/Hooks/useOptionalInput.ts @@ -1,9 +1,10 @@ -import { type Duration, Effect, flow, identity, Option, type ParseResult, Ref, Schema, Stream, SubscriptionRef } from "effect" +import { type Duration, Effect, Equal, type Equivalence, flow, identity, Option, type ParseResult, Ref, Schema, 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" +import { useRefState } from "./useRefState.js" import { useSubscribeRefs } from "./useSubscribeRefs.js" @@ -13,6 +14,7 @@ export namespace useOptionalInput { readonly defaultValue?: A readonly ref: SubscriptionRef.SubscriptionRef> readonly debounce?: Duration.DurationInput + readonly equivalence?: Equivalence.Equivalence } export interface Result { @@ -49,14 +51,14 @@ export const useOptionalInput: { // Sync the upstream state with the internal state // Only mutate the internal state if the upstream encoded value is actually different. This avoids infinite re-render loops. Stream.runForEach(options.ref.changes, Option.match({ - onSome: upstreamValue => Effect.andThen( - Effect.all([Schema.encode(options.schema)(upstreamValue), internalRef]), - ([encodedUpstreamValue, internalValue]) => Effect.when( - Effect.andThen( - Ref.set(internalRef, encodedUpstreamValue), - Ref.set(disabledRef, false), - ), - () => encodedUpstreamValue !== internalValue, + onSome: 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)), ), ), @@ -73,13 +75,7 @@ export const useOptionalInput: { Effect.catchTag("ParseError", e => Effect.sync(() => setError(Option.some(e)))), ), ), - ], { concurrency: "unbounded" }), [options.ref, options.schema, options.debounce, internalRef]) - - const [value, disabled] = yield* useSubscribeRefs(internalRef, disabledRef) - const onChange = yield* useCallbackSync((e: React.ChangeEvent) => Ref.set( - internalRef, - e.target.value, - ), [internalRef]) + ], { concurrency: "unbounded" }), [options.ref, options.schema, options.debounce, options.equivalence, internalRef]) const setDisabled = yield* useCallbackSync((setStateAction: React.SetStateAction) => Effect.andThen( @@ -98,5 +94,9 @@ export const useOptionalInput: { ), [disabledRef, options.ref, internalRef, options.schema]) + const [disabled] = yield* useSubscribeRefs(disabledRef) + const [value, setValue] = yield* useRefState(internalRef) + const onChange = React.useCallback((e: React.ChangeEvent) => setValue(e.target.value), [setValue]) + return { value, onChange, disabled, setDisabled, error } }) diff --git a/packages/example/src/routes/dev/input.tsx b/packages/example/src/routes/dev/input.tsx index 7946fe0..a2b7dc9 100644 --- a/packages/example/src/routes/dev/input.tsx +++ b/packages/example/src/routes/dev/input.tsx @@ -29,7 +29,7 @@ const Input = Component.makeUntraced(function* Input() { - setStr(e.target.value)} /> + ) }).pipe(