Add Effect v4 support, Fast Refresh tooling, and revamped docs #56
+45
-51
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user