Files
effect-fc/packages/docs/docs/state-management.md
T
Julien Valverdé 436dc275b3
Lint / lint (push) Successful in 16s
Docs
2026-06-09 20:34:24 +02:00

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:

  1. Create state with Effect primitives such as SubscriptionRef.
  2. Turn the Effect primitive into a Lens with the matching constructor, such as Lens.fromSubscriptionRef.
  3. Expose read-only reactive state as a Subscribable.
  4. Expose read/write reactive state as a Lens.
  5. 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.