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 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 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:
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:
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.
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:
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.
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.
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.