diff --git a/packages/docs/docs/forms.md b/packages/docs/docs/forms.md index 115300b..f0cc609 100644 --- a/packages/docs/docs/forms.md +++ b/packages/docs/docs/forms.md @@ -52,6 +52,14 @@ valid decoded value should go: | `MutationForm` | Yes | It is passed to a mutation when `submit` runs. | | `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 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 invalid draft from being submitted. -`MutationForm.service` also starts initial validation in the current scope. Use -it inside `Component.useOnMount`, a scoped service, or another Effect scope so -its validation and mutation work is cleaned up with its owner. +`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. ## LensForm: validate, then synchronize diff --git a/packages/docs/docs/getting-started.md b/packages/docs/docs/getting-started.md index dbc8c2d..15cae5d 100644 --- a/packages/docs/docs/getting-started.md +++ b/packages/docs/docs/getting-started.md @@ -295,24 +295,25 @@ const ResourceView = Component.make("Resource")(function* () { with the context already available in the component body, so the resource above is owned by the component's root scope. -`useOnChange`, `useReactEffect`, and `useReactLayoutEffect` each create a scope -that can be replaced without closing the component root scope. +`useOnChange`, `useReactEffect`, and `useReactLayoutEffect` each manage their +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: | 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 | -| `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 | | `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 | -| `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 -not create lifecycle scopes either. They capture the component context, so an -Effect invoked through them sees the component root scope unless it explicitly -provides another one. +`useRunSync` and `useRunPromise` do not create a new scope. The runner they +return includes the component root scope in its context by default. +`useCallbackSync` and `useCallbackPromise` similarly capture the component +context required by their Effect. ### 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 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 callback variants to preserve its identity: diff --git a/packages/docs/docs/mutation.md b/packages/docs/docs/mutation.md index 6d4589a..57aee20 100644 --- a/packages/docs/docs/mutation.md +++ b/packages/docs/docs/mutation.md @@ -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 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 import { Effect } from "effect" import { AsyncResult } from "effect/unstable/reactivity" diff --git a/packages/docs/docs/query.md b/packages/docs/docs/query.md index a3b1403..87da71e 100644 --- a/packages/docs/docs/query.md +++ b/packages/docs/docs/query.md @@ -35,7 +35,6 @@ application runtime once, alongside the services used by your query effects: ```tsx title="src/runtime.ts" import { Layer } from "effect" -import { FetchHttpClient } from "effect/unstable/http" import { QueryClient, ReactRuntime } from "effect-view" const AppLive = Layer.empty.pipe( @@ -44,7 +43,6 @@ const AppLive = Layer.empty.pipe( defaultRefreshOnWindowFocus: true, cacheGcTime: "5 minutes", })), - Layer.provideMerge(FetchHttpClient.layer), ) 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 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 import { Effect, Schema, SubscriptionRef } from "effect" 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 semantics. -`Query.service` creates the query and starts watching its key in the current -scope. Creating it in `Component.useOnMount` keeps one query instance—and one -stable query function identity—for the component's lifetime. +`Query.service` 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. ## Render AsyncResult