Add explicit lifecycle pipelines and scheduled query refreshes (#68)
## 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
This commit was merged in pull request #68.
This commit is contained in:
+14
-16
@@ -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
|
||||
}),
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -248,6 +248,35 @@ workflows that need to wait for the final success or failure state.
|
||||
Invalidating does not itself refetch. Follow it with `refreshView`, change the
|
||||
key, or allow a later fetch to repopulate the cache.
|
||||
|
||||
### Refresh on an interval
|
||||
|
||||
Refresh every five minutes, starting after five minutes:
|
||||
|
||||
```ts
|
||||
import { Schedule } from "effect"
|
||||
|
||||
const query = yield* Query.make(options).pipe(
|
||||
Query.thenRun,
|
||||
Query.withScheduledRefresh(Schedule.spaced("5 minutes")),
|
||||
)
|
||||
```
|
||||
|
||||
Limit the number of refreshes:
|
||||
|
||||
```ts
|
||||
const query = yield* Query.make(options).pipe(
|
||||
Query.thenRun,
|
||||
Query.withScheduledRefresh(
|
||||
Schedule.spaced("5 minutes").pipe(
|
||||
Schedule.upTo({ times: 3 }),
|
||||
),
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
The refresh fiber stops with the surrounding scope. The Effect returns the
|
||||
original query. Cache and `staleTime` rules still apply.
|
||||
|
||||
## Staleness and cache lifetime
|
||||
|
||||
`staleTime` controls how long a successful result can satisfy a fetch without
|
||||
@@ -262,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`
|
||||
|
||||
Reference in New Issue
Block a user