@@ -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(
|
||||
<StrictMode>
|
||||
<ReactRuntime.Provider runtime={runtime}>
|
||||
<ReactRuntime.Provider
|
||||
runtime={runtime}
|
||||
fallback={<p>Starting application...</p>}
|
||||
>
|
||||
<App />
|
||||
</ReactRuntime.Provider>
|
||||
</StrictMode>,
|
||||
)
|
||||
```
|
||||
|
||||
`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
|
||||
<ReactRuntime.Provider runtime={runtime} fallback={<p>Starting...</p>}>
|
||||
<RouterProvider router={router} />
|
||||
</ReactRuntime.Provider>
|
||||
```
|
||||
|
||||
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
|
||||
}) {
|
||||
export const HelloView = Component.make("HelloView")(
|
||||
function* (props: { readonly name: string }) {
|
||||
const message = yield* Effect.succeed(`Hello, ${props.name}`)
|
||||
|
||||
return <h1>{message}</h1>
|
||||
})
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
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 (
|
||||
<section>
|
||||
<Hello name="Effect" />
|
||||
<p>This component is still running inside Effect View.</p>
|
||||
<p>Both components use the same Effect context.</p>
|
||||
</section>
|
||||
)
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
Use `Async.async` when render genuinely depends on an asynchronous Effect. The
|
||||
component then suspends and accepts React Suspense props such as `fallback`:
|
||||
|
||||
```tsx title="src/UserView.tsx"
|
||||
import { Async, Component, Memoized } from "effect-view"
|
||||
import { loadUser } from "./api"
|
||||
|
||||
export const UserView = Component.make("UserView")(
|
||||
function* (props: { readonly userId: string }) {
|
||||
const user = yield* Component.useOnChange(
|
||||
() => loadUser(props.userId),
|
||||
[props.userId],
|
||||
)
|
||||
|
||||
return <h2>{user.name}</h2>
|
||||
},
|
||||
).pipe(
|
||||
Async.async,
|
||||
Async.withOptions({ defaultFallback: <p>Loading user...</p> }),
|
||||
Memoized.memoized,
|
||||
)
|
||||
```
|
||||
|
||||
## Component Lifecycle
|
||||
Use it from an Effect View parent in the usual way:
|
||||
|
||||
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
|
||||
const User = yield* UserView.use
|
||||
|
||||
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.
|
||||
return <User userId={selectedUserId} />
|
||||
```
|
||||
|
||||
Finalizers are forked when the scope closes, so cleanup logic can run
|
||||
asynchronous Effects even though the component body itself must stay
|
||||
synchronous.
|
||||
`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.
|
||||
|
||||
```tsx title="src/MountedMessageView.tsx"
|
||||
import { Console, Effect } from "effect"
|
||||
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 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"),
|
||||
)
|
||||
export const GreetingView = Component.make("Greeting")(
|
||||
function* (props: { readonly name: string }) {
|
||||
const greeting = yield* GreetingService
|
||||
|
||||
return "This value was loaded on mount."
|
||||
}),
|
||||
)
|
||||
|
||||
return <p>{message}</p>
|
||||
return <p>{greeting.greet(props.name)}</p>
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
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.
|
||||
Add `GreetingService.layer` to the application runtime, or provide it only to a
|
||||
subtree as shown later on this page.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
## Understand lifecycle hooks
|
||||
|
||||
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 main lifecycle choices are:
|
||||
|
||||
| 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 |
|
||||
|
||||
### 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:
|
||||
|
||||
```tsx title="src/LocalStateView.tsx"
|
||||
import { Effect, SubscriptionRef } from "effect"
|
||||
import { Component, Lens, View } from "effect-view"
|
||||
|
||||
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 <p>Count: {value}</p>
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
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
|
||||
import { Console, Effect } from "effect"
|
||||
import { Component } from "effect-view"
|
||||
|
||||
const UserPanelView = Component.make("UserPanel")(
|
||||
function* (props: { readonly userId: string }) {
|
||||
const label = yield* Component.useOnChange(
|
||||
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}`),
|
||||
Effect.log(`Stopped viewing ${props.userId}`),
|
||||
)
|
||||
|
||||
return `Viewing user ${props.userId}`
|
||||
}),
|
||||
[props.userId],
|
||||
)
|
||||
|
||||
return <p>{label}</p>
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
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.
|
||||
Dependency arrays follow normal React semantics. Include every reactive value
|
||||
read by the setup Effect.
|
||||
|
||||
## Useful Effect View Hooks
|
||||
### useReactEffect and useReactLayoutEffect
|
||||
|
||||
Unlike plain React hooks, Effect View hooks return Effects. Use `yield*` to run
|
||||
them inside the component body.
|
||||
|
||||
The most common Effect View hooks are:
|
||||
|
||||
- `Component.useOnMount`: run an Effect once during the component's first render
|
||||
after mount, and return its value. The Effect must be synchronous.
|
||||
|
||||
```tsx
|
||||
const initialData = yield* Component.useOnMount(() => loadInitialData)
|
||||
```
|
||||
|
||||
- `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.
|
||||
|
||||
```tsx
|
||||
const label = yield* Component.useOnChange(() => formatUserLabel(id), [id])
|
||||
```
|
||||
|
||||
- `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.
|
||||
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(subscribeToUser(id)),
|
||||
[id],
|
||||
() => Effect.forkScoped(listenForNotifications(props.userId)),
|
||||
[props.userId],
|
||||
)
|
||||
```
|
||||
|
||||
- `Component.useReactLayoutEffect`: Effect-powered `React.useLayoutEffect` with scoped
|
||||
finalizers.
|
||||
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.
|
||||
|
||||
```tsx
|
||||
yield* Component.useReactLayoutEffect(() => measure(ref), [])
|
||||
```
|
||||
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.
|
||||
|
||||
- `Component.useRunSync` and `Component.useRunPromise`: run Effects from React
|
||||
event handlers.
|
||||
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 (
|
||||
<button onClick={() => void runPromise(saveUser)}>
|
||||
<button onClick={() => void runPromise(saveUser(user))}>
|
||||
Save
|
||||
</button>
|
||||
)
|
||||
```
|
||||
|
||||
- `Component.useCallbackSync` and `Component.useCallbackPromise`: create stable
|
||||
React callbacks.
|
||||
Use `Component.useRunSync` only for Effects known to complete synchronously.
|
||||
Use `Component.useRunPromise` for Effects that may suspend, sleep, fetch, or
|
||||
otherwise continue asynchronously.
|
||||
|
||||
When a callback is passed to a memoized child or used as a dependency, use the
|
||||
callback variants to preserve its identity:
|
||||
|
||||
```tsx
|
||||
const save = yield* Component.useCallbackPromise(
|
||||
(user: User) => saveUser(user),
|
||||
[],
|
||||
(nextUser: User) => saveUser(nextUser),
|
||||
[saveUser],
|
||||
)
|
||||
|
||||
return <button onClick={() => void save(user)}>Save</button>
|
||||
return <SaveButton onSave={() => void save(user)} />
|
||||
```
|
||||
|
||||
## Use React Normally
|
||||
`useCallbackSync` and `useCallbackPromise` follow the same dependency rules as
|
||||
`React.useCallback`, while also supplying the Effect context when invoked.
|
||||
|
||||
An Effect View component runs as a React function component after it is bound to
|
||||
a runtime context. It can use regular React hooks, refs, event handlers,
|
||||
context, and JSX composition.
|
||||
## Use React normally
|
||||
|
||||
Effect View components can use regular React hooks, refs, context, event
|
||||
handlers, and JSX composition:
|
||||
|
||||
```tsx
|
||||
import { Component } from "effect-view"
|
||||
@@ -331,84 +413,77 @@ const CounterView = Component.make("Counter")(function* () {
|
||||
const buttonRef = React.useRef<HTMLButtonElement>(null)
|
||||
|
||||
return (
|
||||
<button ref={buttonRef} onClick={() => setCount(count + 1)}>
|
||||
<button
|
||||
ref={buttonRef}
|
||||
onClick={() => setCount((count) => count + 1)}
|
||||
>
|
||||
Count: {count}
|
||||
</button>
|
||||
)
|
||||
})
|
||||
```
|
||||
|
||||
Use regular React hooks for local UI concerns, and reach for Effect View helpers
|
||||
when the component needs Effect services, scopes, resources, or Effect-powered
|
||||
callbacks.
|
||||
Prefer regular React state for simple, component-local UI concerns. Use
|
||||
`Lens`/`View` when state needs Effect integration, subscriptions, focusing, or
|
||||
sharing. The [State Management guide](./state-management) covers that model.
|
||||
|
||||
## Use Tags
|
||||
## Provide services to a subtree
|
||||
|
||||
Components can yield Effect tags directly in the component body. There is no
|
||||
need to memoize the service yourself; just yield the tag wherever the component
|
||||
needs it.
|
||||
|
||||
```tsx title="src/Greeting.tsx"
|
||||
import { Component } from "effect-view"
|
||||
import { GreetingService } from "./services"
|
||||
|
||||
const GreetingView = Component.make("Greeting")(function* (props: {
|
||||
readonly name: string
|
||||
}) {
|
||||
const greeting = yield* GreetingService
|
||||
|
||||
return <p>{greeting.greet(props.name)}</p>
|
||||
})
|
||||
```
|
||||
|
||||
Service values in the runtime context are reactive by default. If a provided
|
||||
service instance changes, Effect View unmounts and mounts again the components
|
||||
that depend on that context, so they read the new service and restart their
|
||||
scoped lifecycle with the new environment. For services that should not trigger
|
||||
that behavior, pass their tags with `Component.withOptions({ nonReactiveTags:
|
||||
[...] })`.
|
||||
|
||||
## Provide Services To A Subtree
|
||||
|
||||
Use `Component.useLayer` when an Effect View component should provide
|
||||
extra services to another Effect View component. It turns a `Layer` into a runtime
|
||||
context that can be provided to `.use`.
|
||||
Use `Component.useLayer` when only one Effect View subtree needs extra
|
||||
services. It builds the layer in a scope and returns the resulting Effect
|
||||
context:
|
||||
|
||||
```tsx title="src/GreetingPageView.tsx"
|
||||
import { Effect } from "effect"
|
||||
import { Component } from "effect-view"
|
||||
import { GreetingView } from "./Greeting"
|
||||
import { GreetingLive } from "./services"
|
||||
import { GreetingView } from "./GreetingView"
|
||||
import { GreetingService } from "./GreetingService"
|
||||
|
||||
export const GreetingPageView = Component.make("GreetingPage")(function* () {
|
||||
const context = yield* Component.useLayer(GreetingLive)
|
||||
const Greeting = yield* GreetingView.use.pipe(Effect.provide(context))
|
||||
export const GreetingPageView = Component.make("GreetingPage")(
|
||||
function* () {
|
||||
const context = yield* Component.useLayer(GreetingService.layer)
|
||||
const Greeting = yield* GreetingView.use.pipe(
|
||||
Effect.provide(context),
|
||||
)
|
||||
|
||||
return <Greeting name="Effect" />
|
||||
})
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
The layer passed to `useLayer` should be stable. If a new layer value
|
||||
is created on every render, Effect View has to build a new context, which can
|
||||
cause unnecessary re-renders and scoped lifecycle restarts. Define static layers
|
||||
outside the component, or memoize layers that depend on React props or state.
|
||||
Keep the layer reference stable. Define static layers outside the component or
|
||||
memoize a layer that depends on props with `React.useMemo`; a new layer object
|
||||
causes reconstruction and scoped cleanup.
|
||||
|
||||
Layers built with `useLayer` are scoped to the component that builds
|
||||
them. That means service construction can register scoped logic, acquire
|
||||
resources, fork scoped fibers, and add finalizers that are tied to that
|
||||
component's lifecycle.
|
||||
Layer construction runs during render through `useOnChange`. A layer with
|
||||
asynchronous acquisition requires the component to be enhanced with
|
||||
`Async.async`. Resources built by the layer are released when the owning
|
||||
component unmounts or the layer reference changes.
|
||||
|
||||
## Where To Go Next
|
||||
## Common pitfalls
|
||||
|
||||
Once the runtime and component boundary are in place, the rest of the library
|
||||
builds on the same idea:
|
||||
- Effect View hooks obey React's Rules of Hooks even though they are called
|
||||
with `yield*`.
|
||||
- Do not yield an asynchronous Effect from a regular component body. Use
|
||||
`Async.async`, Query, a Mutation callback, or a post-commit scoped fiber.
|
||||
- `Component.withContext` needs a matching `ReactRuntime.Provider` above it.
|
||||
- Apply `withContext` at React boundaries, not between Effect View components.
|
||||
- Keep runtimes and layers stable instead of creating them on every render.
|
||||
- Use `useRunPromise` rather than `useRunSync` for asynchronous event work.
|
||||
- Suspense fallbacks handle waiting; React error boundaries handle failed
|
||||
component Effects.
|
||||
|
||||
- `View.useAll` reads Effect views and rerenders when they
|
||||
change.
|
||||
- `Lens` connects React state and Effect `SubscriptionRef` values.
|
||||
- `Query` and `Mutation` model async data and user-triggered operations.
|
||||
- `Form`, `MutationForm`, and `LensForm` help build Effect-backed
|
||||
forms.
|
||||
## Where to go next
|
||||
|
||||
The important pattern is small and repeatable: write Effect View components inside
|
||||
the runtime, then use `Component.withContext` at React boundaries.
|
||||
- [State Management](./state-management) explains `Lens`, `View`, local state,
|
||||
and focused state.
|
||||
- [Query](./query) adds reactive keys, caching, refresh, and invalidation to
|
||||
Effect-based server reads.
|
||||
- [Mutation](./mutation) models user-triggered Effect operations and their
|
||||
`AsyncResult` state.
|
||||
- [Forms](./forms) builds schema-driven editable state with `MutationForm` and
|
||||
`LensForm`.
|
||||
|
||||
The repeatable pattern is small: provide one runtime, define components as
|
||||
Effect programs, cross into plain React with `Component.withContext`, and use
|
||||
`.use` everywhere inside the Effect View tree.
|
||||
|
||||
Reference in New Issue
Block a user