useOptionalInput done
All checks were successful
Lint / lint (push) Successful in 11s

This commit is contained in:
Julien Valverdé
2025-08-16 01:28:59 +02:00
parent 4721e96ca3
commit 90e85fea5e
2 changed files with 29 additions and 8 deletions

View File

@@ -7,6 +7,7 @@ export * from "./useInput.js"
export * from "./useLayoutEffect.js" export * from "./useLayoutEffect.js"
export * from "./useMemo.js" export * from "./useMemo.js"
export * from "./useOnce.js" export * from "./useOnce.js"
export * from "./useOptionalInput.js"
export * from "./useRefFromReactiveValue.js" export * from "./useRefFromReactiveValue.js"
export * from "./useRefState.js" export * from "./useRefState.js"
export * from "./useScope.js" export * from "./useScope.js"

View File

@@ -1,5 +1,6 @@
import { type Duration, Effect, flow, identity, Option, type ParseResult, Ref, Schema, Stream, SubscriptionRef } from "effect" import { type Duration, Effect, flow, identity, Option, type ParseResult, Ref, Schema, Stream, SubscriptionRef } from "effect"
import * as React from "react" import * as React from "react"
import { SetStateAction } from "../../types/index.js"
import { useCallbackSync } from "./useCallbackSync.js" import { useCallbackSync } from "./useCallbackSync.js"
import { useFork } from "./useFork.js" import { useFork } from "./useFork.js"
import { useOnce } from "./useOnce.js" import { useOnce } from "./useOnce.js"
@@ -17,6 +18,8 @@ export namespace useOptionalInput {
export interface Result { export interface Result {
readonly value: string readonly value: string
readonly onChange: React.ChangeEventHandler<HTMLInputElement | HTMLTextAreaElement> readonly onChange: React.ChangeEventHandler<HTMLInputElement | HTMLTextAreaElement>
readonly disabled: boolean
readonly setDisabled: React.Dispatch<React.SetStateAction<boolean>>
readonly error: Option.Option<ParseResult.ParseError> readonly error: Option.Option<ParseResult.ParseError>
} }
} }
@@ -24,7 +27,7 @@ export namespace useOptionalInput {
export const useOptionalInput: { export const useOptionalInput: {
<A, R>(options: useOptionalInput.Options<A, R>): Effect.Effect<useOptionalInput.Result, ParseResult.ParseError, R> <A, R>(options: useOptionalInput.Options<A, R>): Effect.Effect<useOptionalInput.Result, ParseResult.ParseError, R>
} = Effect.fnUntraced(function* <A, R>(options: useOptionalInput.Options<A, R>) { } = Effect.fnUntraced(function* <A, R>(options: useOptionalInput.Options<A, R>) {
const [internalRef, initialDisabled] = yield* useOnce(() => Effect.andThen(options.ref, upstreamValue => const [internalRef, disabledRef] = yield* useOnce(() => Effect.andThen(options.ref, upstreamValue =>
Effect.all([ Effect.all([
Effect.andThen( Effect.andThen(
Option.match(upstreamValue, { Option.match(upstreamValue, {
@@ -36,11 +39,10 @@ export const useOptionalInput: {
SubscriptionRef.make, 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<ParseResult.ParseError>()) const [error, setError] = React.useState(Option.none<ParseResult.ParseError>())
yield* useFork(() => Effect.all([ yield* useFork(() => Effect.all([
@@ -50,12 +52,15 @@ export const useOptionalInput: {
onSome: upstreamValue => Effect.andThen( onSome: upstreamValue => Effect.andThen(
Effect.all([Schema.encode(options.schema)(upstreamValue), internalRef]), Effect.all([Schema.encode(options.schema)(upstreamValue), internalRef]),
([encodedUpstreamValue, internalValue]) => Effect.when( ([encodedUpstreamValue, internalValue]) => Effect.when(
Effect.andThen(
Ref.set(internalRef, encodedUpstreamValue), Ref.set(internalRef, encodedUpstreamValue),
Ref.set(disabledRef, false),
),
() => encodedUpstreamValue !== internalValue, () => encodedUpstreamValue !== internalValue,
), ),
), ),
onNone: () => Effect.sync(() => setDisabled(true)), onNone: () => Ref.set(disabledRef, true),
})), })),
// Sync all changes to the internal state with upstream // 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), internalRef.changes.pipe(options.debounce ? Stream.debounce(options.debounce) : identity),
flow( flow(
Schema.decode(options.schema), 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.andThen(() => setError(Option.none())),
Effect.catchTag("ParseError", e => Effect.sync(() => setError(Option.some(e)))), 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, internalRef])
const [value] = yield* useSubscribeRefs(internalRef) const [value, disabled] = yield* useSubscribeRefs(internalRef, disabledRef)
const onChange = yield* useCallbackSync((e: React.ChangeEvent<HTMLInputElement>) => Ref.set( const onChange = yield* useCallbackSync((e: React.ChangeEvent<HTMLInputElement>) => Ref.set(
internalRef, internalRef,
e.target.value, e.target.value,
), [internalRef]) ), [internalRef])
return { value, onChange, error } const setDisabled = yield* useCallbackSync((setStateAction: React.SetStateAction<boolean>) =>
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 }
}) })