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
3 changed files with 190 additions and 168 deletions
Showing only changes of commit 19d5aba009 - Show all commits
+38 -44
View File
@@ -7,30 +7,30 @@ title: Forms
The form modules provide an Effect-first model for editable, schema-validated The form modules provide an Effect-first model for editable, schema-validated
state. They are not a visual component library. Instead, they give you 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. 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 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`: value used by inputs and the decoded value produced by an Effect `Schema`:
- `encodedValue`: a `Lens` containing the raw input value. - `encodedValue`: a `Lens` containing the raw input value.
- `value`: a `Subscribable` containing the decoded value as an `Option`. - `value`: a `View` containing the decoded value as an `Option`.
- `issues`: a `Subscribable` containing schema validation issues for that field. - `issues`: a `View` containing schema validation issues for that field.
- `canCommit`, `isValidating`, and `isCommitting`: `Subscribable` flags for UI - `canCommit`, `isValidating`, and `isCommitting`: `View` flags for UI
state. state.
There are two root form flavors: 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. 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. back to that target.
Most apps create one of those root forms, then focus it into fields with Most apps create one of those root forms, then focus it into fields with
`Form.focusObjectOn`, `Form.focusArrayAt`, or the other focusing helpers. `Form.focusObjectOn`, `Form.focusArrayAt`, or the other focusing helpers.
The core form model is Effect code and can technically be used outside 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. make those forms convenient to bind to components.
## Field Inputs ## Field Inputs
@@ -40,7 +40,7 @@ a React-style `{ value, setValue }` pair and writes changes back to the field's
`encodedValue` Lens. `encodedValue` Lens.
```tsx ```tsx
import { Component, Form, Subscribable } from "effect-fc" import { Component, Form, View } from "effect-view"
const TextInputView = Component.make("TextInput")( const TextInputView = Component.make("TextInput")(
function* (props: { function* (props: {
@@ -50,7 +50,7 @@ const TextInputView = Component.make("TextInput")(
const input = yield* Form.useInput(props.form, { const input = yield* Form.useInput(props.form, {
debounce: "250 millis", debounce: "250 millis",
}) })
const [issues] = yield* Subscribable.useAll([props.form.issues]) const [issues] = yield* View.useAll([props.form.issues])
return ( return (
<label> <label>
@@ -59,8 +59,8 @@ const TextInputView = Component.make("TextInput")(
value={input.value} value={input.value}
onChange={(event) => input.setValue(event.currentTarget.value)} onChange={(event) => input.setValue(event.currentTarget.value)}
/> />
{issues.map((issue) => ( {issues.map((issue, index) => (
<small key={issue.path.join(".")}> <small key={index}>
{issue.message} {issue.message}
</small> </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 pair plus `enabled` and `setEnabled`, which is useful for optional inputs that
can be toggled on and off. 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. with a schema, and then submits a decoded value.
```tsx ```tsx
import { Effect, Schema } from "effect" 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" import { TextInputView } from "./TextInputView"
const RegisterSchema = Schema.Struct({ const RegisterSchema = Schema.Struct({
@@ -89,35 +89,29 @@ const RegisterSchema = Schema.Struct({
password: Schema.String, password: Schema.String,
}) })
class RegisterFormState extends Effect.Service<RegisterFormState>()( const RegisterFormView = Component.make("RegisterForm")(
"RegisterFormState", function* () {
{ const [form, email, password] = yield* Component.useOnMount(() =>
scoped: Effect.gen(function* () { Effect.gen(function* () {
const form = yield* SubmittableForm.service({ const form = yield* MutationForm.service({
schema: RegisterSchema, schema: RegisterSchema,
initialEncodedValue: { initialEncodedValue: {
email: "", email: "",
password: "", password: "",
}, },
f: ([value]) => f: ([value]) => Effect.log(`Registering ${value.email}`),
Effect.log(`Registering ${value.email}`),
}) })
return { return [
form, form,
email: Form.focusObjectOn(form, "email"), Form.focusObjectOn(form, "email"),
password: Form.focusObjectOn(form, "password"), Form.focusObjectOn(form, "password"),
} as const ] as const
}), }),
}, )
) {} const [canCommit, isCommitting] = yield* View.useAll([
form.canCommit,
const RegisterFormView = Component.make("RegisterForm")( form.isCommitting,
function* () {
const state = yield* RegisterFormState
const [canCommit, isCommitting] = yield* Subscribable.useAll([
state.form.canCommit,
state.form.isCommitting,
]) ])
const runPromise = yield* Component.useRunPromise() const runPromise = yield* Component.useRunPromise()
const TextInput = yield* TextInputView.use const TextInput = yield* TextInputView.use
@@ -126,12 +120,12 @@ const RegisterFormView = Component.make("RegisterForm")(
<form <form
onSubmit={(event) => { onSubmit={(event) => {
event.preventDefault() event.preventDefault()
void runPromise(state.form.submit) void runPromise(form.submit)
}} }}
> >
<TextInput label="Email" form={state.email} /> <TextInput label="Email" form={email} />
<TextInput label="Password" form={state.password} /> <TextInput label="Password" form={password} />
<button disabled={!canCommit}> <button disabled={!canCommit || isCommitting}>
{isCommitting ? "Submitting..." : "Submit"} {isCommitting ? "Submitting..." : "Submit"}
</button> </button>
</form> </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 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 Instead of submitting a final value, valid encoded changes are synchronized back
to the target Lens. to the target Lens.
This is useful for edit screens where the form is a validated view over existing This is useful for edit screens where the form is a validated view over existing
state. Use `SubmittableForm` for "submit this draft" flows, and state. Use `MutationForm` for "submit this draft" flows, and `LensForm` for
`SynchronizedForm` for "keep this target state synchronized when valid" flows. "keep this target state synchronized when valid" flows.
## Focusing Fields ## Focusing Fields
+51 -52
View File
@@ -5,23 +5,23 @@ title: Getting Started
# 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 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 state, and still export a normal React function component at the edge of your
app. app.
## Install ## 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 ```bash npm2yarn
npm install effect-fc effect react npm install effect-view effect react
``` ```
```bash npm2yarn ```bash npm2yarn
npm install --save-dev @types/react 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. native, custom renderers, or any environment where React components can run.
Then install the platform-specific React packages for your target. Then install the platform-specific React packages for your target.
@@ -36,14 +36,14 @@ npm install --save-dev @types/react-dom
## Create A Runtime ## 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`. needs, then share it with React through `ReactRuntime.Provider`.
For an empty app, `Layer.empty` is enough: For an empty app, `Layer.empty` is enough:
```tsx title="src/runtime.ts" ```tsx title="src/runtime.ts"
import { Layer } from "effect" import { Layer } from "effect"
import { ReactRuntime } from "effect-fc" import { ReactRuntime } from "effect-view"
export const runtime = ReactRuntime.make(Layer.empty) 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: As your app grows, add services to the layer:
```tsx title="src/runtime.ts" ```tsx title="src/runtime.ts"
import { FetchHttpClient } from "@effect/platform"
import { Layer } from "effect" 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( const AppLive = Layer.empty.pipe(
Layer.provideMerge(FetchHttpClient.layer), Layer.provideMerge(FetchHttpClient.layer),
@@ -69,7 +69,7 @@ At the React root, wrap your app with `ReactRuntime.Provider`:
```tsx title="src/main.tsx" ```tsx title="src/main.tsx"
import { StrictMode } from "react" import { StrictMode } from "react"
import { createRoot } from "react-dom/client" import { createRoot } from "react-dom/client"
import { ReactRuntime } from "effect-fc" import { ReactRuntime } from "effect-view"
import { App } from "./App" import { App } from "./App"
import { runtime } from "./runtime" import { runtime } from "./runtime"
@@ -83,22 +83,22 @@ createRoot(document.getElementById("root")!).render(
``` ```
`ReactRuntime.Provider` also works with routers. Keep it above your router `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 ## Write Your First Component
Use `Component.make` when you want automatic tracing spans, or Use `Component.make` when you want automatic tracing spans, or
`Component.makeUntraced` when you only want the component behavior. This creates `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 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. async work that happens outside render.
```tsx title="src/HelloView.tsx" ```tsx title="src/HelloView.tsx"
import { Effect } from "effect" import { Effect } from "effect"
import { Component } from "effect-fc" import { Component } from "effect-view"
export const HelloView = Component.make("HelloView")(function* (props: { export const HelloView = Component.make("HelloView")(function* (props: {
readonly name: string readonly name: string
@@ -114,16 +114,16 @@ work, but React cannot render it directly.
## Apply A Runtime ## 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. to become a regular React function component.
```tsx title="src/Hello.tsx" ```tsx title="src/Hello.tsx"
import { Component } from "effect-fc" import { Component } from "effect-view"
import { HelloView } from "./HelloView" import { HelloView } from "./HelloView"
import { runtime } from "./runtime" import { runtime } from "./runtime"
export const Hello = HelloView.pipe( 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 their `.use` effect. Yield `.use` to get a React component for the current
runtime context, then render it like JSX. runtime context, then render it like JSX.
```tsx title="src/GreetingCardView.tsx" ```tsx title="src/GreetingCardView.tsx"
import { Component } from "effect-fc" import { Component } from "effect-view"
import { HelloView } from "./HelloView" import { HelloView } from "./HelloView"
export const GreetingCardView = Component.make("GreetingCard")( export const GreetingCardView = Component.make("GreetingCard")(
@@ -154,29 +154,29 @@ export const GreetingCardView = Component.make("GreetingCard")(
return ( return (
<section> <section>
<Hello name="Effect" /> <Hello name="Effect" />
<p>This component is still running inside Effect-FC.</p> <p>This component is still running inside Effect View.</p>
</section> </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: normal React component again:
```tsx title="src/GreetingCard.tsx" ```tsx title="src/GreetingCard.tsx"
import { Component } from "effect-fc" import { Component } from "effect-view"
import { GreetingCardView } from "./GreetingCardView" import { GreetingCardView } from "./GreetingCardView"
import { runtime } from "./runtime" import { runtime } from "./runtime"
export const GreetingCard = GreetingCardView.pipe( export const GreetingCard = GreetingCardView.pipe(
Component.withRuntime(runtime.context), Component.withContext(runtime.context),
) )
``` ```
## Component Lifecycle ## 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 `Scope.Scope` can use that component scope to register finalizers, fork scoped
work, or acquire scoped resources. work, or acquire scoped resources.
@@ -190,7 +190,7 @@ synchronous.
```tsx title="src/MountedMessageView.tsx" ```tsx title="src/MountedMessageView.tsx"
import { Console, Effect } from "effect" import { Console, Effect } from "effect"
import { Component } from "effect-fc" import { Component } from "effect-view"
export const MountedMessageView = Component.make("MountedMessage")( export const MountedMessageView = Component.make("MountedMessage")(
function* () { function* () {
@@ -222,7 +222,7 @@ render.
```tsx ```tsx
import { Console, Effect } from "effect" import { Console, Effect } from "effect"
import { Component } from "effect-fc" import { Component } from "effect-view"
const UserPanelView = Component.make("UserPanel")( const UserPanelView = Component.make("UserPanel")(
function* (props: { readonly userId: string }) { 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, 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 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 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. 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. 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 - `Component.useOnMount`: run an Effect once during the component's first render
after mount, and return its value. The Effect must be synchronous. 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 ## Use React Normally
An Effect-FC component is still a React function component. Anything you can do An Effect View component runs as a React function component after it is bound to
in a regular React component can also be done in an Effect-FC component, a runtime context. It can use regular React hooks, refs, event handlers,
including React hooks, refs, event handlers, context, and JSX composition. context, and JSX composition.
```tsx ```tsx
import { Component } from "effect-fc" import { Component } from "effect-view"
import * as React from "react" import * as React from "react"
const CounterView = Component.make("Counter")(function* () { 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 when the component needs Effect services, scopes, resources, or Effect-powered
callbacks. callbacks.
@@ -349,6 +349,9 @@ need to memoize the service yourself; just yield the tag wherever the component
needs it. needs it.
```tsx title="src/Greeting.tsx" ```tsx title="src/Greeting.tsx"
import { Component } from "effect-view"
import { GreetingService } from "./services"
const GreetingView = Component.make("Greeting")(function* (props: { const GreetingView = Component.make("Greeting")(function* (props: {
readonly name: string 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 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 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 scoped lifecycle with the new environment. For services that should not trigger
that behavior, pass their tags with `Component.withOptions({ nonReactiveTags: 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 ## Provide Services To A Subtree
Use `Component.useContextFromLayer` when an Effect-FC component should provide Use `Component.useContextFromLayer` when an Effect View component should provide
extra services to another Effect-FC component. It turns a `Layer` into a runtime extra services to another Effect View component. It turns a `Layer` into a runtime
context that can be provided to `.use`. context that can be provided to `.use`.
```tsx title="src/GreetingPageView.tsx" ```tsx title="src/GreetingPageView.tsx"
import { Effect } from "effect" import { Effect } from "effect"
import { Component } from "effect-fc" import { Component } from "effect-view"
import { GreetingView } from "./Greeting" import { GreetingView } from "./Greeting"
import { GreetingService } from "./services" import { GreetingLive } from "./services"
const GreetingLive = GreetingService.Default({
greet: (name: string) => `Welcome, ${name}`,
})
export const GreetingPageView = Component.make("GreetingPage")(function* () { export const GreetingPageView = Component.make("GreetingPage")(function* () {
const context = yield* Component.useContextFromLayer(GreetingLive) 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" /> return <Greeting name="Effect" />
}) })
``` ```
The layer passed to `useContextFromLayer` should be stable. If a new layer value 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 cause unnecessary re-renders and scoped lifecycle restarts. Define static layers
outside the component, or memoize layers that depend on React props or state. 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 Once the runtime and component boundary are in place, the rest of the library
builds on the same idea: builds on the same idea:
- `Subscribable.useAll` reads Effect subscribables and rerenders when they - `View.useAll` reads Effect views and rerenders when they
change. change.
- `Lens` connects React state and Effect `SubscriptionRef` values. - `Lens` connects React state and Effect `SubscriptionRef` values.
- `Query` and `Mutation` model async data and user-triggered operations. - `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. forms.
The important pattern is small and repeatable: write Effect-FC components inside The important pattern is small and repeatable: write Effect View components inside
the runtime, then use `Component.withRuntime` at React boundaries. the runtime, then use `Component.withContext` at React boundaries.
+74 -45
View File
@@ -5,7 +5,7 @@ title: State Management
# 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, 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 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 The usual pattern is to create state with Effect primitives such as
`SubscriptionRef`, turn that primitive into a Lens with a matching constructor `SubscriptionRef`, turn that primitive into a Lens with a matching constructor
such as `Lens.fromSubscriptionRef`, and bind Lens values into components with 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 `View` is the read-only side of this model. Every Lens is also a View.
Subscribable.
`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 [`effect-lens`](https://github.com/Thiladev/effect-lens) for convenience. The
core data model and transformation APIs belong to `effect-lens`, so check 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 ## 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 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. handle, pass it around however you like, including through React props.
If state is shared by multiple components or belongs to application logic, store If state is shared by multiple components or belongs to application logic, store
it in an Effect service: it in an Effect service:
```tsx ```tsx
import { Effect, SubscriptionRef } from "effect" import { Context, Effect, Layer, SubscriptionRef } from "effect"
import { Component, Lens, Subscribable } from "effect-fc" import { Component, Lens, View } from "effect-view"
class CounterState extends Effect.Service<CounterState>()("CounterState", { class CounterState extends Context.Service<
effect: Effect.gen(function* () { CounterState,
{ readonly count: Lens.Lens<number> }
>()("CounterState") {
static readonly layer = Layer.effect(
CounterState,
Effect.gen(function* () {
const count = Lens.fromSubscriptionRef(yield* SubscriptionRef.make(0)) const count = Lens.fromSubscriptionRef(yield* SubscriptionRef.make(0))
return { count } as const return { count } as const
}), }),
}) {} )
}
const CounterValueView = Component.make("CounterValue")(function* () { const CounterValueView = Component.make("CounterValue")(function* () {
const state = yield* CounterState const state = yield* CounterState
const [count] = yield* Subscribable.useAll([state.count]) const [count] = yield* View.useAll([state.count])
return <p>Count: {count}</p> 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 hierarchy of subcomponents that receive it through props, create it with
`Component.useOnMount`: `Component.useOnMount`:
```tsx ```tsx
import { Effect, SubscriptionRef } from "effect" 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 LocalCounterView = Component.make("LocalCounter")(function* () {
const state = yield* Component.useOnMount(() => const state = yield* Component.useOnMount(() =>
@@ -70,7 +75,7 @@ const LocalCounterView = Component.make("LocalCounter")(function* () {
return { count } as const return { count } as const
}), }),
) )
const [count] = yield* Subscribable.useAll([state.count]) const [count] = yield* View.useAll([state.count])
return <p>Count: {count}</p> 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 prefer regular React state. A local "show details" toggle is usually better as
`React.useState(false)` than as a Lens. `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 A `View<A>` is reactive state with a current value and a stream of changes. Use
changes. Use `Subscribable.useAll` whenever a component needs to bind `View.useAll` whenever a component needs to bind View values into render output.
subscribable 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. from a component.
```tsx ```tsx
import { Effect, SubscriptionRef } from "effect" import { Context, Effect, Layer, SubscriptionRef } from "effect"
import { Component, Lens, Subscribable } from "effect-fc" import { Component, Lens, View } from "effect-view"
class CounterState extends Effect.Service<CounterState>()("CounterState", { class CounterState extends Context.Service<
effect: Effect.gen(function* () { 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 count = Lens.fromSubscriptionRef(yield* SubscriptionRef.make(0))
const doubled = Subscribable.map(count, (n) => n * 2) const doubled = View.map(count, (n) => n * 2)
return { count, doubled } as const return { count, doubled } as const
}), }),
}) {} )
}
const CounterReadOnlyView = Component.make("CounterReadOnly")( const CounterReadOnlyView = Component.make("CounterReadOnly")(
function* () { function* () {
const state = yield* CounterState const state = yield* CounterState
const [count, doubled] = yield* Subscribable.useAll([ const [count, doubled] = yield* View.useAll([
state.count, state.count,
state.doubled, 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`: 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")( const CounterControlsView = Component.make("CounterControls")(
function* () { function* () {
const state = yield* CounterState const state = yield* CounterState
const [count] = yield* Subscribable.useAll([state.count]) const [count] = yield* View.useAll([state.count])
const increment = yield* Component.useCallbackSync( const increment = yield* Component.useCallbackSync(
() => Lens.update(state.count, (n) => n + 1), () => 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, setter, especially controlled inputs such as text fields, checkboxes, selects,
or third-party components with `value` / `onChange` props. 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 `Lens.useState` is for places where reading and writing need to be wired
together in React's local-state shape. together in React's local-state shape.
```tsx ```tsx
import { Effect, SubscriptionRef } from "effect" import { Context, Effect, Layer, SubscriptionRef } from "effect"
import { Component, Lens } from "effect-fc" import { Component, Lens } from "effect-view"
class FormState extends Effect.Service<FormState>()("FormState", { class FormState extends Context.Service<
effect: Effect.gen(function* () { FormState,
{ readonly name: Lens.Lens<string> }
>()("FormState") {
static readonly layer = Layer.effect(
FormState,
Effect.gen(function* () {
const name = Lens.fromSubscriptionRef(yield* SubscriptionRef.make("")) const name = Lens.fromSubscriptionRef(yield* SubscriptionRef.make(""))
return { name } as const return { name } as const
}), }),
}) {} )
}
const NameInputView = Component.make("NameInput")(function* () { const NameInputView = Component.make("NameInput")(function* () {
const state = yield* FormState 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 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 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. read/write tuple.
```tsx ```tsx
import { Effect, SubscriptionRef } from "effect" import { Context, Effect, Layer, SubscriptionRef } from "effect"
import { Component, Lens, Subscribable } from "effect-fc" import { Component, Lens, View } from "effect-view"
interface UserProfile { interface UserProfile {
readonly name: string readonly name: string
@@ -202,8 +221,17 @@ interface UserProfile {
readonly role: string readonly role: string
} }
class ProfileState extends Effect.Service<ProfileState>()("ProfileState", { class ProfileState extends Context.Service<
effect: Effect.gen(function* () { 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( const profile = Lens.fromSubscriptionRef(
yield* SubscriptionRef.make<UserProfile>({ yield* SubscriptionRef.make<UserProfile>({
name: "", name: "",
@@ -216,12 +244,13 @@ class ProfileState extends Effect.Service<ProfileState>()("ProfileState", {
return { profile, name, role } as const return { profile, name, role } as const
}), }),
}) {} )
}
const ProfileNameView = Component.make("ProfileName")(function* () { const ProfileNameView = Component.make("ProfileName")(function* () {
const state = yield* ProfileState const state = yield* ProfileState
const [name, setName] = yield* Lens.useState(state.name) const [name, setName] = yield* Lens.useState(state.name)
const [role] = yield* Subscribable.useAll([state.role]) const [role] = yield* View.useAll([state.role])
return ( return (
<label> <label>
@@ -238,7 +267,7 @@ const ProfileNameView = Component.make("ProfileName")(function* () {
Updating the focused `name` Lens through `Lens.useState` updates the parent 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 `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 For focusing into nested state, deriving lenses, custom write behavior, and the
complete API, refer to the complete API, refer to the