Docs work
Lint / lint (push) Failing after 47s

This commit is contained in:
Julien Valverdé
2026-07-20 07:35:44 +02:00
parent 5017ccf5ba
commit 19d5aba009
3 changed files with 190 additions and 168 deletions
+45 -51
View File
@@ -7,30 +7,30 @@ title: Forms
The form modules provide an Effect-first model for editable, schema-validated
state. They are not a visual component library. Instead, they give you
Lenses/Subscribables for form values, validation issues, and commit state, so
Lenses/Views for form values, validation issues, and commit state, so
you can build the UI with whatever components your app already uses.
The base `Form` type represents a field. A field tracks both the raw encoded
value used by inputs and the decoded value produced by an Effect `Schema`:
- `encodedValue`: a `Lens` containing the raw input value.
- `value`: a `Subscribable` containing the decoded value as an `Option`.
- `issues`: a `Subscribable` containing schema validation issues for that field.
- `canCommit`, `isValidating`, and `isCommitting`: `Subscribable` flags for UI
- `value`: a `View` containing the decoded value as an `Option`.
- `issues`: a `View` containing schema validation issues for that field.
- `canCommit`, `isValidating`, and `isCommitting`: `View` flags for UI
state.
There are two root form flavors:
- `SubmittableForm`: owns local encoded form state, validates it, and submits a
- `MutationForm`: owns local encoded form state, validates it, and submits a
decoded value through a mutation.
- `SynchronizedForm`: wraps an existing target `Lens` and writes valid changes
- `LensForm`: wraps an existing target `Lens` and writes valid changes
back to that target.
Most apps create one of those root forms, then focus it into fields with
`Form.focusObjectOn`, `Form.focusArrayAt`, or the other focusing helpers.
The core form model is Effect code and can technically be used outside
Effect-FC. Effect-FC adds the React-facing hooks, such as `Form.useInput`, that
Effect View. Effect View adds the React-facing hooks, such as `Form.useInput`, that
make those forms convenient to bind to components.
## Field Inputs
@@ -40,7 +40,7 @@ a React-style `{ value, setValue }` pair and writes changes back to the field's
`encodedValue` Lens.
```tsx
import { Component, Form, Subscribable } from "effect-fc"
import { Component, Form, View } from "effect-view"
const TextInputView = Component.make("TextInput")(
function* (props: {
@@ -50,7 +50,7 @@ const TextInputView = Component.make("TextInput")(
const input = yield* Form.useInput(props.form, {
debounce: "250 millis",
})
const [issues] = yield* Subscribable.useAll([props.form.issues])
const [issues] = yield* View.useAll([props.form.issues])
return (
<label>
@@ -59,8 +59,8 @@ const TextInputView = Component.make("TextInput")(
value={input.value}
onChange={(event) => input.setValue(event.currentTarget.value)}
/>
{issues.map((issue) => (
<small key={issue.path.join(".")}>
{issues.map((issue, index) => (
<small key={index}>
{issue.message}
</small>
))}
@@ -74,14 +74,14 @@ Use `Form.useOptionalInput` for `Option` fields. It returns the same value/sette
pair plus `enabled` and `setEnabled`, which is useful for optional inputs that
can be toggled on and off.
## Submittable Forms
## Mutation Forms
Use `SubmittableForm` when the user edits local encoded form state, validates it
Use `MutationForm` when the user edits local encoded form state, validates it
with a schema, and then submits a decoded value.
```tsx
import { Effect, Schema } from "effect"
import { Component, Form, SubmittableForm, Subscribable } from "effect-fc"
import { Component, Form, MutationForm, View } from "effect-view"
import { TextInputView } from "./TextInputView"
const RegisterSchema = Schema.Struct({
@@ -89,35 +89,29 @@ const RegisterSchema = Schema.Struct({
password: Schema.String,
})
class RegisterFormState extends Effect.Service<RegisterFormState>()(
"RegisterFormState",
{
scoped: Effect.gen(function* () {
const form = yield* SubmittableForm.service({
schema: RegisterSchema,
initialEncodedValue: {
email: "",
password: "",
},
f: ([value]) =>
Effect.log(`Registering ${value.email}`),
})
return {
form,
email: Form.focusObjectOn(form, "email"),
password: Form.focusObjectOn(form, "password"),
} as const
}),
},
) {}
const RegisterFormView = Component.make("RegisterForm")(
function* () {
const state = yield* RegisterFormState
const [canCommit, isCommitting] = yield* Subscribable.useAll([
state.form.canCommit,
state.form.isCommitting,
const [form, email, password] = yield* Component.useOnMount(() =>
Effect.gen(function* () {
const form = yield* MutationForm.service({
schema: RegisterSchema,
initialEncodedValue: {
email: "",
password: "",
},
f: ([value]) => Effect.log(`Registering ${value.email}`),
})
return [
form,
Form.focusObjectOn(form, "email"),
Form.focusObjectOn(form, "password"),
] as const
}),
)
const [canCommit, isCommitting] = yield* View.useAll([
form.canCommit,
form.isCommitting,
])
const runPromise = yield* Component.useRunPromise()
const TextInput = yield* TextInputView.use
@@ -126,12 +120,12 @@ const RegisterFormView = Component.make("RegisterForm")(
<form
onSubmit={(event) => {
event.preventDefault()
void runPromise(state.form.submit)
void runPromise(form.submit)
}}
>
<TextInput label="Email" form={state.email} />
<TextInput label="Password" form={state.password} />
<button disabled={!canCommit}>
<TextInput label="Email" form={email} />
<TextInput label="Password" form={password} />
<button disabled={!canCommit || isCommitting}>
{isCommitting ? "Submitting..." : "Submit"}
</button>
</form>
@@ -140,19 +134,19 @@ const RegisterFormView = Component.make("RegisterForm")(
)
```
`SubmittableForm.service` starts validation in the form's scope. The form keeps
`MutationForm.service` starts validation in the form's scope. The form keeps
its encoded input state, decoded value, validation issues, submit mutation, and
commit flags available as Lenses/Subscribables.
commit flags available as Lenses/Views.
## Synchronized Forms
## Lens Forms
Use `SynchronizedForm` when a form should edit an existing target `Lens`.
Use `LensForm` when a form should edit an existing target `Lens`.
Instead of submitting a final value, valid encoded changes are synchronized back
to the target Lens.
This is useful for edit screens where the form is a validated view over existing
state. Use `SubmittableForm` for "submit this draft" flows, and
`SynchronizedForm` for "keep this target state synchronized when valid" flows.
state. Use `MutationForm` for "submit this draft" flows, and `LensForm` for
"keep this target state synchronized when valid" flows.
## Focusing Fields
+51 -52
View File
@@ -5,23 +5,23 @@ title: Getting Started
# Getting Started
`effect-fc` lets React components be written as Effect programs. Inside a
`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.
## Install
Install `effect-fc` alongside `effect` and React 19.2 or newer:
Install `effect-view` alongside Effect 4 and React 19.2 or newer:
```bash npm2yarn
npm install effect-fc effect react
npm install effect-view effect react
```
```bash npm2yarn
npm install --save-dev @types/react
```
`effect-fc` is not opinionated about the React platform. Use it with web,
`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.
@@ -36,14 +36,14 @@ npm install --save-dev @types/react-dom
## Create A Runtime
An Effect-FC app needs an Effect runtime. Build one from the services your UI
An Effect View app needs an Effect runtime. Build one from the services your UI
needs, then share it with React through `ReactRuntime.Provider`.
For an empty app, `Layer.empty` is enough:
```tsx title="src/runtime.ts"
import { Layer } from "effect"
import { ReactRuntime } from "effect-fc"
import { ReactRuntime } from "effect-view"
export const runtime = ReactRuntime.make(Layer.empty)
```
@@ -51,9 +51,9 @@ export const runtime = ReactRuntime.make(Layer.empty)
As your app grows, add services to the layer:
```tsx title="src/runtime.ts"
import { FetchHttpClient } from "@effect/platform"
import { Layer } from "effect"
import { ReactRuntime } from "effect-fc"
import { FetchHttpClient } from "effect/unstable/http"
import { ReactRuntime } from "effect-view"
const AppLive = Layer.empty.pipe(
Layer.provideMerge(FetchHttpClient.layer),
@@ -69,7 +69,7 @@ At the React root, wrap your app with `ReactRuntime.Provider`:
```tsx title="src/main.tsx"
import { StrictMode } from "react"
import { createRoot } from "react-dom/client"
import { ReactRuntime } from "effect-fc"
import { ReactRuntime } from "effect-view"
import { App } from "./App"
import { runtime } from "./runtime"
@@ -83,22 +83,22 @@ createRoot(document.getElementById("root")!).render(
```
`ReactRuntime.Provider` also works with routers. Keep it above your router
provider so route components can be converted with the same runtime context.
provider so route components can use the same runtime context.
## Write Your First Component
Use `Component.make` when you want automatic tracing spans, or
`Component.makeUntraced` when you only want the component behavior. This creates
an Effect-FC component, not a plain React component yet.
an Effect View component, not a plain React component yet.
The Effect run during render must be synchronous. Effect-FC follows React's
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-FC helpers for
work. Use lifecycle hooks, callbacks, queries, or other Effect View helpers for
async work that happens outside render.
```tsx title="src/HelloView.tsx"
import { Effect } from "effect"
import { Component } from "effect-fc"
import { Component } from "effect-view"
export const HelloView = Component.make("HelloView")(function* (props: {
readonly name: string
@@ -114,16 +114,16 @@ work, but React cannot render it directly.
## Apply A Runtime
Use `Component.withRuntime` at the boundary where an Effect-FC component needs
Use `Component.withContext` at the boundary where an Effect View component needs
to become a regular React function component.
```tsx title="src/Hello.tsx"
import { Component } from "effect-fc"
import { Component } from "effect-view"
import { HelloView } from "./HelloView"
import { runtime } from "./runtime"
export const Hello = HelloView.pipe(
Component.withRuntime(runtime.context),
Component.withContext(runtime.context),
)
```
@@ -137,14 +137,14 @@ export function App() {
}
```
## Use Effect-FC Components Together
## Use Effect View Components Together
Inside an Effect-FC component, other Effect-FC components are available through
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.
```tsx title="src/GreetingCardView.tsx"
import { Component } from "effect-fc"
import { Component } from "effect-view"
import { HelloView } from "./HelloView"
export const GreetingCardView = Component.make("GreetingCard")(
@@ -154,29 +154,29 @@ export const GreetingCardView = Component.make("GreetingCard")(
return (
<section>
<Hello name="Effect" />
<p>This component is still running inside Effect-FC.</p>
<p>This component is still running inside Effect View.</p>
</section>
)
},
)
```
Use `Component.withRuntime` only when you leave the Effect-FC tree and need a
Use `Component.withContext` only when you leave the Effect View tree and need a
normal React component again:
```tsx title="src/GreetingCard.tsx"
import { Component } from "effect-fc"
import { Component } from "effect-view"
import { GreetingCardView } from "./GreetingCardView"
import { runtime } from "./runtime"
export const GreetingCard = GreetingCardView.pipe(
Component.withRuntime(runtime.context),
Component.withContext(runtime.context),
)
```
## Component Lifecycle
Every Effect-FC component instance exposes an Effect `Scope`. Effects that need
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.
@@ -190,7 +190,7 @@ synchronous.
```tsx title="src/MountedMessageView.tsx"
import { Console, Effect } from "effect"
import { Component } from "effect-fc"
import { Component } from "effect-view"
export const MountedMessageView = Component.make("MountedMessage")(
function* () {
@@ -222,7 +222,7 @@ render.
```tsx
import { Console, Effect } from "effect"
import { Component } from "effect-fc"
import { Component } from "effect-view"
const UserPanelView = Component.make("UserPanel")(
function* (props: { readonly userId: string }) {
@@ -245,18 +245,18 @@ const UserPanelView = Component.make("UserPanel")(
```
In this example, each `userId` gets its own scope. When `userId` changes,
Effect-FC closes the previous scope, runs its finalizers, and creates a new
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-FC hooks follow the same pattern when they
dependency window. Some other Effect View hooks follow the same pattern when they
need a lifecycle that is narrower than the whole component instance.
## Useful Effect-FC Hooks
## Useful Effect View Hooks
Unlike plain React hooks, Effect-FC hooks return Effects. Use `yield*` to run
Unlike plain React hooks, Effect View hooks return Effects. Use `yield*` to run
them inside the component body.
The most common Effect-FC hooks are:
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.
@@ -318,12 +318,12 @@ return <button onClick={() => void save(user)}>Save</button>
## Use React Normally
An Effect-FC component is still a React function component. Anything you can do
in a regular React component can also be done in an Effect-FC component,
including React hooks, refs, event handlers, context, and JSX composition.
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.
```tsx
import { Component } from "effect-fc"
import { Component } from "effect-view"
import * as React from "react"
const CounterView = Component.make("Counter")(function* () {
@@ -338,7 +338,7 @@ const CounterView = Component.make("Counter")(function* () {
})
```
Use regular React hooks for local UI concerns, and reach for Effect-FC helpers
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.
@@ -349,6 +349,9 @@ 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
}) {
@@ -359,7 +362,7 @@ const GreetingView = Component.make("Greeting")(function* (props: {
```
Service values in the runtime context are reactive by default. If a provided
service instance changes, Effect-FC unmounts and mounts again the components
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:
@@ -367,30 +370,26 @@ that behavior, pass their tags with `Component.withOptions({ nonReactiveTags:
## Provide Services To A Subtree
Use `Component.useContextFromLayer` when an Effect-FC component should provide
extra services to another Effect-FC component. It turns a `Layer` into a runtime
Use `Component.useContextFromLayer` 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`.
```tsx title="src/GreetingPageView.tsx"
import { Effect } from "effect"
import { Component } from "effect-fc"
import { Component } from "effect-view"
import { GreetingView } from "./Greeting"
import { GreetingService } from "./services"
const GreetingLive = GreetingService.Default({
greet: (name: string) => `Welcome, ${name}`,
})
import { GreetingLive } from "./services"
export const GreetingPageView = Component.make("GreetingPage")(function* () {
const context = yield* Component.useContextFromLayer(GreetingLive)
const Greeting = yield* Effect.provide(GreetingView.use, context)
const Greeting = yield* GreetingView.use.pipe(Effect.provide(context))
return <Greeting name="Effect" />
})
```
The layer passed to `useContextFromLayer` should be stable. If a new layer value
is created on every render, Effect-FC has to build a new context, which can
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.
@@ -404,12 +403,12 @@ component's lifecycle.
Once the runtime and component boundary are in place, the rest of the library
builds on the same idea:
- `Subscribable.useAll` reads Effect subscribables and rerenders when they
- `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`, `SubmittableForm`, and `SynchronizedForm` help build Effect-backed
- `Form`, `MutationForm`, and `LensForm` help build Effect-backed
forms.
The important pattern is small and repeatable: write Effect-FC components inside
the runtime, then use `Component.withRuntime` at React boundaries.
The important pattern is small and repeatable: write Effect View components inside
the runtime, then use `Component.withContext` at React boundaries.
+94 -65
View File
@@ -5,7 +5,7 @@ title: State Management
# State Management
`Lens` is the main type used for state management in `effect-fc`.
`Lens` is the main type used for state management in `effect-view`.
A Lens is an effectful handle to a piece of state. It can read the current value,
subscribe to changes, and write updates back to the underlying source. A Lens
@@ -14,53 +14,58 @@ can point at a whole state object or focus on one nested field inside it.
The usual pattern is to create state with Effect primitives such as
`SubscriptionRef`, turn that primitive into a Lens with a matching constructor
such as `Lens.fromSubscriptionRef`, and bind Lens values into components with
`Subscribable.useAll`.
`View.useAll`.
`Subscribable` is the read-only side of this model. Every Lens is also a
Subscribable.
`View` is the read-only side of this model. Every Lens is also a View.
`effect-fc` re-exports the `Lens` and `Subscribable` modules from
`effect-view` re-exports the `Lens` and `View` modules from
[`effect-lens`](https://github.com/Thiladev/effect-lens) for convenience. The
core data model and transformation APIs belong to `effect-lens`, so check the
[`effect-lens` documentation](https://github.com/Thiladev/effect-lens/tree/master/packages/effect-lens) for the full Lens/Subscribable API.
[`effect-lens` documentation](https://github.com/Thiladev/effect-lens/tree/master/packages/effect-lens) for the full Lens/View API.
## Where To Store State
State can live pretty much anywhere as a `Lens` or `Subscribable`: in a
State can live pretty much anywhere as a `Lens` or `View`: in a
service, in a layer, in a component scope, or alongside plain React state. Pick
the owner based on who needs the state. Once you have a Lens/Subscribable
the owner based on who needs the state. Once you have a Lens/View
handle, pass it around however you like, including through React props.
If state is shared by multiple components or belongs to application logic, store
it in an Effect service:
```tsx
import { Effect, SubscriptionRef } from "effect"
import { Component, Lens, Subscribable } from "effect-fc"
import { Context, Effect, Layer, SubscriptionRef } from "effect"
import { Component, Lens, View } from "effect-view"
class CounterState extends Effect.Service<CounterState>()("CounterState", {
effect: Effect.gen(function* () {
const count = Lens.fromSubscriptionRef(yield* SubscriptionRef.make(0))
class CounterState extends Context.Service<
CounterState,
{ readonly count: Lens.Lens<number> }
>()("CounterState") {
static readonly layer = Layer.effect(
CounterState,
Effect.gen(function* () {
const count = Lens.fromSubscriptionRef(yield* SubscriptionRef.make(0))
return { count } as const
}),
}) {}
return { count } as const
}),
)
}
const CounterValueView = Component.make("CounterValue")(function* () {
const state = yield* CounterState
const [count] = yield* Subscribable.useAll([state.count])
const [count] = yield* View.useAll([state.count])
return <p>Count: {count}</p>
})
```
If state belongs to a single Effect-FC component instance, or to a shallow
If state belongs to a single Effect View component instance, or to a shallow
hierarchy of subcomponents that receive it through props, create it with
`Component.useOnMount`:
```tsx
import { Effect, SubscriptionRef } from "effect"
import { Component, Lens, Subscribable } from "effect-fc"
import { Component, Lens, View } from "effect-view"
const LocalCounterView = Component.make("LocalCounter")(function* () {
const state = yield* Component.useOnMount(() =>
@@ -70,7 +75,7 @@ const LocalCounterView = Component.make("LocalCounter")(function* () {
return { count } as const
}),
)
const [count] = yield* Subscribable.useAll([state.count])
const [count] = yield* View.useAll([state.count])
return <p>Count: {count}</p>
})
@@ -80,32 +85,40 @@ For simple UI state that is not shared and does not need Effect integration,
prefer regular React state. A local "show details" toggle is usually better as
`React.useState(false)` than as a Lens.
## Subscribable.useAll
## View.useAll
A `Subscribable<A>` is reactive state with a current value and a stream of
changes. Use `Subscribable.useAll` whenever a component needs to bind
subscribable values into render output.
A `View<A>` is reactive state with a current value and a stream of changes. Use
`View.useAll` whenever a component needs to bind View values into render output.
`Lens` is a `Subscribable`, so this is also the default way to read Lens values
`Lens` is a `View`, so this is also the default way to read Lens values
from a component.
```tsx
import { Effect, SubscriptionRef } from "effect"
import { Component, Lens, Subscribable } from "effect-fc"
import { Context, Effect, Layer, SubscriptionRef } from "effect"
import { Component, Lens, View } from "effect-view"
class CounterState extends Effect.Service<CounterState>()("CounterState", {
effect: Effect.gen(function* () {
const count = Lens.fromSubscriptionRef(yield* SubscriptionRef.make(0))
const doubled = Subscribable.map(count, (n) => n * 2)
class CounterState extends Context.Service<
CounterState,
{
readonly count: Lens.Lens<number>
readonly doubled: View.View<number>
}
>()("CounterState") {
static readonly layer = Layer.effect(
CounterState,
Effect.gen(function* () {
const count = Lens.fromSubscriptionRef(yield* SubscriptionRef.make(0))
const doubled = View.map(count, (n) => n * 2)
return { count, doubled } as const
}),
}) {}
return { count, doubled } as const
}),
)
}
const CounterReadOnlyView = Component.make("CounterReadOnly")(
function* () {
const state = yield* CounterState
const [count, doubled] = yield* Subscribable.useAll([
const [count, doubled] = yield* View.useAll([
state.count,
state.doubled,
])
@@ -115,7 +128,7 @@ const CounterReadOnlyView = Component.make("CounterReadOnly")(
)
```
`Subscribable.useAll` reads the current values during render and uses scoped subscriptions to update React state when changes arrive.
`View.useAll` reads the current values during render and uses scoped subscriptions to update React state when changes arrive.
When you need to modify state, write to the Lens with `Lens.set` or `Lens.update`:
@@ -123,7 +136,7 @@ When you need to modify state, write to the Lens with `Lens.set` or `Lens.update
const CounterControlsView = Component.make("CounterControls")(
function* () {
const state = yield* CounterState
const [count] = yield* Subscribable.useAll([state.count])
const [count] = yield* View.useAll([state.count])
const increment = yield* Component.useCallbackSync(
() => Lens.update(state.count, (n) => n + 1),
@@ -152,21 +165,27 @@ tuple, backed by a Lens. Reach for it when the JSX API expects a synchronous
setter, especially controlled inputs such as text fields, checkboxes, selects,
or third-party components with `value` / `onChange` props.
If a component only needs to display the value, prefer `Subscribable.useAll`.
If a component only needs to display the value, prefer `View.useAll`.
`Lens.useState` is for places where reading and writing need to be wired
together in React's local-state shape.
```tsx
import { Effect, SubscriptionRef } from "effect"
import { Component, Lens } from "effect-fc"
import { Context, Effect, Layer, SubscriptionRef } from "effect"
import { Component, Lens } from "effect-view"
class FormState extends Effect.Service<FormState>()("FormState", {
effect: Effect.gen(function* () {
const name = Lens.fromSubscriptionRef(yield* SubscriptionRef.make(""))
class FormState extends Context.Service<
FormState,
{ readonly name: Lens.Lens<string> }
>()("FormState") {
static readonly layer = Layer.effect(
FormState,
Effect.gen(function* () {
const name = Lens.fromSubscriptionRef(yield* SubscriptionRef.make(""))
return { name } as const
}),
}) {}
return { name } as const
}),
)
}
const NameInputView = Component.make("NameInput")(function* () {
const state = yield* FormState
@@ -189,12 +208,12 @@ same Lens sees the update.
Use focused Lenses when a component should work with one part of a larger state
object. A focused Lens is still a Lens, so it can be read with
`Subscribable.useAll` or used with `Lens.useState` when React needs a
`View.useAll` or used with `Lens.useState` when React needs a
read/write tuple.
```tsx
import { Effect, SubscriptionRef } from "effect"
import { Component, Lens, Subscribable } from "effect-fc"
import { Context, Effect, Layer, SubscriptionRef } from "effect"
import { Component, Lens, View } from "effect-view"
interface UserProfile {
readonly name: string
@@ -202,26 +221,36 @@ interface UserProfile {
readonly role: string
}
class ProfileState extends Effect.Service<ProfileState>()("ProfileState", {
effect: Effect.gen(function* () {
const profile = Lens.fromSubscriptionRef(
yield* SubscriptionRef.make<UserProfile>({
name: "",
email: "",
role: "reader",
}),
)
const name = Lens.focusObjectOn(profile, "name")
const role = Lens.focusObjectOn(profile, "role")
class ProfileState extends Context.Service<
ProfileState,
{
readonly profile: Lens.Lens<UserProfile>
readonly name: Lens.Lens<string>
readonly role: Lens.Lens<string>
}
>()("ProfileState") {
static readonly layer = Layer.effect(
ProfileState,
Effect.gen(function* () {
const profile = Lens.fromSubscriptionRef(
yield* SubscriptionRef.make<UserProfile>({
name: "",
email: "",
role: "reader",
}),
)
const name = Lens.focusObjectOn(profile, "name")
const role = Lens.focusObjectOn(profile, "role")
return { profile, name, role } as const
}),
}) {}
return { profile, name, role } as const
}),
)
}
const ProfileNameView = Component.make("ProfileName")(function* () {
const state = yield* ProfileState
const [name, setName] = yield* Lens.useState(state.name)
const [role] = yield* Subscribable.useAll([state.role])
const [role] = yield* View.useAll([state.role])
return (
<label>
@@ -238,7 +267,7 @@ const ProfileNameView = Component.make("ProfileName")(function* () {
Updating the focused `name` Lens through `Lens.useState` updates the parent
`profile` Lens. The focused `role` Lens is only read, so it stays on the simpler
`Subscribable.useAll` path.
`View.useAll` path.
For focusing into nested state, deriving lenses, custom write behavior, and the
complete API, refer to the