Add Effect v4 support, Fast Refresh tooling, and revamped docs (#56)
## Summary - Add `effect-fc-next`, a React 19 integration targeting Effect v4 beta. - Introduce component lifecycles, scoped resources, queries, mutations, forms, lenses, views, and refreshable components. - Add `@effect-view/vite-plugin` for Vite Fast Refresh support. - Add an Effect v4 example application covering the new APIs. - Expand test coverage for both the existing and next-generation packages. - Replace the starter Docusaurus content with complete Effect View documentation while preserving the Effect v3 docs as a versioned snapshot. - Update the landing page, navigation, package scripts, and build output. - Extend CI with linting, tests, package builds, Docker builds, and container publishing. ## Validation - `bun lint:tsc` - `bun lint:biome` - `bun test` - `bun run build` - `bun pack` - Docker image build --------- Co-authored-by: Julien Valverdé <julien.valverde@mailo.com> Reviewed-on: Thilawyn/effect-fc#56
This commit was merged in pull request #56.
This commit is contained in:
@@ -0,0 +1,245 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
title: State Management
|
||||
---
|
||||
|
||||
# State Management
|
||||
|
||||
`Lens` is the main type used for state management in `effect-fc`.
|
||||
|
||||
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
|
||||
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`.
|
||||
|
||||
`Subscribable` is the read-only side of this model. Every Lens is also a
|
||||
Subscribable.
|
||||
|
||||
`effect-fc` re-exports the `Lens` and `Subscribable` 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.
|
||||
|
||||
## Where To Store State
|
||||
|
||||
State can live pretty much anywhere as a `Lens` or `Subscribable`: 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
|
||||
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"
|
||||
|
||||
class CounterState extends Effect.Service<CounterState>()("CounterState", {
|
||||
effect: Effect.gen(function* () {
|
||||
const count = Lens.fromSubscriptionRef(yield* SubscriptionRef.make(0))
|
||||
|
||||
return { count } as const
|
||||
}),
|
||||
}) {}
|
||||
|
||||
const CounterValueView = Component.make("CounterValue")(function* () {
|
||||
const state = yield* CounterState
|
||||
const [count] = yield* Subscribable.useAll([state.count])
|
||||
|
||||
return <p>Count: {count}</p>
|
||||
})
|
||||
```
|
||||
|
||||
If state belongs to a single Effect-FC 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"
|
||||
|
||||
const LocalCounterView = Component.make("LocalCounter")(function* () {
|
||||
const state = yield* Component.useOnMount(() =>
|
||||
Effect.gen(function* () {
|
||||
const count = Lens.fromSubscriptionRef(yield* SubscriptionRef.make(0))
|
||||
|
||||
return { count } as const
|
||||
}),
|
||||
)
|
||||
const [count] = yield* Subscribable.useAll([state.count])
|
||||
|
||||
return <p>Count: {count}</p>
|
||||
})
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
`Lens` is a `Subscribable`, 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"
|
||||
|
||||
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)
|
||||
|
||||
return { count, doubled } as const
|
||||
}),
|
||||
}) {}
|
||||
|
||||
const CounterReadOnlyView = Component.make("CounterReadOnly")(
|
||||
function* () {
|
||||
const state = yield* CounterState
|
||||
const [count, doubled] = yield* Subscribable.useAll([
|
||||
state.count,
|
||||
state.doubled,
|
||||
])
|
||||
|
||||
return <p>Count: {count}, doubled: {doubled}</p>
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
`Subscribable.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`:
|
||||
|
||||
```tsx
|
||||
const CounterControlsView = Component.make("CounterControls")(
|
||||
function* () {
|
||||
const state = yield* CounterState
|
||||
const [count] = yield* Subscribable.useAll([state.count])
|
||||
|
||||
const increment = yield* Component.useCallbackSync(
|
||||
() => Lens.update(state.count, (n) => n + 1),
|
||||
[],
|
||||
)
|
||||
const reset = yield* Component.useCallbackSync(
|
||||
() => Lens.set(state.count, 0),
|
||||
[],
|
||||
)
|
||||
|
||||
return (
|
||||
<section>
|
||||
<p>Count: {count}</p>
|
||||
<button onClick={increment}>Increment</button>
|
||||
<button onClick={reset}>Reset</button>
|
||||
</section>
|
||||
)
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
## Lens.useState
|
||||
|
||||
`Lens.useState` is useful when React needs the familiar `[value, setValue]`
|
||||
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`.
|
||||
`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"
|
||||
|
||||
class FormState extends Effect.Service<FormState>()("FormState", {
|
||||
effect: Effect.gen(function* () {
|
||||
const name = Lens.fromSubscriptionRef(yield* SubscriptionRef.make(""))
|
||||
|
||||
return { name } as const
|
||||
}),
|
||||
}) {}
|
||||
|
||||
const NameInputView = Component.make("NameInput")(function* () {
|
||||
const state = yield* FormState
|
||||
const [name, setName] = yield* Lens.useState(state.name)
|
||||
|
||||
return (
|
||||
<input
|
||||
value={name}
|
||||
onChange={(event) => setName(event.currentTarget.value)}
|
||||
/>
|
||||
)
|
||||
})
|
||||
```
|
||||
|
||||
`Lens.useState` returns the current value and a React-compatible setter. Calling
|
||||
the setter writes through the Lens, so every other component subscribed to the
|
||||
same Lens sees the update.
|
||||
|
||||
## Focused Lenses
|
||||
|
||||
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
|
||||
read/write tuple.
|
||||
|
||||
```tsx
|
||||
import { Effect, SubscriptionRef } from "effect"
|
||||
import { Component, Lens, Subscribable } from "effect-fc"
|
||||
|
||||
interface UserProfile {
|
||||
readonly name: string
|
||||
readonly email: string
|
||||
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")
|
||||
|
||||
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])
|
||||
|
||||
return (
|
||||
<label>
|
||||
Name
|
||||
<input
|
||||
value={name}
|
||||
onChange={(event) => setName(event.currentTarget.value)}
|
||||
/>
|
||||
<span>Role: {role}</span>
|
||||
</label>
|
||||
)
|
||||
})
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
For focusing into nested state, deriving lenses, custom write behavior, and the
|
||||
complete API, refer to the
|
||||
[`effect-lens` documentation](https://github.com/Thiladev/effect-lens/tree/master/packages/effect-lens).
|
||||
Reference in New Issue
Block a user