Add Effect v4 support, Fast Refresh tooling, and revamped docs #56

Merged
Thilawyn merged 133 commits from next into master 2026-07-26 02:32:59 +02:00
Showing only changes of commit 1651e96696 - Show all commits
+22 -52
View File
@@ -301,33 +301,19 @@ 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.
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.
`useOnChange`, `useReactEffect`, and `useReactLayoutEffect` each create a scope
that can be replaced without closing the component root scope.
The main lifecycle choices are:
| Hook | Scope seen by setup | Scope closes when |
| --- | --- | --- |
| 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 |
| 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 |
| `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 |
`useRunSync`, `useRunPromise`, `useCallbackSync`, and `useCallbackPromise` do
not create lifecycle scopes either. They capture the component context, so an
@@ -336,11 +322,9 @@ provides another one.
### useOnMount
`Component.useOnMount` computes a value during the component's initial render
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:
`Component.useOnMount` computes a value during the initial render and caches it
for later renders. It uses the component root scope, so its resources live
until the component unmounts.
```tsx title="src/LocalStateView.tsx"
import { Effect, SubscriptionRef } from "effect"
@@ -372,10 +356,9 @@ asynchronous and will suspend the component.
### useOnChange
`Component.useOnChange` has the same render-time constraint, but owns a scope
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:
`Component.useOnChange` computes and caches a value, then recomputes it when its
dependencies change. Each dependency set gets its own scope, which closes when
the dependencies change or the component unmounts.
```tsx
const label = yield* Component.useOnChange(
@@ -396,10 +379,12 @@ read by the setup Effect.
### useReactEffect and useReactLayoutEffect
Use `Component.useReactEffect` for work that should start only after React has
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:
`Component.useReactEffect` runs side effects after React commits, while
`Component.useReactLayoutEffect` runs before the browser paints. Each hook owns
a scope that closes when its dependencies change or the component unmounts.
Setup must complete synchronously, but it can fork asynchronous work into the
hook scope:
```tsx
yield* Component.useReactEffect(
@@ -408,21 +393,6 @@ yield* Component.useReactEffect(
)
```
When dependencies change or the component unmounts, the hook closes its scope
and runs registered finalizers. Use `Component.useReactLayoutEffect` for the
same ownership model when setup must happen before the browser paints, such as
DOM measurement.
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
more than once; production retains the normal component lifetime semantics.