From 22933b2a09222dba1e1b9e2e8854cbb037dfd5f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Wed, 22 Jul 2026 03:34:27 +0200 Subject: [PATCH] Update docs --- packages/docs/docs/getting-started.md | 94 ++++++++++++++++++++++----- 1 file changed, 78 insertions(+), 16 deletions(-) diff --git a/packages/docs/docs/getting-started.md b/packages/docs/docs/getting-started.md index ee87910..f2f5add 100644 --- a/packages/docs/docs/getting-started.md +++ b/packages/docs/docs/getting-started.md @@ -271,20 +271,76 @@ Effect View hooks are still React hooks internally. Call them unconditionally at the top level of the component body, in a consistent order, and never inside branches, loops, event handlers, or nested callbacks. +### The component root scope + +Every rendered Effect View component gets a root `Scope.Scope`. Effect View +creates it when the component instance is rendered, provides it to the entire +component body, and closes it when that React component unmounts. + +Effects yielded directly from the body therefore see the root scope. This also +means that `Effect.addFinalizer`, `Effect.acquireRelease`, and +`Effect.forkScoped` can be used naturally inside component setup: + +```tsx +import { Effect } from "effect" +import { Component } from "effect-view" + +const ResourceView = Component.make("Resource")(function* () { + const resource = yield* Component.useOnMount(() => + Effect.acquireRelease( + openResource, + (resource) => closeResource(resource), + ), + ) + + return

{resource.name}

+}) +``` + +`useOnMount` does **not** create or provide another scope. It runs its Effect +with the context already available in the component body, so the resource above +is owned by the component's root scope. + +Other lifecycle hooks need a cleanup boundary narrower than the whole +component, so they create a new scope and provide it to their setup Effect: + +```text +React component instance +└── Component root scope + ├── Component body uses the root scope + ├── useOnMount setup uses the root scope + ├── useOnChange setup for dependencies A gets its own scope + ├── useReactEffect setup gets its own scope + └── useReactLayoutEffect setup gets its own scope +``` + +The narrower scopes are managed by their hooks rather than being implicit +Effect child scopes. React unmounting closes all of them; dependency changes +can close and replace a hook scope without closing the component root scope. + The main lifecycle choices are: -| Need | Hook | When setup runs | +| Hook | Scope seen by setup | Scope closes when | | --- | --- | --- | -| Compute and cache a value for this component instance | `useOnMount` | During the initial render | -| Recompute a value when dependencies change | `useOnChange` | During render when dependencies change | -| Run a post-commit side effect | `useReactEffect` | After React commits | -| Measure or update layout before paint | `useReactLayoutEffect` | After DOM mutation, before paint | +| Component body | Component root scope | The component unmounts | +| `useOnMount` | The same component root scope | The component unmounts | +| `useOnChange` | A new dependency scope | Dependencies change or the component unmounts | +| `useReactEffect` | A new effect scope | Dependencies change or the component unmounts | +| `useReactLayoutEffect` | A new layout-effect scope | Dependencies change or the component unmounts | +| `useLayer` | A dependency scope created through `useOnChange` | 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. ### useOnMount `Component.useOnMount` computes a value during the component's initial render -and returns the cached value on later renders. It receives the component scope, -so acquired resources and forked fibers can be tied to the component lifetime: +and returns the cached value on later renders. It does not install a scope of +its own: its Effect inherits the root scope provided to the component body. +Acquired resources and forked fibers are therefore tied to the full component +lifetime: ```tsx title="src/LocalStateView.tsx" import { Effect, SubscriptionRef } from "effect" @@ -317,8 +373,9 @@ asynchronous and will suspend the component. ### useOnChange `Component.useOnChange` has the same render-time constraint, but owns a scope -for one dependency window. When a dependency changes, the previous scope is -closed and a new value is computed: +for one dependency window. It creates that scope and provides it to the setup +Effect instead of the component root scope. When a dependency changes, the +previous dependency scope is closed and a new value is computed: ```tsx const label = yield* Component.useOnChange( @@ -340,8 +397,9 @@ read by the setup Effect. ### useReactEffect and useReactLayoutEffect Use `Component.useReactEffect` for work that should start only after React has -committed the render. Setup must complete synchronously, but it can fork -asynchronous work into the provided scope: +committed the render. The hook creates a fresh scope, provides it to the setup +Effect, and closes it through React effect cleanup. Setup must complete +synchronously, but it can fork asynchronous work into that scope: ```tsx yield* Component.useReactEffect( @@ -355,11 +413,15 @@ and runs registered finalizers. Use `Component.useReactLayoutEffect` for the same ownership model when setup must happen before the browser paints, such as DOM measurement. -Component and dependency scopes debounce finalizer execution by 100 -milliseconds by default. This reduces unnecessary teardown during rapid -unmount/remount cycles, including development behavior. Configure -`finalizerExecutionDebounce` through `Component.withOptions` or the relevant -hook options when cleanup timing must differ. +The component root scope and scopes created by `useOnChange` debounce finalizer +execution by 100 milliseconds by default. This reduces unnecessary teardown +during rapid unmount/remount cycles, including development behavior. Configure +the root through `Component.withOptions({ finalizerExecutionDebounce: ... })`, +or pass `finalizerExecutionDebounce` in the `useOnChange` options. + +`useReactEffect` and `useReactLayoutEffect` instead close their scopes directly +from React cleanup. Their `finalizerExecutionMode` option controls whether that +close runs synchronously or is forked; it defaults to `"fork"`. React Strict Mode may intentionally repeat development-only render and effect setup. Initializers and resource acquisition should therefore be safe to run