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" import { useSubscribeRefs } from "./useSubscribeRefs.js" export namespace useOptionalInput { export interface Options { readonly schema: Schema.Schema readonly defaultValue?: A readonly ref: SubscriptionRef.SubscriptionRef> readonly debounce?: Duration.DurationInput } export interface Result { readonly value: string readonly onChange: React.ChangeEventHandler readonly disabled: boolean readonly setDisabled: React.Dispatch> readonly error: Option.Option } } export const useOptionalInput: { (options: useOptionalInput.Options): Effect.Effect } = Effect.fnUntraced(function* (options: useOptionalInput.Options) { const [internalRef, disabledRef] = 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.isNone(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 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, ), ), onNone: () => Ref.set(disabledRef, true), })), // Sync all changes to the internal state with upstream Stream.runForEach( internalRef.changes.pipe(options.debounce ? Stream.debounce(options.debounce) : identity), 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.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]) 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 } })