Structure
Behavior
The primary composition unit: a pure description of how a feature responds to actions and to state changes.
struct Behavior<Action, State, Environment> where Action : Sendable, State : Sendable, Environment : Sendable
Mentioned In
Overview
A Behavior is a monoid of Consequences — literally [Consequence], with the empty list the identity and concatenation the composition. Each consequence is either a reaction to an action (a Reaction — reduce and/or produce) or a supervision of state (a Supervision — the channels to keep). Behavior unifies the jobs of Reducer (state mutation), Middleware (action-driven effects), and state-driven channels in one value.
Describe, don’t do — the purity boundary
A Behavior only describes: reduce describes a mutation, produce describes an effect, supervise describes the channels to keep. The Store is the boundary that does: it mutates, performs, and keeps. Nothing in a behavior runs an effect.
Two clocks
When the Store receives an action it processes the .reaction consequences (the action clock) in three globally-ordered phases; after every state change it reconciles the .supervision consequences (the state clock):
Phase 1 handle(action, preCtx) — fold all reactions, pre-mutation state
Phase 2 reaction.mutation.runEndoMut(&state) — all mutations, zero-copy inout
Phase 3 reaction.produce.runReader(postCtx) — all effects, post-mutation state
Phase 5 reconcile(supervisor(state)) — state-driven channels, diffed
Because every reaction in phase 1 sees the same pre-mutation state, composing two behaviors with combine(_:_:) gives them identical views — neither can depend on the other’s mutations.
Creating a Behavior
// Grouped action builder — react, with reduce and/or produce inside
let loader = Behavior<AppAction, AppState, AppEnvironment>.react { action, _ in
guard case .load(let id) = action else { return .doNothing }
return .reduce { $0.isLoading = true }
.produce { ctx in ctx.environment.api.fetch(id).asEffect() }
}
// Fluent — each concern combined into one behavior
let feature = Behavior<AppAction, AppState, AppEnvironment>
.reduce { action, state in /* … */ }
.produce { action, ctx in Reader { c in /* … */ } }
.supervise { state in Supervision { env in /* … */ } }
// From a Reducer, or a Reducer + Middleware
let counter = counterReducer.asBehavior()
let auth = Behavior(reducer: authReducer, middleware: authMiddleware)
Composition & lifting
Behavior is a Semigroup and Monoid: combine(_:_:) concatenates the consequence lists, so both behaviors’ reactions run in phase 1 (same pre-mutation state), their mutations sequence (lhs then rhs), their effects run concurrently, and their supervisions union. Use the lift family (liftAction(_:), liftState(_:), liftEnvironment(_:), lift(_:)) to embed feature behaviors in the app’s global types.
Note
handle is @MainActor. The Store calls it on the main actor, so context.stateBefore is safe to call directly inside the closure.
The primary composition unit — a monoid of Consequences fusing a Reducer, a Middleware, and state-driven supervision into one liftable, composable value.
Overview
A Behavior<Action, State, Environment> is [Consequence]. Three fluent builders describe a feature’s concerns, each composing by <>:
reduce(_:) — the Reducer: a pure (Action, inout State) -> Void. The Store maintains the state.
produce(_:) — the Effect Producer: an action produces an Effect. The Store performs it.
supervise(_:) — the Effect Supervisor: the state keeps a Supervision of Channels alive. The Store supervises them. See State-Driven Effects.
let room = Behavior<RoomAction, RoomState, RoomEnv>
.reduce { action, state in … } // what changes
.produce { action, ctx in … } // what to do because of an action
.supervise { state in … } // what to keep alive while the state holds
Each builder exists as a static factory (Behavior.reduce { … }) and as an instance method (someBehavior.produce { … }), so a fluent chain is exactly an <> fold. To share pre-work between a mutation and its effect, use the grouped react(_:) builder — it hands you the action and returns a whole Reaction:
Behavior.react { action, _ in
guard case .load(let id) = action else { return .doNothing }
return .reduce { $0.isLoading = true }
.produce { ctx in ctx.environment.api.fetch(id).asEffect() }
}
You can also pair the reducer and middleware axes with Behavior(reducer:middleware:); asBehavior() and asBehavior lift each half on its own (a Middleware’s own supervise axis carries through).
The algebra — the free monoid [Consequence]
Behavior is a Monoid — literally the free monoid over its consequences: combine(_:_:) concatenates the lists, identity is []. Composing runs both behaviors’ reactions on the same pre-mutation state (mutations fold sequentially, effects merge in parallel — each Reaction is a product monoid) and unions their supervisions. It is a single flat pass, not a nested closure tree, and an all-no-op fold stays ReducerOutcome.unchanged so the Store skips the notification entirely. See The Algebra.
let app = Behavior.combine(counter.lifted, profile.lifted) // or counter.lifted <> profile.lifted
Scaling a feature up
lift(_:) raises a feature from its local types to the app’s global types in one shot: a Relay.Scope names all three axes through a leading-dot builder — .action re-indexes the action (a Prism/\.case), .state focuses the slice (a WritableKeyPath/Lens/AffineTraversal), .environment narrows the world.
let lifted = room.lift(.action(AppAction.prism.room).state(\.room).environment(\.roomEnv))
liftOptional is the 0-or-1 host: a state-only scope over an optional (or otherwise affine) slice, with the action and environment axes left absent. While the focus is nil the behavior is a complete no-op — never asked to mutate, produce, or supervise (stricter than a plain affine state lift); while present it runs on the unwrapped value. A key-path spelling is sugar for the same call:
dayBehavior.liftOptional(.state(\AppState.currentDay)) // currentDay: DayDetail.State?
dayBehavior.liftOptional(\AppState.currentDay) // key-path sugar
liftCollection(_:) routes an addressed global action to one element of a collection. The state lane locates it — by Identifiable id (.state(\.rows)), a custom key (.state(\.rows, id: \.slug)), position (.state(indexed: \.rows)), or dictionary key (.state(dictionary: \.configs)) — while the action lane carries an ElementAction:
rowBehavior.liftCollection(
.action(AppAction.prism.row).state(\.rows).environment(\.rowEnv)
)
liftEach(_:) is the broadcast form: one global action reaches every present element, the action lane bridging a plain inbound prism into the per-element ElementAction:
rowBehavior.liftEach(
.action(broadcast: AppAction.prism.tickAll, into: AppAction.prism.row)
.state(\.rows).environment(\.rowEnv)
)
Every lift carries all three axes — including supervise: a lifted feature’s channels are re-embedded and (for collections) per-element stamped, so state-driven nav and per-row sockets just work. In all four the lifted unit sees the unwrapped local value, and each element’s effect ids and supervision fan out per element automatically. See Lifting Features.
Routing actions — the .on(…) bridge
.on composes declarative action-routing onto any behavior: when this action arrives, dispatch that one — optionally co-locating a state mutation (reduce:). Every .on is combine(self, routingBehavior). It speaks the same axis vocabulary as every other host: a trigger (.action(…) — an Extracts that previews the payload) and, to route, a dispatch (.action(…) embed). An optional when guard sits right after the trigger, because it gates the whole routing — both the dispatch and the reduce:
let behavior = Behavior<AppAction, AppState, World>.identity
// route the extracted payload to another action
.on(.action(\.didLoad), dispatch: .action(\.renderItems))
// + a state guard (right after the trigger — it gates dispatch and reduce)
.on(.action(\.didTapBuy), when: { $0.isLoggedIn }, dispatch: .action(\.checkout))
// + a co-located mutation
.on(.action(\.didLoad), dispatch: .action(\.renderItems),
reduce: { items, state in state.items = items; state.isLoading = false })
// transform the payload — a trailing closure is the embed (the `review:` label is implicit)
.on(.action(\.didSearch), dispatch: .action { AppAction.performSearch($0) })
// react by mutating state only, no dispatch
.on(.action(\.reset), reduce: { _, state in state = .init() })
// a bool test with no payload → extract Void, guard, then route
.on(.action { if case .submit = $0 { () } else { nil } },
when: { !$0.isSubmitting }, dispatch: .action(\.doSubmit))
The trigger reads with any .action(…) strategy (\.case / prism / a preview closure); the dispatch embeds with any (\.case / prism / a review closure) — and a trailing closure drops the review:/preview: label, so a transform is simply .action { … }. Both reduce and when are optional.
State is never copied unless the action filter passes first. Variants without reduce: and without when: use mutation: .identity — no inout reference to state is ever taken, guaranteeing zero copy-on-write interaction. The bridge is also the tool for decoupled cross-feature communication: route one module’s output action into another module’s input without either module importing the other — see Modularisation & Features.
Topics
Building a Behavior
Composing
Lifting to a Larger Scope
func lift<A, S, E>(Relay.Scope<A, S, E>) -> Behavior<A.G, S.G, E.G>Lift this behavior into a global domain through a Relay.Scope — the single replacement for the three-axis lift(action:state:environment:) family. Needs the action lane to be duplex (ExtractsProtocol & EmbedsProtocol — extract inbound global actions, re-embed emitted ones), the state lane to write (WritesProtocol, total or affine), and the environment lane to narrow (NarrowsProtocol). It delegates to the per-axis primitives, reconstructing a Prism and an AffineTraversal from the lane witnesses (the affine form covers total state too).
func liftAction(_:)Lifts the action axis using a \.case key path (PrismKeyPath) — the case-key-path spelling of liftAction(_:).
func liftState(_:)Lifts the state axis of this behavior using a WritableKeyPath, embedding it in a wider global state type.
func liftEnvironment<GlobalEnvironment>((GlobalEnvironment) -> Environment) -> Behavior<Action, State, GlobalEnvironment>Lifts the environment axis of this behavior using a projection closure, embedding it in a wider global environment.
func liftCollection(action:embed:stateContainer:elements:)Primitive — closure-driven extraction plus a Lens state container (for composed or let-property containers).
func liftEach(action:embed:each:stateContainer:)Broadcasts across every focus of an IndexedTraversal (any container, not just collections); each focus’s index is its scoping id. State container via Lens.
Initializers
Instance Properties
Instance Methods
func liftCollection<A, S, E>(Relay.Scope<A, S, E>) -> Behavior<A.G, S.G, E.G>Route-to-one collection lift: an addressed global action drives one element, located by the id the action lane extracts. The lifted behavior sees the unwrapped element.
func liftCollection(action:stateCollection:)Lifts to a mutable collection, locating the element by its Identifiable.id. State container via Lens.
func liftCollection(action:stateCollection:identifier:)Lifts to a mutable collection, locating the element by a custom Hashable field. State container via Lens.
func liftCollection(action:stateDictionary:)Lifts to a Dictionary, locating the entry by its key. State container via Lens.
func liftEach<A, S, E>(Relay.Scope<A, S, E>) -> Behavior<A.G, S.G, E.G>Broadcast collection lift: a global action is delivered to every present element, and each element’s emitted actions/effects are re-embedded and stamped at its own id.
func liftEach(action:embed:ids:element:stateContainer:)Primitive — broadcast across an enumerated, addressable container, with a Lens container.
func liftEach(action:embed:stateCollection:)Broadcasts to every element of a mutable collection, keyed by Identifiable.id. State container via Lens.
func liftEach(action:embed:stateCollection:identifier:)Broadcasts to every element of a mutable collection, keyed by a custom Hashable field. State container via Lens.
func liftEach(action:embed:stateDictionary:)Broadcasts to every value of a dictionary, keyed by its key. State container via Lens.
func liftEnvironment<GlobalEnvironment>() -> Behavior<Action, State, GlobalEnvironment>Widens the environment axis from Void to any GlobalEnvironment, discarding the global environment entirely.
func liftOptional(_:)Lifts this behavior over an optional sub-state — the 0-or-1 sibling of liftCollection and liftEach (which are 0-or-n).
func on(_:reduce:)Matches a key path (Void case) and runs a state mutation with no dispatch.
func on(_:reduce:when:)Matches a key path (Void case) and runs a state mutation with no dispatch, guarded by a predicate.
func on<T>(Relay.ActionAxis.Extracts<Action, T>, when: ((State) -> Bool)?, dispatch: Relay.ActionAxis.Embeds<Action, T>, reduce: ((T, inout State) -> Void)?, file: String, function: String, line: UInt) -> Behavior<Action, State, Environment>Route the trigger’s payload by embedding it into an outbound action (.action(…)), optionally mutating state (reduce:). The optional when guard gates the whole routing — dispatch and reduce alike — which is why it sits right after the trigger.
func on<T>(Relay.ActionAxis.Extracts<Action, T>, when: ((State) -> Bool)?, reduce: (T, inout State) -> Void) -> Behavior<Action, State, Environment>React to the trigger by mutating state only — no action is dispatched — optionally guarded by when (which sits right after the trigger, gating the reaction).
func produce((Action, PreReducerContext<State>) -> Reader<PostReducerContext<State, Environment>, Effect<Action>>) -> Behavior<Action, State, Environment>Adds an effect concern, combining it with self — b.produce { … } ≡ b <> .produce { … }.
func react((Action, PreReducerContext<State>) -> Reaction<Action, State, Environment>) -> Behavior<Action, State, Environment>Adds a grouped action concern, combining it with self — b.react { … } ≡ b <> .react { … }.
func reduce((Action, inout State) -> Void) -> Behavior<Action, State, Environment>Adds a mutation concern, combining it with self — b.reduce { … } ≡ b <> .reduce { … }.
func supervise((State) -> Supervision<Action, Environment>) -> Behavior<Action, State, Environment>Adds a state-driven concern, combining it with self — b.supervise { … } ≡ b <> .supervise { … }.
Type Methods
static func combine([Behavior<Action, State, Environment>]) -> Behavior<Action, State, Environment>Folds an array of behaviors into one, in order, using the Behavior monoid (identity as the empty element, combine(_:_:) as the binary operation).
static func produce((Action, PreReducerContext<State>) -> Reader<PostReducerContext<State, Environment>, Effect<Action>>) -> Behavior<Action, State, Environment>An effect-only behavior — produces an effect in reaction to an action, no mutation. The closure returns the Reader<PostReducerContext, Effect> the Store performs in phase 3.
static func react((Action, PreReducerContext<State>) -> Reaction<Action, State, Environment>) -> Behavior<Action, State, Environment>The grouped action builder — react to an action with a Reaction (a reduce and/or produce). Pre-work shared by both halves lives in the closure body:
static func reduce((Action, inout State) -> Void) -> Behavior<Action, State, Environment>A mutation-only behavior — reduces state per action, no effects.
static func supervise((State) -> Supervision<Action, Environment>) -> Behavior<Action, State, Environment>A state-driven behavior — the supervise (Sub) side, with no action handling.
Relationships
Conforms To
See Also
Related Documentation