From ea5b5f9ac26fa3006bad9037d93fe5155cb7ab0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Mon, 18 Aug 2025 03:11:09 +0200 Subject: [PATCH] Input work --- packages/effect-fc/src/hooks/Hooks/index.ts | 3 ++- .../effect-fc/src/hooks/Hooks/useInput.ts | 20 ++++++++----------- .../hooks/Hooks/useRefFromReactiveValue.ts | 12 ----------- .../src/hooks/Hooks/useRefFromState.ts | 20 +++++++++++++++++++ packages/example/src/routes/dev/input.tsx | 19 +++++++++++------- 5 files changed, 42 insertions(+), 32 deletions(-) delete mode 100644 packages/effect-fc/src/hooks/Hooks/useRefFromReactiveValue.ts create mode 100644 packages/effect-fc/src/hooks/Hooks/useRefFromState.ts diff --git a/packages/effect-fc/src/hooks/Hooks/index.ts b/packages/effect-fc/src/hooks/Hooks/index.ts index 6b67a59..a788fb9 100644 --- a/packages/effect-fc/src/hooks/Hooks/index.ts +++ b/packages/effect-fc/src/hooks/Hooks/index.ts @@ -3,12 +3,13 @@ export * from "./useCallbackPromise.js" export * from "./useCallbackSync.js" export * from "./useContext.js" export * from "./useEffect.js" +export * from "./useFork.js" 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 "./useRefFromState.js" export * from "./useRefState.js" export * from "./useScope.js" export * from "./useStreamFromReactiveValues.js" diff --git a/packages/effect-fc/src/hooks/Hooks/useInput.ts b/packages/effect-fc/src/hooks/Hooks/useInput.ts index d63c8d8..eba241f 100644 --- a/packages/effect-fc/src/hooks/Hooks/useInput.ts +++ b/packages/effect-fc/src/hooks/Hooks/useInput.ts @@ -1,9 +1,8 @@ import { type Duration, Effect, flow, identity, Option, type ParseResult, Ref, Schema, Stream, SubscriptionRef } from "effect" import * as React from "react" -import { useCallbackSync } from "./useCallbackSync.js" import { useFork } from "./useFork.js" import { useOnce } from "./useOnce.js" -import { useSubscribeRefs } from "./useSubscribeRefs.js" +import { useRefFromState } from "./useRefFromState.js" export namespace useInput { @@ -23,12 +22,14 @@ export namespace useInput { export const useInput: { (options: useInput.Options): Effect.Effect } = Effect.fnUntraced(function* (options: useInput.Options) { - const internalRef = yield* useOnce(() => options.ref.pipe( - Effect.andThen(Schema.encode(options.schema)), - Effect.andThen(SubscriptionRef.make), - )) + const internalState = React.useState(yield* useOnce(() => Effect.andThen( + options.ref, + Schema.encode(options.schema), + ))) + const [value, setValue] = internalState const [error, setError] = React.useState(Option.none()) + const internalRef = yield* useRefFromState(internalState) 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. @@ -54,11 +55,6 @@ export const useInput: { ), ], { concurrency: "unbounded" }), [options.ref, options.schema, options.debounce, internalRef]) - const [value] = yield* useSubscribeRefs(internalRef) - const onChange = yield* useCallbackSync((e: React.ChangeEvent) => Ref.set( - internalRef, - e.target.value, - ), [internalRef]) - + const onChange = React.useCallback((e: React.ChangeEvent) => setValue(e.target.value), []) return { value, onChange, error } }) diff --git a/packages/effect-fc/src/hooks/Hooks/useRefFromReactiveValue.ts b/packages/effect-fc/src/hooks/Hooks/useRefFromReactiveValue.ts deleted file mode 100644 index d3e0f39..0000000 --- a/packages/effect-fc/src/hooks/Hooks/useRefFromReactiveValue.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Effect, Ref, SubscriptionRef } from "effect" -import { useEffect } from "./useEffect.js" -import { useOnce } from "./useOnce.js" - - -export const useRefFromReactiveValue: { - (value: A): Effect.Effect> -} = Effect.fnUntraced(function*(value) { - const ref = yield* useOnce(() => SubscriptionRef.make(value)) - yield* useEffect(() => Ref.set(ref, value), [value]) - return ref -}) diff --git a/packages/effect-fc/src/hooks/Hooks/useRefFromState.ts b/packages/effect-fc/src/hooks/Hooks/useRefFromState.ts new file mode 100644 index 0000000..a6138f8 --- /dev/null +++ b/packages/effect-fc/src/hooks/Hooks/useRefFromState.ts @@ -0,0 +1,20 @@ +import { Effect, Equivalence, Ref, Stream, SubscriptionRef } from "effect" +import type * as React from "react" +import { useEffect } from "./useEffect.js" +import { useFork } from "./useFork.js" +import { useOnce } from "./useOnce.js" + + +export const useRefFromState: { + (state: readonly [A, React.Dispatch>]): Effect.Effect> +} = Effect.fnUntraced(function*([value, setValue]) { + const ref = yield* useOnce(() => SubscriptionRef.make(value)) + + yield* useEffect(() => Ref.set(ref, value), [value]) + yield* useFork(() => Stream.runForEach( + Stream.changesWith(ref.changes, Equivalence.strict()), + v => Effect.sync(() => setValue(v)), + ), [setValue]) + + return ref +}) diff --git a/packages/example/src/routes/dev/input.tsx b/packages/example/src/routes/dev/input.tsx index c17b910..6895fdc 100644 --- a/packages/example/src/routes/dev/input.tsx +++ b/packages/example/src/routes/dev/input.tsx @@ -1,27 +1,32 @@ import { TextFieldInput } from "@/lib/TextFieldInput" import { runtime } from "@/runtime" -import { Container } from "@radix-ui/themes" +import { Container, TextField } from "@radix-ui/themes" import { createFileRoute } from "@tanstack/react-router" import { Console, Schema, Stream, SubscriptionRef } from "effect" import { Component, Memo } from "effect-fc" -import { useOnce } from "effect-fc/hooks" -import { useFork } from "effect-fc/hooks/Hooks/useFork" +import { useFork, useInput, useOnce, useRefState } from "effect-fc/hooks" -const IntTextFieldInput = TextFieldInput({ schema: Schema.NumberFromString.pipe(Schema.int()) }) +const IntFromString = Schema.NumberFromString.pipe(Schema.int()) + +const IntTextFieldInput = TextFieldInput({ schema: IntFromString }) const StringTextFieldInput = TextFieldInput({ schema: Schema.String }) const Input = Component.makeUntraced(function* Input() { const IntTextFieldInputFC = yield* IntTextFieldInput const StringTextFieldInputFC = yield* StringTextFieldInput - const intRef = yield* useOnce(() => SubscriptionRef.make(0)) + const intRef1 = yield* useOnce(() => SubscriptionRef.make(0)) + const intRef2 = yield* useOnce(() => SubscriptionRef.make(0)) const stringRef = yield* useOnce(() => SubscriptionRef.make("")) - yield* useFork(() => Stream.runForEach(intRef.changes, Console.log), [intRef]) + // yield* useFork(() => Stream.runForEach(intRef1.changes, Console.log), [intRef1]) + + const input2 = yield* useInput({ schema: IntFromString, ref: intRef2 }) return ( - + + )