2 Commits

Author SHA1 Message Date
Julien Valverdé 3fef9b1a47 Bump version
Publish / publish (push) Successful in 3m26s
Lint / lint (push) Successful in 47s
2026-07-30 15:45:43 +02:00
Thilawyn 2b1c6aeda9 Add explicit lifecycle pipelines and scheduled query refreshes (#68)
Lint / lint (push) Successful in 48s
Publish / publish (push) Has been cancelled
## Summary

- Replace auto-running `service` constructors with explicit `make(...).pipe(thenRun)` lifecycle pipelines.
- Add scoped, schedule-driven Query refreshes with `Query.withScheduledRefresh`.
- Upgrade the Effect v4 stack to beta 102.
- Add `@effect-view/vite-plugin` to the publish workflow.
- Update examples and documentation for the new APIs.

## Breaking changes

The following constructors have been replaced:

- `Query.service(options)` → `Query.make(options).pipe(Query.thenRun)`
- `QueryClient.service(options)` → `QueryClient.make(options).pipe(QueryClient.thenRun)`
- `MutationForm.service(options)` → `MutationForm.make(options).pipe(MutationForm.thenRun)`
- `LensForm.service(options)` → `LensForm.make(options).pipe(LensForm.thenRun)`

This separates object construction from starting its scoped background behavior.

## Scheduled Query refreshes

Queries can now be refreshed with any Effect `Schedule`:

```ts
const query = yield* Query.make(options).pipe(
  Query.thenRun,
  Query.withScheduledRefresh(Schedule.spaced("5 minutes")),
)

---------

Co-authored-by: Julien Valverdé <julien.valverde@mailo.com>
Reviewed-on: #68
2026-07-30 15:36:49 +02:00
16 changed files with 109 additions and 119 deletions
+1 -1
View File
@@ -9,7 +9,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Clone repo
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Lint TypeScript
+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
password: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}
- name: Clone repo
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Install dependencies
run: bun install --frozen-lockfile
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
uses: docker/setup-buildx-action@v3
- name: Clone repo
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Install dependencies
run: bun install --frozen-lockfile
+11 -11
View File
@@ -85,18 +85,18 @@
"version": "0.1.0",
"dependencies": {
"@standard-schema/spec": "^1.1.0",
"effect-lens": "^2.0.1-beta.101",
"effect-lens": "^2.0.1-beta.102",
},
"devDependencies": {
"@effect/platform-browser": "4.0.0-beta.101",
"@effect/platform-browser": "4.0.0-beta.102",
"@testing-library/react": "^16.3.0",
"effect": "4.0.0-beta.101",
"effect": "4.0.0-beta.102",
"jsdom": "^26.1.0",
"vitest": "^3.2.4",
},
"peerDependencies": {
"@types/react": "^19.2.0",
"effect": "4.0.0-beta.101",
"effect": "4.0.0-beta.102",
"react": "^19.2.0",
},
},
@@ -104,9 +104,9 @@
"name": "@effect-view/example",
"version": "0.0.0",
"dependencies": {
"@effect/platform-browser": "4.0.0-beta.101",
"@effect/platform-browser": "4.0.0-beta.102",
"@radix-ui/themes": "^3.3.0",
"effect": "4.0.0-beta.101",
"effect": "4.0.0-beta.102",
"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.101", "", { "dependencies": { "multipasta": "^0.2.8" }, "peerDependencies": { "effect": "^4.0.0-beta.101" } }, "sha512-05//60oEzMyQyNjk8Ll1mML6gU2RvT9TTaomq5cGmNQu6Gzb6jlWfzn2/ZGzFtehhB9iOpqtjy0QkT5tDh9ElA=="],
"@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": ["effect@4.0.0-beta.101", "", { "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-HjowumlIo+orthn4jMlEJPuzIYPBV+uq/XiciHWhiedLsXQpWHdNJHO5d59BVDP5s1LPuvERcktwFqRXnJqnhA=="],
"@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/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.101", "", { "dependencies": { "multipasta": "^0.2.8" }, "peerDependencies": { "effect": "^4.0.0-beta.101" } }, "sha512-05//60oEzMyQyNjk8Ll1mML6gU2RvT9TTaomq5cGmNQu6Gzb6jlWfzn2/ZGzFtehhB9iOpqtjy0QkT5tDh9ElA=="],
"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": ["effect@4.0.0-beta.101", "", { "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-HjowumlIo+orthn4jMlEJPuzIYPBV+uq/XiciHWhiedLsXQpWHdNJHO5d59BVDP5s1LPuvERcktwFqRXnJqnhA=="],
"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-lens": ["effect-lens@2.0.1-beta.101", "", { "peerDependencies": { "effect": "4.0.0-beta.101" } }, "sha512-ME5JruBkF3AG34rhb9MlHb1uwEbW70hr6lFITf8tcHBTgVF5QXFCe8nWATraYZpxBHGqM2bOnRvoadmwh2yyvQ=="],
"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=="],
"esrecurse/estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
+14 -16
View File
@@ -54,11 +54,9 @@ valid decoded value should go:
## Create forms once
`MutationForm.service` and `LensForm.service` are Effect constructors, not
hooks. Create each root form once and keep it stable. For a component-owned
form, call the constructor inside `Component.useOnMount`; for a form shared by
multiple components, create it in an Effect service. Update the existing form's
Lenses rather than reconstructing the form during render.
`MutationForm.make` and `LensForm.make` construct forms. Pipe them through the
module's `run` operator to start validation or synchronization in the current
scope. Create each root form once and keep it stable.
## MutationForm: validate, then submit
@@ -73,7 +71,7 @@ import { Component, MutationForm, View } from "effect-view"
const CreateProfileView = Component.make("CreateProfile")(function* () {
const form = yield* Component.useOnMount(() =>
MutationForm.service({
MutationForm.make({
schema: ProfileSchema,
initialEncodedValue: {
displayName: "",
@@ -84,7 +82,7 @@ const CreateProfileView = Component.make("CreateProfile")(function* () {
Effect.log(
`Creating ${profile.displayName}, age ${profile.age}`,
),
}),
}).pipe(MutationForm.thenRun),
)
const [canCommit, isCommitting] = yield* View.useAll([
@@ -110,9 +108,9 @@ mutation receives the schema's decoded profile, so `profile.age` is a number.
Schema transformations happen before the mutation and schema issues prevent an
invalid draft from being submitted.
`MutationForm.service` also starts initial validation in the current scope. In
the example above, `Component.useOnMount` keeps that validation and the form's
mutation work tied to the component that owns them.
`MutationForm.thenRun` starts initial validation. In the example above,
`Component.useOnMount` keeps that validation and the form's mutation work tied
to the component that owns them.
## LensForm: validate, then synchronize
@@ -136,10 +134,10 @@ const EditProfileView = Component.make("EditProfile")(function* () {
}),
)
const form = yield* LensForm.service({
const form = yield* LensForm.make({
schema: ProfileSchema,
target: profile,
})
}).pipe(LensForm.thenRun)
return [form, profile] as const
}),
@@ -168,8 +166,8 @@ reaches the target. If another part of the application updates the target,
`LensForm` encodes that value back into the draft.
Pass `initialEncodedValue` only when the first draft should differ from the
encoded target. Otherwise `LensForm.service` obtains the initial draft by
encoding the target through the schema.
encoded target. Otherwise `LensForm.make` obtains the initial draft by encoding
the target through the schema.
## Focus into subforms
@@ -427,14 +425,14 @@ const AppointmentSchema = Schema.Struct({
const AppointmentView = Component.make("Appointment")(function* () {
const [form, startsAtField] = yield* Component.useOnMount(() =>
Effect.gen(function* () {
const form = yield* MutationForm.service({
const form = yield* MutationForm.make({
schema: AppointmentSchema,
initialEncodedValue: { startsAt: "" },
f: ([appointment]) =>
Effect.log(
`Saving ${DateTime.formatIso(appointment.startsAt)}`,
),
})
}).pipe(MutationForm.thenRun)
return [form, Form.focusObjectOn(form, "startsAt")] as const
}),
+12 -10
View File
@@ -57,11 +57,11 @@ client defaults. Window-focus refresh also requires the optional
## Create a reactive query
A query is driven by a `View` rather than by a value read during one React
render. Whenever that key changes, `Query.service` checks the cache and starts
render. Whenever that key changes, a running Query checks the cache and starts
the query effect when necessary.
`Query.service` is an Effect constructor, not a hook. Create each query instance
once and keep it stable. In a component, the usual place is
`Query.make` constructs the Query and `Query.thenRun` starts it in the current
scope. Create each query instance once and keep it stable. In a component, the usual place is
`Component.useOnMount`; a query shared by multiple components can instead be
owned by an Effect service. Change the existing query's reactive key rather
than reconstructing the query during render.
@@ -84,7 +84,7 @@ const PostView = Component.make("Post")(function* () {
yield* SubscriptionRef.make(["post", 1 as number] as const),
)
const query = yield* Query.service({
const query = yield* Query.make({
key,
staleTime: "1 minute",
f: ([, id]) =>
@@ -95,7 +95,7 @@ const PostView = Component.make("Post")(function* () {
Effect.andThen((response) => response.json),
Effect.andThen(Schema.decodeUnknownEffect(Post)),
),
})
}).pipe(Query.thenRun)
return [Lens.focusTupleAt(key, 1), query] as const
}),
@@ -124,7 +124,7 @@ Effect equality by default, so structurally equal Effect data types work well
as query keys. Supply `keyEquivalence` when the key needs different equality
semantics.
`Query.service` starts watching its key in the current scope. The
`Query.thenRun` starts watching its key in the current scope. The
`Component.useOnMount` call above keeps both the query instance and its query
function identity stable for the component's lifetime.
@@ -255,7 +255,8 @@ Refresh every five minutes, starting after five minutes:
```ts
import { Schedule } from "effect"
yield* query.pipe(
const query = yield* Query.make(options).pipe(
Query.thenRun,
Query.withScheduledRefresh(Schedule.spaced("5 minutes")),
)
```
@@ -263,7 +264,8 @@ yield* query.pipe(
Limit the number of refreshes:
```ts
yield* query.pipe(
const query = yield* Query.make(options).pipe(
Query.thenRun,
Query.withScheduledRefresh(
Schedule.spaced("5 minutes").pipe(
Schedule.upTo({ times: 3 }),
@@ -289,12 +291,12 @@ By default, the client enables refresh on browser window focus. Set it globally
or override it for one query:
```tsx
const query = yield* Query.service({
const query = yield* Query.make({
key,
f: loadPost,
staleTime: "10 seconds",
refreshOnWindowFocus: false,
})
}).pipe(Query.thenRun)
```
Window-focus refresh depends on the optional `@effect/platform-browser`
+5 -5
View File
@@ -1,7 +1,7 @@
{
"name": "effect-view",
"description": "Write React function components with Effect",
"version": "0.1.0",
"version": "0.1.1",
"type": "module",
"files": [
"./README.md",
@@ -97,19 +97,19 @@
"clean:modules": "rm -rf node_modules"
},
"devDependencies": {
"@effect/platform-browser": "4.0.0-beta.101",
"@effect/platform-browser": "4.0.0-beta.102",
"@testing-library/react": "^16.3.0",
"effect": "4.0.0-beta.101",
"effect": "4.0.0-beta.102",
"jsdom": "^26.1.0",
"vitest": "^3.2.4"
},
"peerDependencies": {
"@types/react": "^19.2.0",
"effect": "4.0.0-beta.101",
"effect": "4.0.0-beta.102",
"react": "^19.2.0"
},
"dependencies": {
"@standard-schema/spec": "^1.1.0",
"effect-lens": "^2.0.1-beta.101"
"effect-lens": "^2.0.1-beta.102"
}
}
+5 -10
View File
@@ -189,18 +189,13 @@ export const make = Effect.fnUntraced(function* <A, I = A, RD = never, RE = neve
)
})
export declare namespace service {
export interface Options<in out A, out I = A, out RD = never, out RE = never, out TER = never, out TEW = never, out TRR = never, out TRW = never>
extends make.Options<A, I, RD, RE, TER, TEW, TRR, TRW> {}
}
export const service = <A, I = A, RD = never, RE = never, TER = never, TEW = never, TRR = never, TRW = never>(
options: service.Options<A, I, RD, RE, TER, TEW, TRR, TRW>
export const thenRun = <A, I = A, RD = never, RE = never, TER = never, TEW = never, TRR = never, TRW = never, E = never, R = never>(
self: Effect.Effect<LensForm<A, I, RD, RE, TER, TEW, TRR, TRW>, E, R>,
): Effect.Effect<
LensForm<A, I, RD, RE, TER, TEW, TRR, TRW>,
Schema.SchemaError | TER,
Scope.Scope | RD | RE | TRR | TRW
E,
Scope.Scope | R
> => Effect.tap(
make(options),
self,
form => Effect.forkScoped(form.run),
)
+5 -10
View File
@@ -196,18 +196,13 @@ export const make = Effect.fnUntraced(function* <A, I = A, RD = never, RE = neve
)
})
export declare namespace service {
export interface Options<in out A, in out I = A, in out RD = never, in out RE = never, out MA = void, out ME = never, out MR = never>
extends make.Options<A, I, RD, RE, MA, ME, MR> {}
}
export const service = <A, I = A, RD = never, RE = never, MA = void, ME = never, MR = never>(
options: service.Options<A, I, RD, RE, MA, ME, MR>
export const thenRun = <A, I = A, RD = never, RE = never, MA = void, ME = never, MR = never, E = never, R = never>(
self: Effect.Effect<MutationForm<A, I, RD, RE, MA, ME, MR>, E, R>,
): Effect.Effect<
MutationForm<A, I, RD, RE, MA, ME, MR>,
never,
Scope.Scope | RD | RE | MR
E,
Scope.Scope | R
> => Effect.tap(
make(options),
self,
form => Effect.forkScoped(form.run),
)
+9 -9
View File
@@ -92,14 +92,14 @@ describe("Query", () => {
return calls
}),
staleTime: "0 millis",
})
yield* query.fetch([1])
const returnedQuery = yield* query.pipe(Query.withScheduledRefresh(
}).pipe(
Query.thenRun,
Query.withScheduledRefresh(
Schedule.spaced("1 second").pipe(
Schedule.upTo({ times: 1 }),
),
))
),
)
yield* TestClock.adjust("999 millis")
const beforeInterval = calls
@@ -108,7 +108,7 @@ describe("Query", () => {
const afterFirstInterval = calls
return {
returnsQuery: returnedQuery === query,
isQuery: Query.isQuery(query),
beforeInterval,
afterFirstInterval,
}
@@ -117,7 +117,7 @@ describe("Query", () => {
))
expect(result).toEqual({
returnsQuery: true,
isQuery: true,
beforeInterval: 1,
afterFirstInterval: 2,
})
@@ -180,14 +180,14 @@ describe("Query", () => {
const key = staticKey<readonly [number]>([1])
const effect = Effect.gen(function*() {
const query = yield* Query.service({
const query = yield* Query.make({
key,
f: ([id]: readonly [number]) => Effect.sync(() => {
calls += 1
return `value:${id}:${calls}`
}),
staleTime: "1 minute",
})
}).pipe(Query.thenRun)
const latestFinalState = yield* Effect.sleep("1 millis").pipe(
Effect.andThen(View.get(query.latestFinalState)),
+19 -21
View File
@@ -410,14 +410,10 @@ export const make = Effect.fnUntraced(function* <K, A, E = never, R = never>(
)
})
export const service = <K, A, E = never, R = never>(
options: make.Options<K, A, E, R>
): Effect.Effect<
Query<K, A, E, R>,
Cause.NoSuchElementError,
Scope.Scope | QueryClient.QueryClient | R
> => Effect.tap(
make(options),
export const thenRun = <K, A, E = never, R = never, E2 = never, R2 = never>(
self: Effect.Effect<Query<K, A, E, R>, E2, R2>,
): Effect.Effect<Query<K, A, E, R>, E2, Scope.Scope | R2> => Effect.tap(
self,
query => Effect.forkScoped(query.run),
)
@@ -426,14 +422,16 @@ export const service = <K, A, E = never, R = never>(
*
* @example Refresh every five minutes, starting after five minutes
* ```ts
* yield* query.pipe(
* yield* Query.make(options).pipe(
* Query.thenRun,
* Query.withScheduledRefresh(Schedule.spaced("5 minutes")),
* )
* ```
*
* @example Refresh at most three times
* ```ts
* yield* query.pipe(
* yield* Query.make(options).pipe(
* Query.thenRun,
* Query.withScheduledRefresh(
* Schedule.spaced("5 minutes").pipe(Schedule.upTo({ times: 3 })),
* ),
@@ -443,19 +441,19 @@ export const service = <K, A, E = never, R = never>(
export const withScheduledRefresh: {
<Output, Error, Env>(
schedule: Schedule.Schedule<Output, unknown, Error, Env>,
): <K, A, E, R>(
self: Query<K, A, E, R>,
) => Effect.Effect<Query<K, A, E, R>, never, Scope.Scope | Env>
<K, A, E, R, Output, Error, Env>(
self: Query<K, A, E, R>,
): <K, A, E, R, E2, R2>(
self: Effect.Effect<Query<K, A, E, R>, E2, R2>,
) => Effect.Effect<Query<K, A, E, R>, E2, Scope.Scope | Env | R2>
<K, A, E, R, E2, R2, Output, Error, Env>(
self: Effect.Effect<Query<K, A, E, R>, E2, R2>,
schedule: Schedule.Schedule<Output, unknown, Error, Env>,
): Effect.Effect<Query<K, A, E, R>, never, Scope.Scope | Env>
} = Function.dual(2, <K, A, E, R, Output, Error, Env>(
self: Query<K, A, E, R>,
): Effect.Effect<Query<K, A, E, R>, E2, Scope.Scope | Env | R2>
} = Function.dual(2, <K, A, E, R, E2, R2, Output, Error, Env>(
self: Effect.Effect<Query<K, A, E, R>, E2, R2>,
schedule: Schedule.Schedule<Output, unknown, Error, Env>,
) => Effect.schedule(self.refresh, schedule).pipe(
Effect.forkScoped,
Effect.as(self),
) => Effect.tap(
self,
query => Effect.forkScoped(Effect.schedule(query.refresh, schedule)),
))
+5 -9
View File
@@ -115,18 +115,14 @@ export const make = Effect.fnUntraced(function* (
)
})
export declare namespace service {
export interface Options extends make.Options {}
}
export const service = (
options?: service.Options
): Effect.Effect<QueryClientService, Cause.NoSuchElementError, Scope.Scope> => Effect.tap(
make(options),
export const thenRun = <E = never, R = never>(
self: Effect.Effect<QueryClientService, E, R>,
): Effect.Effect<QueryClientService, E, Scope.Scope | R> => Effect.tap(
self,
client => Effect.forkScoped(client.run),
)
export const layer = (options?: service.Options) => Layer.effect(QueryClient, service(options))
export const layer = (options?: make.Options) => Layer.effect(QueryClient, thenRun(make(options)))
export const QueryClientCacheKeyTypeId: unique symbol = Symbol.for("@effect-view/QueryClient/QueryClientCacheKey")
+3 -3
View File
@@ -27,15 +27,15 @@
"vite": "^8.0.16"
},
"dependencies": {
"@effect/platform-browser": "4.0.0-beta.101",
"@effect/platform-browser": "4.0.0-beta.102",
"@radix-ui/themes": "^3.3.0",
"effect": "4.0.0-beta.101",
"effect": "4.0.0-beta.102",
"effect-view": "workspace:*",
"react-icons": "^5.6.0"
},
"overrides": {
"@types/react": "^19.2.15",
"effect": "4.0.0-beta.101",
"effect": "4.0.0-beta.102",
"react": "^19.2.6"
}
}
+4 -2
View File
@@ -26,11 +26,13 @@ const RegisterRouteComponent = Component.make("RegisterRouteView")(function*() {
}))
const [form, emailField, passwordField] = yield* Component.useOnMount(() => Effect.gen(function*() {
const form = yield* MutationForm.service({
const form = yield* MutationForm.make({
schema: RegisterSchema,
initialEncodedValue: { email: "", password: "" },
f: ([value]) => Effect.log(`Registered ${value.email}`),
})
}).pipe(
MutationForm.thenRun,
)
const emailField = Form.focusObjectOn(form, "email")
const passwordField = Form.focusObjectOn(form, "password")
+4 -2
View File
@@ -65,14 +65,16 @@ const UserProfileEditorView = Component.make("UserProfileEditorView")(function*(
Stream.runForEach(Lens.changes(appState.lens), Console.log)
)
const form = yield* LensForm.service({
const form = yield* LensForm.make({
schema: UserProfileSchema,
target: Lens.focusObjectOn(appState.lens, "currentUser"),
initialEncodedValue: {
email: "",
password: "",
},
})
}).pipe(
LensForm.thenRun,
)
const emailField = Form.focusObjectOn(form, "email")
const passwordField = Form.focusObjectOn(form, "password")
+8 -6
View File
@@ -36,16 +36,18 @@ const QueryRouteComponent = Component.make("QueryRouteView")(function*() {
const keyLens = Lens.fromSubscriptionRef(yield* SubscriptionRef.make(["post", 1 as number] as const))
const idLens = Lens.focusTupleAt(keyLens, 1)
const query = yield* Query.service({
const query = yield* Query.make({
key: keyLens,
f: ([, id]) => HttpClient.HttpClient.pipe(
Effect.tap(Effect.sleep("1 second")),
Effect.andThen(client => client.get(`https://jsonplaceholder.typicode.com/posts/${ id }`)),
Effect.andThen(response => response.json),
Effect.andThen(Schema.decodeUnknownEffect(Post)),
Effect.tap(Effect.sleep("500 millis")),
Effect.flatMap(client => client.get(`https://jsonplaceholder.typicode.com/posts/${ id }`)),
Effect.flatMap(response => response.json),
Effect.flatMap(Schema.decodeUnknownEffect(Post)),
),
staleTime: "10 seconds",
})
}).pipe(
Query.thenRun,
)
const mutation = yield* Mutation.make({
f: ([id]: [id: number]) => HttpClient.HttpClient.pipe(