Add debounced form status API and upgrade Effect beta dependencies (#71)
# Add debounced form status API and upgrade Effect beta dependencies ## Summary - Add `Form.useStatus` for debounced form validation and commit status. - Expose `isValidating`, `isCommitting`, and `canCommit` as a presentation-friendly status object. - Support configurable status debounce intervals, defaulting to 250 ms. - Update form documentation and examples to use `Form.useStatus`. - Bump `effect-view` to `0.1.2`. - Upgrade Effect, `@effect/platform-browser`, and `effect-lens` to beta.103. - Align the example workspace with Effect beta.103. - Regenerate `bun.lock`. ## Validation - `bun run lint:tsc` - `bun run lint:biome` - `bun run --cwd packages/effect-view test` --------- Co-authored-by: Julien Valverdé <julien.valverde@mailo.com> Reviewed-on: #71
This commit was merged in pull request #71.
This commit is contained in:
@@ -82,21 +82,21 @@
|
||||
},
|
||||
"packages/effect-view": {
|
||||
"name": "effect-view",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.2",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "^1.1.0",
|
||||
"effect-lens": "^2.0.1-beta.102",
|
||||
"effect-lens": "2.0.1-beta.103",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@effect/platform-browser": "4.0.0-beta.102",
|
||||
"@effect/platform-browser": "4.0.0-beta.103",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"effect": "4.0.0-beta.102",
|
||||
"effect": "4.0.0-beta.103",
|
||||
"jsdom": "^26.1.0",
|
||||
"vitest": "^3.2.4",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.2.0",
|
||||
"effect": "4.0.0-beta.102",
|
||||
"effect": "4.0.0-beta.103",
|
||||
"react": "^19.2.0",
|
||||
},
|
||||
},
|
||||
@@ -104,9 +104,9 @@
|
||||
"name": "@effect-view/example",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@effect/platform-browser": "4.0.0-beta.102",
|
||||
"@effect/platform-browser": "4.0.0-beta.103",
|
||||
"@radix-ui/themes": "^3.3.0",
|
||||
"effect": "4.0.0-beta.102",
|
||||
"effect": "4.0.0-beta.103",
|
||||
"effect-view": "workspace:*",
|
||||
"react-icons": "^5.6.0",
|
||||
},
|
||||
@@ -3052,9 +3052,9 @@
|
||||
|
||||
"@docusaurus/utils/jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="],
|
||||
|
||||
"@effect-view/example/@effect/platform-browser": ["@effect/platform-browser@4.0.0-beta.102", "", { "dependencies": { "multipasta": "^0.2.8" }, "peerDependencies": { "effect": "^4.0.0-beta.102" } }, "sha512-DXXVt+oOpukqI3lEaGvvXQxSobzIy3lAsIQvlpVK+MuYBm1o4FHZrHKbrOFItDUvSR1t0kiTncFPRi01YaJUmA=="],
|
||||
"@effect-view/example/@effect/platform-browser": ["@effect/platform-browser@4.0.0-beta.103", "", { "dependencies": { "multipasta": "^0.2.8" }, "peerDependencies": { "effect": "^4.0.0-beta.103" } }, "sha512-3GXGzjv+dB5E9sCCj/JhPVyNB2NrBquGY2tYQ8VQqGK4uOpME6I5oLqRt+MmB7BWqWYPXUqvXj0wvMJRlQC4fw=="],
|
||||
|
||||
"@effect-view/example/effect": ["effect@4.0.0-beta.102", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.9.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.4", "multipasta": "^0.2.8", "toml": "^4.1.2", "uuid": "^14.0.1", "yaml": "^2.9.0" } }, "sha512-z8Y+Q76Hh/kjLFZrXu8tGn6e+tDsg45R+UHhxd190pXxD53OGwf/G/zDxXTkse4HJ5mobNZfitLfUCp4fMvu6w=="],
|
||||
"@effect-view/example/effect": ["effect@4.0.0-beta.103", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.9.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.4", "multipasta": "^0.2.8", "toml": "^4.1.2", "uuid": "^14.0.1", "yaml": "^2.9.0" } }, "sha512-pE8TxF4m2tQzVI+77dIlm3s+81TACV1AiX1JEkvY+zVuxgQQ8aGSkqXNJF6b/ST+coCSk5cUdbULpQ7sm4oHyw=="],
|
||||
|
||||
"@effect-view/vite-plugin/typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
|
||||
|
||||
@@ -3122,11 +3122,11 @@
|
||||
|
||||
"dot-prop/is-obj": ["is-obj@2.0.0", "", {}, "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w=="],
|
||||
|
||||
"effect-view/@effect/platform-browser": ["@effect/platform-browser@4.0.0-beta.102", "", { "dependencies": { "multipasta": "^0.2.8" }, "peerDependencies": { "effect": "^4.0.0-beta.102" } }, "sha512-DXXVt+oOpukqI3lEaGvvXQxSobzIy3lAsIQvlpVK+MuYBm1o4FHZrHKbrOFItDUvSR1t0kiTncFPRi01YaJUmA=="],
|
||||
"effect-view/@effect/platform-browser": ["@effect/platform-browser@4.0.0-beta.103", "", { "dependencies": { "multipasta": "^0.2.8" }, "peerDependencies": { "effect": "^4.0.0-beta.103" } }, "sha512-3GXGzjv+dB5E9sCCj/JhPVyNB2NrBquGY2tYQ8VQqGK4uOpME6I5oLqRt+MmB7BWqWYPXUqvXj0wvMJRlQC4fw=="],
|
||||
|
||||
"effect-view/effect": ["effect@4.0.0-beta.102", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.9.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.4", "multipasta": "^0.2.8", "toml": "^4.1.2", "uuid": "^14.0.1", "yaml": "^2.9.0" } }, "sha512-z8Y+Q76Hh/kjLFZrXu8tGn6e+tDsg45R+UHhxd190pXxD53OGwf/G/zDxXTkse4HJ5mobNZfitLfUCp4fMvu6w=="],
|
||||
"effect-view/effect": ["effect@4.0.0-beta.103", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.9.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.4", "multipasta": "^0.2.8", "toml": "^4.1.2", "uuid": "^14.0.1", "yaml": "^2.9.0" } }, "sha512-pE8TxF4m2tQzVI+77dIlm3s+81TACV1AiX1JEkvY+zVuxgQQ8aGSkqXNJF6b/ST+coCSk5cUdbULpQ7sm4oHyw=="],
|
||||
|
||||
"effect-view/effect-lens": ["effect-lens@2.0.1-beta.102", "", { "peerDependencies": { "effect": "4.0.0-beta.102" } }, "sha512-YQSPI/r42Do/24X+RurTd4BLV2EdxG4LcgySpR4Pt7W2Z+wCmoBlSAV+B7rs2lTpqOny3kKTLnn9tbpfQVzSAA=="],
|
||||
"effect-view/effect-lens": ["effect-lens@2.0.1-beta.103", "", { "peerDependencies": { "effect": "4.0.0-beta.103" } }, "sha512-Z9yiCnC4Dhol68gjTqyj+YZ+ldu1ou5eOGCT7kX01j1/apx/VU6HCYi9bqOoe1i0RG/wr+yCf2kJgodvTXCFfg=="],
|
||||
|
||||
"esrecurse/estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
|
||||
|
||||
|
||||
@@ -254,6 +254,13 @@ Use `Form.useOptionalInput` when the encoded field is an Effect `Option`. It
|
||||
returns `value` and `setValue` together with `enabled` and `setEnabled`, making
|
||||
it suitable for an optional input that the user can toggle on and off.
|
||||
|
||||
Use `Form.useStatus` for displayed form status. It debounces short-lived state
|
||||
changes, avoiding flicker in pending indicators.
|
||||
|
||||
```tsx
|
||||
const { isValidating, isCommitting, canCommit } = yield* Form.useStatus(form)
|
||||
```
|
||||
|
||||
`useInput` and `useOptionalInput` can be used directly, but they are primarily
|
||||
building blocks for your own reusable input components. A component library
|
||||
can wrap them to consistently handle labels, validation issues, validating
|
||||
@@ -299,11 +306,8 @@ export const TextFieldFormInputView = Component.make(
|
||||
>,
|
||||
) {
|
||||
const input = yield* Form.useInput(props.form, props)
|
||||
const [issues, isValidating, isCommitting] = yield* View.useAll([
|
||||
props.form.issues,
|
||||
props.form.isValidating,
|
||||
props.form.isCommitting,
|
||||
])
|
||||
const [issues] = yield* View.useAll([props.form.issues])
|
||||
const { isValidating, isCommitting } = yield* Form.useStatus(props.form)
|
||||
|
||||
return (
|
||||
<Flex direction="column" gap="1">
|
||||
@@ -351,8 +355,9 @@ They expose the schema pipeline as reactive Lenses and Views:
|
||||
| `isCommitting` | Whether a mutation or target write is in progress. |
|
||||
|
||||
The form model itself is Effect code. Effect View adds the React-facing hooks,
|
||||
including `Form.useInput` and `Form.useOptionalInput`, while your own component
|
||||
library remains responsible for rendering controls and layout.
|
||||
including `Form.useInput`, `Form.useOptionalInput`, and `Form.useStatus`, while
|
||||
your own component library remains responsible for rendering controls and
|
||||
layout.
|
||||
|
||||
## Example: local date input, UTC domain value
|
||||
|
||||
@@ -415,7 +420,7 @@ the mutation receives UTC:
|
||||
|
||||
```tsx
|
||||
import { DateTime, Effect, Schema } from "effect"
|
||||
import { Component, Form, MutationForm, View } from "effect-view"
|
||||
import { Component, Form, MutationForm } from "effect-view"
|
||||
import { DateTimeUtcFromZonedInput } from "./DateTimeUtcFromZonedInput"
|
||||
|
||||
const AppointmentSchema = Schema.Struct({
|
||||
@@ -439,7 +444,7 @@ const AppointmentView = Component.make("Appointment")(function* () {
|
||||
)
|
||||
|
||||
const startsAt = yield* Form.useInput(startsAtField)
|
||||
const [canCommit] = yield* View.useAll([form.canCommit])
|
||||
const { canCommit } = yield* Form.useStatus(form)
|
||||
const runPromise = yield* Component.useRunPromise()
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "effect-view",
|
||||
"description": "Write React function components with Effect",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.2",
|
||||
"type": "module",
|
||||
"files": [
|
||||
"./README.md",
|
||||
@@ -97,19 +97,19 @@
|
||||
"clean:modules": "rm -rf node_modules"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@effect/platform-browser": "4.0.0-beta.102",
|
||||
"@effect/platform-browser": "4.0.0-beta.103",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"effect": "4.0.0-beta.102",
|
||||
"effect": "4.0.0-beta.103",
|
||||
"jsdom": "^26.1.0",
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.2.0",
|
||||
"effect": "4.0.0-beta.102",
|
||||
"effect": "4.0.0-beta.103",
|
||||
"react": "^19.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "^1.1.0",
|
||||
"effect-lens": "^2.0.1-beta.102"
|
||||
"effect-lens": "2.0.1-beta.103"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { StandardSchemaV1 } from "@standard-schema/spec"
|
||||
import { Array, type Cause, Chunk, type Duration, Effect, Equal, Function, identity, Option, Pipeable, Predicate, type Scope, Stream, SubscriptionRef } from "effect"
|
||||
import type * as React from "react"
|
||||
import * as React from "react"
|
||||
import * as Component from "./Component.js"
|
||||
import * as Lens from "./Lens.js"
|
||||
import * as View from "./View.js"
|
||||
@@ -156,6 +156,47 @@ export const focusChunkAt: {
|
||||
})
|
||||
|
||||
|
||||
export declare namespace useStatus {
|
||||
export interface Options {
|
||||
/**
|
||||
* The debounce interval applied to changes in the returned status.
|
||||
*
|
||||
* Defaults to 250 milliseconds.
|
||||
*/
|
||||
readonly debounce?: Duration.Input
|
||||
}
|
||||
|
||||
export interface Success {
|
||||
readonly isValidating: boolean
|
||||
readonly isCommitting: boolean
|
||||
readonly canCommit: boolean
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to form status flags for presentation.
|
||||
*
|
||||
* Status changes are debounced to avoid transient UI feedback. The underlying
|
||||
* form state is not changed.
|
||||
*/
|
||||
export const useStatus = Effect.fnUntraced(function* <P extends readonly PropertyKey[], A, I, ER, EW>(
|
||||
form: Form<P, A, I, ER, EW>,
|
||||
options?: useStatus.Options,
|
||||
): Effect.fn.Return<useStatus.Success, ER, Scope.Scope> {
|
||||
const views = React.useMemo(() => {
|
||||
const debounce = options?.debounce ?? "250 millis"
|
||||
|
||||
return [
|
||||
View.mapStream(form.isValidating, Stream.debounce(debounce)),
|
||||
View.mapStream(form.isCommitting, Stream.debounce(debounce)),
|
||||
View.mapStream(form.canCommit, Stream.debounce(debounce)),
|
||||
] as const
|
||||
}, [form, options?.debounce])
|
||||
|
||||
const [isValidating, isCommitting, canCommit] = yield* View.useAll(views)
|
||||
return { isValidating, isCommitting, canCommit }
|
||||
})
|
||||
|
||||
export namespace useInput {
|
||||
export interface Options {
|
||||
readonly debounce?: Duration.Input
|
||||
|
||||
@@ -27,15 +27,15 @@
|
||||
"vite": "^8.0.16"
|
||||
},
|
||||
"dependencies": {
|
||||
"@effect/platform-browser": "4.0.0-beta.102",
|
||||
"@effect/platform-browser": "4.0.0-beta.103",
|
||||
"@radix-ui/themes": "^3.3.0",
|
||||
"effect": "4.0.0-beta.102",
|
||||
"effect": "4.0.0-beta.103",
|
||||
"effect-view": "workspace:*",
|
||||
"react-icons": "^5.6.0"
|
||||
},
|
||||
"overrides": {
|
||||
"@types/react": "^19.2.15",
|
||||
"effect": "4.0.0-beta.102",
|
||||
"effect": "4.0.0-beta.103",
|
||||
"react": "^19.2.6"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user