0.1.3 #4

Merged
Thilawyn merged 90 commits from next into master 2025-08-23 03:07:28 +02:00
3 changed files with 22 additions and 21 deletions
Showing only changes of commit d7128c963f - Show all commits

View File

@@ -1,4 +1,4 @@
import { type Duration, Effect, Equal, flow, identity, Option, type ParseResult, Ref, Schema, Stream, SubscriptionRef } from "effect" import { type Duration, Effect, Equal, type Equivalence, flow, identity, Option, type ParseResult, Ref, Schema, Stream, SubscriptionRef } from "effect"
import * as React from "react" import * as React from "react"
import { useFork } from "./useFork.js" import { useFork } from "./useFork.js"
import { useOnce } from "./useOnce.js" import { useOnce } from "./useOnce.js"
@@ -10,6 +10,7 @@ export namespace useInput {
readonly schema: Schema.Schema<A, string, R> readonly schema: Schema.Schema<A, string, R>
readonly ref: SubscriptionRef.SubscriptionRef<A> readonly ref: SubscriptionRef.SubscriptionRef<A>
readonly debounce?: Duration.DurationInput readonly debounce?: Duration.DurationInput
readonly equivalence?: Equivalence.Equivalence<A>
} }
export interface Result { export interface Result {
@@ -30,7 +31,7 @@ export const useInput: {
yield* useFork(() => Effect.all([ yield* useFork(() => Effect.all([
// Sync the upstream state with the internal state // 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. // Only mutate the internal state if the upstream value is actually different. This avoids infinite re-render loops.
Stream.runForEach(options.ref.changes, upstreamValue => Stream.runForEach(options.ref.changes, upstreamValue =>
Effect.whenEffect( Effect.whenEffect(
Effect.andThen( Effect.andThen(
@@ -39,7 +40,7 @@ export const useInput: {
), ),
internalRef.pipe( internalRef.pipe(
Effect.andThen(Schema.decode(options.schema)), Effect.andThen(Schema.decode(options.schema)),
Effect.andThen(decodedInternalValue => !Equal.equals(upstreamValue, decodedInternalValue)), Effect.andThen(decodedInternalValue => !(options.equivalence ?? Equal.equals)(upstreamValue, decodedInternalValue)),
), ),
) )
), ),
@@ -54,7 +55,7 @@ export const useInput: {
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, options.equivalence, internalRef])
const [value, setValue] = yield* useRefState(internalRef) const [value, setValue] = yield* useRefState(internalRef)
const onChange = React.useCallback((e: React.ChangeEvent<HTMLInputElement>) => setValue(e.target.value), [setValue]) const onChange = React.useCallback((e: React.ChangeEvent<HTMLInputElement>) => setValue(e.target.value), [setValue])

View File

@@ -1,9 +1,10 @@
import { type Duration, Effect, flow, identity, Option, type ParseResult, Ref, Schema, Stream, SubscriptionRef } from "effect" import { type Duration, Effect, Equal, type Equivalence, 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 { 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"
import { useRefState } from "./useRefState.js"
import { useSubscribeRefs } from "./useSubscribeRefs.js" import { useSubscribeRefs } from "./useSubscribeRefs.js"
@@ -13,6 +14,7 @@ export namespace useOptionalInput {
readonly defaultValue?: A readonly defaultValue?: A
readonly ref: SubscriptionRef.SubscriptionRef<Option.Option<A>> readonly ref: SubscriptionRef.SubscriptionRef<Option.Option<A>>
readonly debounce?: Duration.DurationInput readonly debounce?: Duration.DurationInput
readonly equivalence?: Equivalence.Equivalence<A>
} }
export interface Result { export interface Result {
@@ -49,14 +51,14 @@ export const useOptionalInput: {
// Sync the upstream state with the internal state // 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. // 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({ Stream.runForEach(options.ref.changes, Option.match({
onSome: upstreamValue => Effect.andThen( onSome: upstreamValue => Effect.whenEffect(
Effect.all([Schema.encode(options.schema)(upstreamValue), internalRef]), Effect.andThen(
([encodedUpstreamValue, internalValue]) => Effect.when( Schema.encode(options.schema)(upstreamValue),
Effect.andThen( encodedUpstreamValue => Ref.set(internalRef, encodedUpstreamValue),
Ref.set(internalRef, encodedUpstreamValue), ),
Ref.set(disabledRef, false), internalRef.pipe(
), Effect.andThen(Schema.decode(options.schema)),
() => encodedUpstreamValue !== internalValue, Effect.andThen(decodedInternalValue => !(options.equivalence ?? Equal.equals)(upstreamValue, decodedInternalValue)),
), ),
), ),
@@ -73,13 +75,7 @@ export const useOptionalInput: {
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, options.equivalence, internalRef])
const [value, disabled] = yield* useSubscribeRefs(internalRef, disabledRef)
const onChange = yield* useCallbackSync((e: React.ChangeEvent<HTMLInputElement>) => Ref.set(
internalRef,
e.target.value,
), [internalRef])
const setDisabled = yield* useCallbackSync((setStateAction: React.SetStateAction<boolean>) => const setDisabled = yield* useCallbackSync((setStateAction: React.SetStateAction<boolean>) =>
Effect.andThen( Effect.andThen(
@@ -98,5 +94,9 @@ export const useOptionalInput: {
), ),
[disabledRef, options.ref, internalRef, options.schema]) [disabledRef, options.ref, internalRef, options.schema])
const [disabled] = yield* useSubscribeRefs(disabledRef)
const [value, setValue] = yield* useRefState(internalRef)
const onChange = React.useCallback((e: React.ChangeEvent<HTMLInputElement>) => setValue(e.target.value), [setValue])
return { value, onChange, disabled, setDisabled, error } return { value, onChange, disabled, setDisabled, error }
}) })

View File

@@ -29,7 +29,7 @@ const Input = Component.makeUntraced(function* Input() {
<Container> <Container>
<IntTextFieldInputFC ref={intRef1} /> <IntTextFieldInputFC ref={intRef1} />
<StringTextFieldInputFC ref={stringRef} /> <StringTextFieldInputFC ref={stringRef} />
<TextField.Root value={str} onChange={e => setStr(e.target.value)} /> <StringTextFieldInputFC ref={stringRef} />
</Container> </Container>
) )
}).pipe( }).pipe(