From fef1b70b2c44ee85d267c7545fd940e98f02db10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Tue, 19 Aug 2025 20:07:35 +0200 Subject: [PATCH] Fix --- .../effect-fc/src/hooks/Hooks/useInput.ts | 10 ++--- .../src/hooks/Hooks/useOptionalInput.ts | 42 +++++++++---------- packages/example/src/lib/TextAreaInput.tsx | 39 ++++++++--------- packages/example/src/lib/TextFieldInput.tsx | 19 ++++----- packages/example/src/todo/Todo.tsx | 2 +- 5 files changed, 49 insertions(+), 63 deletions(-) diff --git a/packages/effect-fc/src/hooks/Hooks/useInput.ts b/packages/effect-fc/src/hooks/Hooks/useInput.ts index 19333c9..9d7a483 100644 --- a/packages/effect-fc/src/hooks/Hooks/useInput.ts +++ b/packages/effect-fc/src/hooks/Hooks/useInput.ts @@ -8,14 +8,14 @@ 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 - readonly equivalence?: Equivalence.Equivalence } export interface Result { readonly value: string - readonly onChange: React.ChangeEventHandler + readonly setValue: React.Dispatch> readonly error: Option.Option } } @@ -56,10 +56,8 @@ export const useInput: { Effect.catchTag("ParseError", e => Effect.sync(() => setError(Option.some(e)))), ), ), - ], { concurrency: "unbounded" }), [options.ref, options.schema, options.debounce, options.equivalence, internalRef]) + ], { concurrency: "unbounded" }), [options.schema, options.equivalence, options.ref, options.debounce, internalRef]) const [value, setValue] = yield* useRefState(internalRef) - const onChange = React.useCallback((e: React.ChangeEvent) => setValue(e.target.value), [setValue]) - - return { value, onChange, error } + return { value, setValue, error } }) diff --git a/packages/effect-fc/src/hooks/Hooks/useOptionalInput.ts b/packages/effect-fc/src/hooks/Hooks/useOptionalInput.ts index d4977d1..837739b 100644 --- a/packages/effect-fc/src/hooks/Hooks/useOptionalInput.ts +++ b/packages/effect-fc/src/hooks/Hooks/useOptionalInput.ts @@ -12,16 +12,16 @@ 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 - readonly equivalence?: Equivalence.Equivalence } export interface Result { readonly value: string - readonly onChange: React.ChangeEventHandler - readonly disabled: boolean - readonly setDisabled: React.Dispatch> + readonly setValue: React.Dispatch> + readonly enabled: boolean + readonly setEnabled: React.Dispatch> readonly error: Option.Option } } @@ -29,7 +29,7 @@ export namespace useOptionalInput { export const useOptionalInput: { (options: useOptionalInput.Options): Effect.Effect } = Effect.fnUntraced(function* (options: useOptionalInput.Options) { - const [internalRef, disabledRef] = yield* useOnce(() => Effect.andThen(options.ref, upstreamValue => + const [internalRef, enabledRef] = yield* useOnce(() => Effect.andThen(options.ref, upstreamValue => Effect.all([ Effect.andThen( Option.match(upstreamValue, { @@ -41,7 +41,7 @@ export const useOptionalInput: { SubscriptionRef.make, ), - SubscriptionRef.make(Option.isNone(upstreamValue)), + SubscriptionRef.make(Option.isSome(upstreamValue)), ]) )) @@ -63,7 +63,7 @@ export const useOptionalInput: { ), ), - onNone: () => Ref.set(disabledRef, true), + onNone: () => Ref.set(enabledRef, false), })), // Sync all changes to the internal state with upstream @@ -76,28 +76,24 @@ export const useOptionalInput: { Effect.catchTag("ParseError", e => Effect.sync(() => setError(Option.some(e)))), ), ), - ], { concurrency: "unbounded" }), [options.ref, options.schema, options.debounce, options.equivalence, internalRef]) + ], { concurrency: "unbounded" }), [options.schema, options.equivalence, options.ref, options.debounce, 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( + 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()), ), - [disabledRef, options.ref, internalRef, options.schema]) + [options.schema, options.ref, internalRef, enabledRef], + ) - const [disabled] = yield* useSubscribeRefs(disabledRef) + const [enabled] = yield* useSubscribeRefs(enabledRef) const [value, setValue] = yield* useRefState(internalRef) - const onChange = React.useCallback((e: React.ChangeEvent) => setValue(e.target.value), [setValue]) - - return { value, onChange, disabled, setDisabled, error } + return { value, setValue, enabled, setEnabled, error } }) diff --git a/packages/example/src/lib/TextAreaInput.tsx b/packages/example/src/lib/TextAreaInput.tsx index 4893148..9bfeb37 100644 --- a/packages/example/src/lib/TextAreaInput.tsx +++ b/packages/example/src/lib/TextAreaInput.tsx @@ -1,45 +1,40 @@ import { Callout, Flex, TextArea, TextAreaProps } from "@radix-ui/themes" -import { Option, ParseResult, Schema, Struct } from "effect" +import { Array, Equivalence, Option, ParseResult, Schema, Struct } from "effect" import { Component } from "effect-fc" import { useInput } from "effect-fc/hooks" import * as React from "react" -export interface TextAreaInputProps -extends - Omit, "schema">, - Omit -{} +export type TextAreaInputProps = Omit, "schema" | "equivalence"> & Omit -export const TextAreaInput = ( - schema: Schema.Schema -): Component.Component< +export const TextAreaInput = (options: { + readonly schema: Schema.Schema + readonly equivalence?: Equivalence.Equivalence +}): Component.Component< TextAreaInputProps, React.JSX.Element, ParseResult.ParseError, R > => Component.makeUntraced(function* TextFieldInput(props) { - const input = yield* useInput({ schema, ...props }) - const issues = React.useMemo(() => Option.map( - input.error, - ParseResult.ArrayFormatter.formatErrorSync, + 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 ( - {Option.isSome(issues) && - - -
    {issues.value.map((issue, i) =>
  • {issue.message}
  • )}
-
-
- } -