## Summary - Add comprehensive AI-oriented documentation for effect-view components, state, async rendering, queries, mutations, forms, streams, and runtime setup. - Fix `Mutation.mutate` so each invocation consistently uses its own key instead of reusing the previous mutation key. - Improve Vite Fast Refresh instrumentation for: - User-defined component wrappers. - Data-first and pipeline-based `withContext`/`withRuntime` entrypoints. - Nested function handling. - Stable hook signatures that preserve state for non-hook-related edits. - Add regression tests for mutation keys and refresh behavior. - Bump `effect-view` to `0.1.5` and `@effect-view/vite-plugin` to `0.0.2`. ## Testing - `bun run --cwd packages/effect-view test -- src/Mutation.test.ts` - 4 tests passed - `bun run --cwd packages/vite-plugin test -- src/plugin.test.ts` - 11 tests passed The full test suite was not run. --------- Co-authored-by: Julien Valverdé <julien.valverde@mailo.com> Reviewed-on: #76
5.5 KiB
Component
Defines a React function component as an Effect program. A Component is a description, not yet a React component — cross into React with Component.withContext or .use.
Define
import { Effect } from "effect"
import { Component } from "effect-view"
export const HelloView = Component.make("HelloView")(function* (props: { readonly name: string }) {
const message = yield* Effect.succeed(`Hello, ${props.name}`)
return <h1>{message}</h1>
})
Component.make(spanName?)(generatorBody, ...pipeArgs)— same overloads asEffect.fn/Effect.gen: a generator body, or a body plus(_, props) => nextpipeline steps. Passing aspanNamewraps the body in a tracing span and setsdisplayName.Component.makeUntracedis identical but skips the automatic span (still setsdisplayNamefrom the name argument).- The component's props type, return type, error channel, and required services (
R) are all inferred from the generator body.
Cross into React
export const Hello = HelloView.pipe(Component.withContext(runtime.context))
// <Hello name="Effect" />
Component.withContext(context) reads the Effect context supplied by the matching ReactRuntime.Provider and turns the component into a plain React.FC. Apply it only at boundaries where plain React (a router, a third-party lib, an app root) needs a function component — never between two effect-view components.
Compose inside effect-view
const Hello = yield* HelloView.use
return <Hello name="Effect" />
component.use is an Effect<F, never, Exclude<R, Scope.Scope>> that binds the child to the current Effect context/scope and returns a stable function-component reference. Yield it from a parent component body; do not call withContext here.
Lifecycle hooks
Hooks are plain React hooks under the hood: call them unconditionally, at the top level, in the same order every render — never in branches, loops, callbacks, or after a suspend point.
Every rendered component instance gets a root Scope.Scope, created on mount and closed on unmount, provided to the whole body. Effect.addFinalizer, Effect.acquireRelease, Effect.forkScoped used directly in the body run against this scope.
| Hook | Purpose | Scope | Closes |
|---|---|---|---|
| body | produce rendered output | component root scope | unmount |
useOnMount(() => effect) |
compute + cache once | component root scope | unmount |
useOnChange(() => effect, deps) |
recompute on deps change | new scope per dep set | deps change / unmount |
useReactEffect(() => effect, deps?) |
post-commit side effect (Effect.useEffect analog) |
new scope | deps change / unmount |
useReactLayoutEffect(() => effect, deps?) |
pre-paint side effect | new scope | deps change / unmount |
useLayer(layer, options?) |
build + provide a Layer, returns its Context |
new scope tied to layer identity | layer ref changes / unmount |
const state = yield* Component.useOnMount(() =>
Effect.gen(function* () {
yield* Effect.addFinalizer(() => Effect.log("disposed"))
return Lens.fromSubscriptionRef(yield* SubscriptionRef.make(0))
}),
)
- Setup passed to
useOnMount/useOnChange/useLayermust complete synchronously in a regular component (it runs during render). Wrap the component withAsync.asyncto allow suspending setup. useReactEffect/useReactLayoutEffectsetup must also start synchronously but mayEffect.forkScopedasync work into the hook's own scope.useRunSync/useRunPromise/useCallbackSync/useCallbackPromisedo not create a new scope; they capture the component root scope (and any extra services you request) by default.
Run Effects from event handlers
const runPromise = yield* Component.useRunPromise() // or useRunPromise<Scope.Scope | SomeService>()
<button onClick={() => void runPromise(saveUser(user))}>Save</button>
useRunSync<R>()— only for Effects guaranteed to complete synchronously.useRunPromise<R>()— for Effects that may suspend/sleep/fetch.useCallbackSync(f, deps)/useCallbackPromise(f, deps)— memoized variants (same deps semantics asReact.useCallback) for passing stable callbacks to children.- Both runners provide
Scope.Scopeautomatically; add extra services with an explicit type argument (useRunPromise<Scope.Scope | UserRepository>()).
Provide services
Static layer, one instance per mounted component, disposed on unmount:
const GreetingViewLive = GreetingView.pipe(Component.provide(GreetingService.layer))
Layer built from render-time state (props/context), provided to children explicitly:
const layer = React.useMemo(() => Layer.succeed(GreetingService, {...}), [props.greeting])
const context = yield* Component.useLayer(layer)
const Greeting = yield* Effect.provide(GreetingView.use, context)
return <Greeting name="Effect" />
Keep layer references stable (module scope or React.useMemo) — a new layer object triggers rebuild and finalizer cleanup. Async layer construction requires Async.async on the owning component.
Common pitfalls
- Never yield an asynchronous Effect from a regular component body — use
Async.async,Query, aMutationcallback, or a scoped fiber forked from a post-commit hook instead. Component.withContextneeds a matchingReactRuntime.Providerabove it in the tree.- Prefer
useRunPromiseoveruseRunSyncfor event handlers that may be async. - Regular React hooks, refs, context, and state work normally inside a component body alongside
yield*.