Finalize the Effect View rename and refresh docs, examples, and tooling #59

Merged
Thilawyn merged 12 commits from next into master 2026-07-27 04:11:42 +02:00
4 changed files with 49 additions and 16 deletions
Showing only changes of commit 74cacc8d61 - Show all commits
+11 -3
View File
@@ -52,6 +52,14 @@ valid decoded value should go:
| `MutationForm` | Yes | It is passed to a mutation when `submit` runs. | | `MutationForm` | Yes | It is passed to a mutation when `submit` runs. |
| `LensForm` | Yes | It is written automatically to a target `Lens`. | | `LensForm` | Yes | It is written automatically to a target `Lens`. |
## 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: validate, then submit ## MutationForm: validate, then submit
Use `MutationForm` for registration, checkout, search, and other workflows with Use `MutationForm` for registration, checkout, search, and other workflows with
@@ -102,9 +110,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 Schema transformations happen before the mutation and schema issues prevent an
invalid draft from being submitted. invalid draft from being submitted.
`MutationForm.service` also starts initial validation in the current scope. Use `MutationForm.service` also starts initial validation in the current scope. In
it inside `Component.useOnMount`, a scoped service, or another Effect scope so the example above, `Component.useOnMount` keeps that validation and the form's
its validation and mutation work is cleaned up with its owner. mutation work tied to the component that owns them.
## LensForm: validate, then synchronize ## LensForm: validate, then synchronize
+24 -8
View File
@@ -295,24 +295,25 @@ const ResourceView = Component.make("Resource")(function* () {
with the context already available in the component body, so the resource above with the context already available in the component body, so the resource above
is owned by the component's root scope. is owned by the component's root scope.
`useOnChange`, `useReactEffect`, and `useReactLayoutEffect` each create a scope `useOnChange`, `useReactEffect`, and `useReactLayoutEffect` each manage their
that can be replaced without closing the component root scope. own resources. When their dependencies change, those resources are cleaned up
and recreated while the rest of the component remains active.
The main lifecycle choices are: The main lifecycle choices are:
| Hook | What it does | Scope seen by setup | Scope closes when | | Hook | What it does | Scope seen by setup | Scope closes when |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| Component body | Produces the component's rendered value | Component root scope | The component unmounts | | Component body | Produces the component's rendered value | Component root scope | The component unmounts |
| `useOnMount` | Computes and caches a value for the component instance | The same component root scope | The component unmounts | | `useOnMount` | Computes and caches a value for the component instance | Component root scope | The component unmounts |
| `useOnChange` | Recomputes a cached value when dependencies change | A new dependency scope | Dependencies change or the component unmounts | | `useOnChange` | Recomputes a cached value when dependencies change | A new dependency scope | Dependencies change or the component unmounts |
| `useReactEffect` | Runs a post-commit side effect | A new effect scope | Dependencies change or the component unmounts | | `useReactEffect` | Runs a post-commit side effect | A new effect scope | Dependencies change or the component unmounts |
| `useReactLayoutEffect` | Runs a layout effect before the browser paints | A new layout-effect scope | Dependencies change or the component unmounts | | `useReactLayoutEffect` | Runs a layout effect before the browser paints | A new layout-effect scope | Dependencies change or the component unmounts |
| `useLayer` | Builds and provides a layer context | A dependency scope created through `useOnChange` | The layer reference changes or the component unmounts | | `useLayer` | Builds and provides a layer context | A new scope tied to the current layer reference | The layer reference changes or the component unmounts |
`useRunSync`, `useRunPromise`, `useCallbackSync`, and `useCallbackPromise` do `useRunSync` and `useRunPromise` do not create a new scope. The runner they
not create lifecycle scopes either. They capture the component context, so an return includes the component root scope in its context by default.
Effect invoked through them sees the component root scope unless it explicitly `useCallbackSync` and `useCallbackPromise` similarly capture the component
provides another one. context required by their Effect.
### useOnMount ### useOnMount
@@ -410,6 +411,21 @@ Use `Component.useRunSync` only for Effects known to complete synchronously.
Use `Component.useRunPromise` for Effects that may suspend, sleep, fetch, or Use `Component.useRunPromise` for Effects that may suspend, sleep, fetch, or
otherwise continue asynchronously. otherwise continue asynchronously.
Both runners provide `Scope.Scope` by default, so they can run scoped Effects
without a type argument. When an Effect also requires application services,
declare the complete runner context explicitly:
```tsx
import { Scope } from "effect"
const runPromise = yield* Component.useRunPromise<
Scope.Scope | UserRepository
>()
```
The resulting runner accepts Effects that require the component scope,
`UserRepository`, or both.
When a callback is passed to a memoized child or used as a dependency, use the When a callback is passed to a memoized child or used as a dependency, use the
callback variants to preserve its identity: callback variants to preserve its identity:
+5
View File
@@ -30,6 +30,11 @@ Create a mutation in a component scope with `Mutation.make`. Its function can
be any Effect, including one that requires services from the application be any Effect, including one that requires services from the application
runtime: runtime:
`Mutation.make` is an Effect constructor, not a hook. Create each mutation
instance once and keep it stable instead of calling `Mutation.make` again on
every render. Use `Component.useOnMount` for a component-owned mutation or
create it in an Effect service when it should be shared.
```tsx ```tsx
import { Effect } from "effect" import { Effect } from "effect"
import { AsyncResult } from "effect/unstable/reactivity" import { AsyncResult } from "effect/unstable/reactivity"
+9 -5
View File
@@ -35,7 +35,6 @@ application runtime once, alongside the services used by your query effects:
```tsx title="src/runtime.ts" ```tsx title="src/runtime.ts"
import { Layer } from "effect" import { Layer } from "effect"
import { FetchHttpClient } from "effect/unstable/http"
import { QueryClient, ReactRuntime } from "effect-view" import { QueryClient, ReactRuntime } from "effect-view"
const AppLive = Layer.empty.pipe( const AppLive = Layer.empty.pipe(
@@ -44,7 +43,6 @@ const AppLive = Layer.empty.pipe(
defaultRefreshOnWindowFocus: true, defaultRefreshOnWindowFocus: true,
cacheGcTime: "5 minutes", cacheGcTime: "5 minutes",
})), })),
Layer.provideMerge(FetchHttpClient.layer),
) )
export const runtime = ReactRuntime.make(AppLive) export const runtime = ReactRuntime.make(AppLive)
@@ -60,6 +58,12 @@ 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, `Query.service` checks the cache and starts
the query effect when necessary. 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
`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.
```tsx ```tsx
import { Effect, Schema, SubscriptionRef } from "effect" import { Effect, Schema, SubscriptionRef } from "effect"
import { HttpClient } from "effect/unstable/http" import { HttpClient } from "effect/unstable/http"
@@ -118,9 +122,9 @@ Effect equality by default, so structurally equal Effect data types work well
as query keys. Supply `keyEquivalence` when the key needs different equality as query keys. Supply `keyEquivalence` when the key needs different equality
semantics. semantics.
`Query.service` creates the query and starts watching its key in the current `Query.service` starts watching its key in the current scope. The
scope. Creating it in `Component.useOnMount` keeps one query instanceand one `Component.useOnMount` call above keeps both the query instance and its query
stable query function identityfor the component's lifetime. function identity stable for the component's lifetime.
## Render AsyncResult ## Render AsyncResult