diff --git a/packages/effect-fc/src/hooks/Hooks/index.ts b/packages/effect-fc/src/hooks/Hooks/index.ts index 6b3c65d..6b67a59 100644 --- a/packages/effect-fc/src/hooks/Hooks/index.ts +++ b/packages/effect-fc/src/hooks/Hooks/index.ts @@ -7,6 +7,7 @@ export * from "./useInput.js" export * from "./useLayoutEffect.js" export * from "./useMemo.js" export * from "./useOnce.js" +export * from "./useOptionalInput.js" export * from "./useRefFromReactiveValue.js" export * from "./useRefState.js" export * from "./useScope.js" diff --git a/packages/effect-fc/src/hooks/Hooks/useOptionalInput.ts b/packages/effect-fc/src/hooks/Hooks/useOptionalInput.ts index 3a775ba..d7c22a1 100644 --- a/packages/effect-fc/src/hooks/Hooks/useOptionalInput.ts +++ b/packages/effect-fc/src/hooks/Hooks/useOptionalInput.ts @@ -1,5 +1,6 @@ import { type Duration, Effect, 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" @@ -17,6 +18,8 @@ export namespace useOptionalInput { export interface Result { readonly value: string readonly onChange: React.ChangeEventHandler + readonly disabled: boolean + readonly setDisabled: React.Dispatch> readonly error: Option.Option } } @@ -24,7 +27,7 @@ export namespace useOptionalInput { export const useOptionalInput: { (options: useOptionalInput.Options): Effect.Effect } = Effect.fnUntraced(function* (options: useOptionalInput.Options) { - const [internalRef, initialDisabled] = yield* useOnce(() => Effect.andThen(options.ref, upstreamValue => + const [internalRef, disabledRef] = yield* useOnce(() => Effect.andThen(options.ref, upstreamValue => Effect.all([ Effect.andThen( Option.match(upstreamValue, { @@ -36,11 +39,10 @@ export const useOptionalInput: { SubscriptionRef.make, ), - Effect.succeed(Option.isNone(upstreamValue)), + SubscriptionRef.make(Option.isNone(upstreamValue)), ]) )) - const [disabled, setDisabled] = React.useState(initialDisabled) const [error, setError] = React.useState(Option.none()) yield* useFork(() => Effect.all([ @@ -50,12 +52,15 @@ export const useOptionalInput: { onSome: upstreamValue => Effect.andThen( Effect.all([Schema.encode(options.schema)(upstreamValue), internalRef]), ([encodedUpstreamValue, internalValue]) => Effect.when( - Ref.set(internalRef, encodedUpstreamValue), + Effect.andThen( + Ref.set(internalRef, encodedUpstreamValue), + Ref.set(disabledRef, false), + ), () => encodedUpstreamValue !== internalValue, ), ), - onNone: () => Effect.sync(() => setDisabled(true)), + onNone: () => Ref.set(disabledRef, true), })), // Sync all changes to the internal state with upstream @@ -63,18 +68,33 @@ export const useOptionalInput: { internalRef.changes.pipe(options.debounce ? Stream.debounce(options.debounce) : identity), flow( Schema.decode(options.schema), - Effect.andThen(v => Ref.set(options.ref, v)), + 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.ref, options.schema, options.debounce, internalRef]) - const [value] = yield* useSubscribeRefs(internalRef) + const [value, disabled] = yield* useSubscribeRefs(internalRef, disabledRef) const onChange = yield* useCallbackSync((e: React.ChangeEvent) => Ref.set( internalRef, e.target.value, ), [internalRef]) - return { value, onChange, error } + const setDisabled = yield* useCallbackSync((setStateAction: React.SetStateAction) => + Effect.andThen( + Ref.updateAndGet(disabledRef, prevState => + SetStateAction.value(setStateAction, prevState) + ), + + disabled => !disabled + ? Ref.set(options.ref, Option.none()) + : internalRef.pipe( + Effect.andThen(Schema.decode(options.schema)), + Effect.andThen(v => Ref.set(options.ref, Option.some(v))), + ), + ), + [disabledRef, options.ref, internalRef, options.schema]) + + return { value, onChange, disabled, setDisabled, error } })