Structure
Middleware
The side-effecting half of a feature’s logic — a simplified Behavior that produces effects and supervises channels but never mutates state (that is Reducer’s job). Pair the two with Behavior(reducer:middleware:).
struct Middleware<Action, State, Environment> where Action : Sendable, State : Sendable, Environment : Sendable
Mentioned In
Overview
Middleware shares Behavior’s model: it is a monoid of Consequences, but only the effect-producing (.reaction with no mutation) and .supervision kinds. It receives every dispatched action with a PreReducerContext and returns a Reader<PostReducerContext<State, Environment>, Effect<Action>> the Store performs in phase 3.
Describe, don’t do
produce describes an effect; supervise describes the channels to keep. The Store performs and keeps them — the middleware itself runs nothing.
Composition & lifting
Middleware is a Semigroup and Monoid: combine(_:_:) concatenates consequence lists (effects merge and run concurrently, supervisions union). Use liftAction(_:) / liftState(_:) / liftEnvironment(_:) to embed a feature middleware in the app’s types.
The effect-only layer of a feature — a simplified Behavior that reads state and the environment and runs side-effects, but never mutates state. It carries both effect axes: action-driven produce(_:) and state-driven supervise(_:).
Overview
A Middleware<Action, State, Environment> owns the two effect concerns a feature can have, and only those — state mutation is Reducer’s job:
produce(_:) — the Effect Producer: maps an action (and a pre-mutation PreReducerContext) to a Reader<PostReducerContext, Effect> the Store performs in phase 3 (post-mutation), with access to the Environment and committed liveState. This is the action-driven effect.
supervise(_:) — the Effect Supervisor: maps the state to a Supervision — the channels to Keep alive while that state holds. The Store supervises them. This is the state-driven effect. See State-Driven Effects.
let search = Middleware<AppAction, AppState, API>
.produce { action, _ in
guard case .search(let query) = action else { return .doNothing }
return Producer { ctx in ctx.environment.search(query).asEffect(AppAction.results) }
}
.supervise { state in
Supervision { env in state.isConnected ? [env.makeFeedChannel()] : [] }
}
Both builders exist as static factories and instance methods, so a .produce { … }.supervise { … } chain is an <> fold of single-concern middlewares. Combine a Reducer with a Middleware to get a Behavior.
The two-phase context
handle receives the action plus a PreReducerContext — a @MainActor, deliberately non-Sendable snapshot of the pre-mutation world (stateBefore, source) — and returns a Reader<PostReducerContext, Effect> that the Store runs in phase 3, after the reducers. The compiler stops you from capturing the pre-context into the @Sendable phase-3 closure; copy context.stateBefore into a local let when you need a before/after comparison:
let logger = Middleware<AppAction, AppState, Logger>.handle { action, context in
let before = context.stateBefore // phase 1 — pre-mutation
return Producer { ctx in
ctx.environment.log(action, before: before) // phase 3 — runs with the environment
return .empty
}
}
PostReducerContext carries environment and the committed liveState (a @MainActor read; from a non-isolated context use await MainActor.run { ctx.liveState }). The reactive companion products — SwiftRex.Combine, SwiftRex.RxSwift, SwiftRex.ReactiveSwift, SwiftRex.ReactiveConcurrency — each add a readLiveState() extension on PostReducerContext returning a one-element stream that hops to @MainActor automatically, so post-mutation state can feed a pipeline:
return .produce { ctx in
ctx.readLiveState() // e.g. Publisher<State, Never>
.flatMap { state in ctx.environment.api.save(state.draft) }
.asEffect()
}
The state is read lazily — only when a subscriber attaches — so it always reflects the post-mutation value of the current dispatch cycle.
The algebra — a parallel monoid
Middleware is a Monoid: combine(_:_:) runs both middlewares on the same action and merges their effects, and unions their supervisors (the desired channel sets combine); identity produces no effect and keeps nothing. See The Algebra.
Scaling a feature up
A middleware written at local types is lifted to your app’s global types before composing, each lift naming its axes through a Relay.Scope leading-dot builder. A middleware only reads state — it never mutates — so the .state lane is a plain focus, not a write:
lift(_:) — all three axes in one scope: .action re-indexes, .state reads the slice, .environment narrows the world.
liftOptional — the 0-or-1 host: a state-only scope over an optional (or otherwise affine) slice, with action and environment left absent. While the focus is nil the middleware is skipped entirely (no effect produced); while present it reads the unwrapped value. A key-path spelling is sugar for the same call:
dayMiddleware.liftOptional(.state(\AppState.currentDay)) // currentDay: DayDetail.State?
dayMiddleware.liftOptional(\AppState.currentDay) // key-path sugar
liftCollection(action:embed:stateContainer:elements:) — route an addressed global action to one element the state lane locates (.state(\.rows), .state(\.rows, id: \.slug), .state(indexed: \.rows), .state(dictionary: \.configs)), observing that element’s unwrapped state:
rowMiddleware.liftCollection(
.action(AppAction.prism.row).state(\.rows).environment(\.rowEnv)
)
Every lift carries both effect axes — including supervise: a lifted middleware’s channels are re-embedded and (for collections) per-element stamped, so each element’s effect ids and supervision fan out per element automatically. Use the on(…) family to route by action case without a manual guard case — the same Prism / KeyPath / predicate families documented on Behavior, minus the reduce: parameter (a middleware never mutates).
Becoming a Behavior
asBehavior lifts a Middleware into a Behavior whose state mutation is always ReducerOutcome.unchanged — the bridge for combining pure effects with reducers under one type. Its supervise axis carries through unchanged.
Topics
Creating a Middleware
Composing
Lifting to a Larger Scope
func lift<A, S, E>(Relay.Scope<A, S, E>) -> Middleware<A.G, S.G, E.G>Lift this middleware into a global domain through a Relay.Scope — the single replacement for the three-axis lift(action:state:environment:) family. A middleware reads state (never mutates it), so it needs a duplex action lane, a reading state lane (ReadsProtocol), and a narrowing environment lane; it delegates to the per-axis primitives.
func liftAction<GlobalAction>(Prism<GlobalAction, Action>) -> Middleware<GlobalAction, State, Environment>Lifts the action axis of this middleware using a Prism, embedding it in a wider global action type.
func liftState(_:)Lifts the state axis of this middleware using a projection closure, embedding it in a wider global state type.
func liftEnvironment<GlobalEnvironment>((GlobalEnvironment) -> Environment) -> Middleware<Action, State, GlobalEnvironment>Lifts the environment axis of this middleware 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). Only the lens get is used.
func liftEach(action:embed:each:stateContainer:)Broadcasts across every focus of an IndexedTraversal (any container); each focus’s index is its scoping id. State container via Lens.
Bridging
Initializers
Instance Properties
Instance Methods
func liftCollection<A, S, E>(Relay.Scope<A, S, E>) -> Middleware<A.G, S.G, E.G>Route-to-one collection lift for a middleware — an addressed global action drives the one element the action lane locates; the middleware observes that element’s unwrapped state.
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>) -> Middleware<A.G, S.G, E.G>Broadcast collection lift for a middleware — observes every present element, re-embedding and per-element stamping each element’s emitted effects/channels.
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 liftOptional(_:)Lifts this middleware over an optional sub-state — the 0-or-1 sibling of liftCollection/liftEach (0-or-n).
func on<T>(Relay.ActionAxis.Extracts<Action, T>, when: ((State) -> Bool)?, dispatch: Relay.ActionAxis.Embeds<Action, T>, file: String, function: String, line: UInt) -> Middleware<Action, State, Environment>Route the trigger’s payload by embedding it into an outbound action, optionally guarded by when (which sits right after the trigger, gating the routing).
func produce((Action, PreReducerContext<State>) -> Reader<PostReducerContext<State, Environment>, Effect<Action>>) -> Middleware<Action, State, Environment>Adds an effect concern, combining it with self — m.produce { … } ≡ m <> .produce { … }.
func supervise((State) -> Supervision<Action, Environment>) -> Middleware<Action, State, Environment>Adds a state-driven concern, combining it with self — m.supervise { … } ≡ m <> .supervise { … }.
Type Methods
See Also
Related Documentation