diff --git a/packages/docs/docs/getting-started.md b/packages/docs/docs/getting-started.md
index e941385..ee87910 100644
--- a/packages/docs/docs/getting-started.md
+++ b/packages/docs/docs/getting-started.md
@@ -5,50 +5,40 @@ title: Getting Started
# Getting Started
-`effect-view` lets React components be written as Effect programs. Inside a
-component body you can yield services, run Effects, subscribe to Effect-powered
-state, and still export a normal React function component at the edge of your
-app.
+`effect-view` lets a React function component be described as an Effect
+program. Inside a component you can yield services, create scoped resources,
+subscribe to Effect-powered state, and turn Effects into React callbacks. At a
+React boundary, that description becomes a normal function component.
+
+The core model has four pieces:
+
+1. `ReactRuntime` builds the Effect services available to the UI.
+2. `Component.make` defines an Effect View component.
+3. `Component.withContext` converts an Effect View component into a normal
+ React component at an application or router boundary.
+4. Effect View children are composed through their `.use` Effect.
## Install
-Install `effect-view` alongside Effect 4 and React 19.2 or newer:
+For a web application, install Effect View with Effect 4 and React 19.2 or
+newer:
```bash npm2yarn
-npm install effect-view effect react
+npm install effect-view effect react react-dom
```
-```bash npm2yarn
-npm install --save-dev @types/react
-```
-
-`effect-view` is not opinionated about the React platform. Use it with web,
-native, custom renderers, or any environment where React components can run.
-Then install the platform-specific React packages for your target.
-
-For web apps, install React DOM:
```bash npm2yarn
-npm install react-dom
-```
-```bash npm2yarn
-npm install --save-dev @types/react-dom
+npm install --save-dev @types/react @types/react-dom
```
-## Create A Runtime
+Effect View is not tied to React DOM. For React Native or another renderer,
+install that renderer instead of `react-dom` and keep the rest of the setup the
+same.
-An Effect View app needs an Effect runtime. Build one from the services your UI
-needs, then share it with React through `ReactRuntime.Provider`.
+## Create the runtime
-For an empty app, `Layer.empty` is enough:
-
-```tsx title="src/runtime.ts"
-import { Layer } from "effect"
-import { ReactRuntime } from "effect-view"
-
-export const runtime = ReactRuntime.make(Layer.empty)
-```
-
-As your app grows, add services to the layer:
+`ReactRuntime` owns a managed Effect runtime. Define it at module scope from the
+layers needed by the UI:
```tsx title="src/runtime.ts"
import { Layer } from "effect"
@@ -62,9 +52,23 @@ const AppLive = Layer.empty.pipe(
export const runtime = ReactRuntime.make(AppLive)
```
-## Provide The Runtime
+Use `Layer.empty` by itself when the application does not need any additional
+services yet:
-At the React root, wrap your app with `ReactRuntime.Provider`:
+```tsx
+export const runtime = ReactRuntime.make(Layer.empty)
+```
+
+Keep the runtime stable. Creating it during a React render would create new
+managed resources and a new React context on every render.
+
+## Provide the runtime
+
+Place `ReactRuntime.Provider` above every Effect View entrypoint. The provider
+builds the runtime layer, makes its Effect context available through React, and
+disposes the managed runtime when the provider unmounts.
+
+Runtime construction can suspend, so give the provider a fallback:
```tsx title="src/main.tsx"
import { StrictMode } from "react"
@@ -75,47 +79,52 @@ import { runtime } from "./runtime"
createRoot(document.getElementById("root")!).render(
-
+ Starting application...
}
+ >
,
)
```
-`ReactRuntime.Provider` also works with routers. Keep it above your router
-provider so route components can use the same runtime context.
+With a router, keep the provider above the router provider:
-## Write Your First Component
+```tsx
+Starting...}>
+
+
+```
-Use `Component.make` when you want automatic tracing spans, or
-`Component.makeUntraced` when you only want the component behavior. This creates
-an Effect View component, not a plain React component yet.
+If runtime layer construction can fail, place an appropriate React error
+boundary above `ReactRuntime.Provider` as well.
-The Effect run during render must be synchronous. Effect View follows React's
-render model, so component bodies should produce JSX without waiting on async
-work. Use lifecycle hooks, callbacks, queries, or other Effect View helpers for
-async work that happens outside render.
+## Write your first component
+
+`Component.make` defines a component body using the same generator style as
+`Effect.gen` and `Effect.fn`. Providing a name creates a tracing span and also
+sets the React DevTools display name:
```tsx title="src/HelloView.tsx"
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}`)
+export const HelloView = Component.make("HelloView")(
+ function* (props: { readonly name: string }) {
+ const message = yield* Effect.succeed(`Hello, ${props.name}`)
- return
{message}
-})
+ return
{message}
+ },
+)
```
-At this point `HelloView` can yield Effects, services, and scoped lifecycle
-work, but React cannot render it directly.
+Use `Component.makeUntraced("HelloView")` when you want the display name but do
+not want an automatic tracing span.
-## Apply A Runtime
-
-Use `Component.withContext` at the boundary where an Effect View component needs
-to become a regular React function component.
+`HelloView` is an Effect View component description, not yet a normal React
+component. Convert it at the point where plain React, a router, or a third-party
+library needs a function component:
```tsx title="src/Hello.tsx"
import { Component } from "effect-view"
@@ -127,7 +136,7 @@ export const Hello = HelloView.pipe(
)
```
-`Hello` is now a normal React component:
+It can now be rendered by ordinary React:
```tsx title="src/App.tsx"
import { Hello } from "./Hello"
@@ -137,11 +146,14 @@ export function App() {
}
```
-## Use Effect View Components Together
+`Component.withContext` reads the context populated by the matching
+`ReactRuntime.Provider`. It does not build or provide the runtime by itself.
-Inside an Effect View component, other Effect View components are available through
-their `.use` effect. Yield `.use` to get a React component for the current
-runtime context, then render it like JSX.
+## Compose Effect View components
+
+Inside another Effect View component, yield a child's `.use` Effect. This binds
+the child to the current Effect context and returns a component that can be used
+in JSX:
```tsx title="src/GreetingCardView.tsx"
import { Component } from "effect-view"
@@ -154,173 +166,243 @@ export const GreetingCardView = Component.make("GreetingCard")(
return (
-
This component is still running inside Effect View.
+
Both components use the same Effect context.
)
},
)
```
-Use `Component.withContext` only when you leave the Effect View tree and need a
-normal React component again:
+Only apply `Component.withContext` when crossing from Effect View into plain
+React. Applying it to every nested component is unnecessary and makes it harder
+to provide local services to a subtree.
-```tsx title="src/GreetingCard.tsx"
-import { Component } from "effect-view"
-import { GreetingCardView } from "./GreetingCardView"
-import { runtime } from "./runtime"
+## Synchronous and asynchronous components
-export const GreetingCard = GreetingCardView.pipe(
- Component.withContext(runtime.context),
-)
-```
+A regular Effect View component runs its body during React render. That Effect
+must complete synchronously: yielding a service, reading synchronous state, or
+creating a scoped object is fine; sleeping, fetching, or awaiting a promise is
+not.
-## Component Lifecycle
+Use `Async.async` when render genuinely depends on an asynchronous Effect. The
+component then suspends and accepts React Suspense props such as `fallback`:
-Every Effect View component instance exposes an Effect `Scope`. Effects that need
-`Scope.Scope` can use that component scope to register finalizers, fork scoped
-work, or acquire scoped resources.
+```tsx title="src/UserView.tsx"
+import { Async, Component, Memoized } from "effect-view"
+import { loadUser } from "./api"
-The component scope is created when React mounts the component and closes when
-React unmounts it. When the scope closes, Effect runs the finalizers registered
-inside that scope.
-
-Finalizers are forked when the scope closes, so cleanup logic can run
-asynchronous Effects even though the component body itself must stay
-synchronous.
-
-```tsx title="src/MountedMessageView.tsx"
-import { Console, Effect } from "effect"
-import { Component } from "effect-view"
-
-export const MountedMessageView = Component.make("MountedMessage")(
- function* () {
- const message = yield* Component.useOnMount(() =>
- Effect.gen(function* () {
- yield* Console.log("MountedMessage mounted")
- yield* Effect.addFinalizer(() =>
- Console.log("MountedMessage unmounted"),
- )
-
- return "This value was loaded on mount."
- }),
- )
-
- return
{message}
- },
-)
-```
-
-Use `Component.useOnMount` when the component needs a value produced by an
-Effect during its first render. The value is then cached for the component
-instance. You can also use it to set up scoped component logic, subscriptions,
-or resources that should live for the lifetime of that component instance.
-
-Use `Component.useOnChange` when render needs the value computed by
-scoped work that depends on changing inputs. When a dependency changes, React
-re-renders the component and the hook computes the next value during that
-render.
-
-```tsx
-import { Console, Effect } from "effect"
-import { Component } from "effect-view"
-
-const UserPanelView = Component.make("UserPanel")(
+export const UserView = Component.make("UserView")(
function* (props: { readonly userId: string }) {
- const label = yield* Component.useOnChange(
- () =>
- Effect.gen(function* () {
- yield* Console.log(`Preparing view for ${props.userId}`)
- yield* Effect.addFinalizer(() =>
- Console.log(`Cleaning up ${props.userId}`),
- )
-
- return `Viewing user ${props.userId}`
- }),
+ const user = yield* Component.useOnChange(
+ () => loadUser(props.userId),
[props.userId],
)
- return
}),
+ Memoized.memoized,
+)
+```
+
+Use it from an Effect View parent in the usual way:
+
+```tsx
+const User = yield* UserView.use
+
+return
+```
+
+`Memoized.memoized` prevents an unrelated parent re-render from restarting an
+async child whose props have not changed. A changed `userId` creates a new
+dependency scope, cleans up the previous one, and runs `loadUser` again.
+
+An async component's rejected Effect is handled by the nearest React error
+boundary. Suspense handles waiting, not failures.
+
+For server data that should be cached, refreshed, and shared, prefer the
+[Query module](./query) over a raw async component.
+
+## Use Effect services
+
+Components can yield Effect service tags directly. Their required service type
+becomes part of the component type, so the final runtime or a local layer must
+provide it:
+
+```tsx title="src/GreetingService.ts"
+import { Context, Layer } from "effect"
+
+export class GreetingService extends Context.Service<
+ GreetingService,
+ { readonly greet: (name: string) => string }
+>()("GreetingService") {
+ static readonly layer = Layer.succeed(GreetingService, {
+ greet: (name) => `Hello, ${name}`,
+ })
+}
+```
+
+```tsx title="src/GreetingView.tsx"
+import { Component } from "effect-view"
+import { GreetingService } from "./GreetingService"
+
+export const GreetingView = Component.make("Greeting")(
+ function* (props: { readonly name: string }) {
+ const greeting = yield* GreetingService
+
+ return
{greeting.greet(props.name)}
},
)
```
-In this example, each `userId` gets its own scope. When `userId` changes,
-Effect View closes the previous scope, runs its finalizers, and creates a new
-scope for the next load. Unlike `useOnMount`, `useOnChange` does not expose the
-component's root scope directly. It creates and provides its own scope for that
-dependency window. Some other Effect View hooks follow the same pattern when they
-need a lifecycle that is narrower than the whole component instance.
+Add `GreetingService.layer` to the application runtime, or provide it only to a
+subtree as shown later on this page.
-## Useful Effect View Hooks
+Effect service instances are reactive at component boundaries. If the supplied
+context changes to contain a different service instance, dependent Effect View
+components are recreated so they read the new environment and restart their
+scoped lifecycle.
-Unlike plain React hooks, Effect View hooks return Effects. Use `yield*` to run
-them inside the component body.
+## Understand lifecycle hooks
-The most common Effect View hooks are:
+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.
-- `Component.useOnMount`: run an Effect once during the component's first render
- after mount, and return its value. The Effect must be synchronous.
+The main lifecycle choices are:
-```tsx
-const initialData = yield* Component.useOnMount(() => loadInitialData)
-```
+| Need | Hook | When setup runs |
+| --- | --- | --- |
+| 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.useOnChange`: when a dependency changes, React re-renders the
- component and the Effect is run again inside the component body. It returns
- the latest value, so the Effect must be synchronous too.
+### useOnMount
-```tsx
-const label = yield* Component.useOnChange(() => formatUserLabel(id), [id])
-```
+`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:
-- `Component.useReactEffect`: Effect-powered `React.useEffect` for side effects
- that do not need to compute render output or trigger a re-render. The Effect
- must be synchronous and can register scoped finalizers.
+```tsx title="src/LocalStateView.tsx"
+import { Effect, SubscriptionRef } from "effect"
+import { Component, Lens, View } from "effect-view"
-```tsx
-yield* Component.useReactEffect(
- () => Effect.forkScoped(subscribeToUser(id)),
- [id],
+export const LocalStateView = Component.make("LocalState")(
+ function* () {
+ const count = yield* Component.useOnMount(() =>
+ Effect.gen(function* () {
+ yield* Effect.addFinalizer(() =>
+ Effect.log("LocalState disposed"),
+ )
+
+ return Lens.fromSubscriptionRef(
+ yield* SubscriptionRef.make(0),
+ )
+ }),
+ )
+
+ const [value] = yield* View.useAll([count])
+ return
Count: {value}
+ },
)
```
-- `Component.useReactLayoutEffect`: Effect-powered `React.useLayoutEffect` with scoped
- finalizers.
+In a regular component, the setup Effect must be synchronous because it runs
+during render. In a component enhanced with `Async.async`, it may be
+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:
```tsx
-yield* Component.useReactLayoutEffect(() => measure(ref), [])
+const label = yield* Component.useOnChange(
+ () =>
+ Effect.gen(function* () {
+ yield* Effect.addFinalizer(() =>
+ Effect.log(`Stopped viewing ${props.userId}`),
+ )
+
+ return `Viewing user ${props.userId}`
+ }),
+ [props.userId],
+)
```
-- `Component.useRunSync` and `Component.useRunPromise`: run Effects from React
- event handlers.
+Dependency arrays follow normal React semantics. Include every reactive value
+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:
+
+```tsx
+yield* Component.useReactEffect(
+ () => Effect.forkScoped(listenForNotifications(props.userId)),
+ [props.userId],
+)
+```
+
+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.
+
+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.
+
+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.
+
+## Run Effects from event handlers
+
+React event handlers are plain functions. Use Effect View runners to execute an
+Effect with the component's current context and scope:
```tsx
const runPromise = yield* Component.useRunPromise()
return (
-