5.8 KiB
sidebar_position, title
| sidebar_position | title |
|---|---|
| 2 | State Management |
State Management
effect-fc works well with state that lives in Effect services, layers, and
scopes. The usual pattern is:
- Create state with Effect primitives such as
SubscriptionRef. - Turn the Effect primitive into a
Lenswith the matching constructor, such asLens.fromSubscriptionRef. - Expose read-only reactive state as a
Subscribable. - Expose read/write reactive state as a
Lens. - Bind values into components with
Subscribable.useAll.
effect-fc re-exports the Lens and Subscribable modules from
effect-lens for convenience. The
core data model and transformation APIs belong to effect-lens, so check the
effect-lens documentation for the full Lens/Subscribable API.
Where To Store State
State can live pretty much anywhere: in a service, in a layer, in a component scope, or in plain React state. Pick the owner based on who needs the state.
If state is shared by multiple components or belongs to application logic, store it in an Effect service:
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 but still benefits
from Lens/Subscribable APIs, create it with Component.useOnMount:
import { Effect, SubscriptionRef } from "effect"
import { Component, Lens, Subscribable } from "effect-fc"
const LocalCounterView = Component.make("LocalCounter")(function* () {
const countLens = yield* Component.useOnMount(() =>
Effect.map(SubscriptionRef.make(0), Lens.fromSubscriptionRef),
)
const [count] = yield* Subscribable.useAll([countLens])
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.
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 the state is a Lens, keep binding it with Subscribable.useAll and
write to it with Lens.set or Lens.update from an Effect-FC callback:
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),
[state.count],
)
const reset = yield* Component.useCallbackSync(
() => Lens.set(state.count, 0),
[state.count],
)
return (
<section>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
<button onClick={reset}>Reset</button>
</section>
)
},
)
Lenses
A Lens<A> is a Subscribable<A> that can also be written to. If a component
only needs to display the value, keep using Subscribable.useAll.
Use Lens.useState when React needs a read/write tuple, especially for inputs
that require synchronous updates such as controlled text inputs.
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.
Choosing One
Use Subscribable.useAll when render needs to read values:
const [count, doubled] = yield* Subscribable.useAll([
state.count,
state.doubled,
])
Use Lens.useState when JSX needs both the current value and a synchronous
setter:
const [name, setName] = yield* Lens.useState(state.name)
For focusing into nested state, deriving lenses, custom write behavior, and the
complete API, refer to the
effect-lens documentation.