Structure
Reducer
A pure function that calculates how state should change in response to an action.
struct Reducer<ActionType, StateType> where ActionType : Sendable, StateType : Sendable
Mentioned In
Overview
A Reducer is the only place in SwiftRex that is allowed to mutate State. It is completely pure — no side effects, no async work, no environment access. Side effects belong in Middleware and are expressed as Effect values returned from Behavior.
Internal representation
Internally a Reducer stores @Sendable (ActionType) -> EndoMut<StateType>: given an action, it returns an in-place endomorphism on State. This representation:
Avoids copying state on every action — mutations are applied directly via inout.
Makes the Monoid structure a direct, pointwise lift of EndoMut’s Monoid.
Keeps composition associative and free of allocation overhead.
The Store calls reducer.reduce(action).runEndoMut(&_state) during phase 2 of dispatch.
Constructors
Four factory overloads cover the most common use cases:
// 1. Idiomatic Swift — direct inout mutation (preferred for leaf reducers)
let counter = Reducer<CounterAction, Int>.reduce { action, state in
switch action {
case .increment: state += 1
case .decrement: state -= 1
case .reset: state = 0
}
}
// 2. From a pure (Action, State) -> State function
let toggle = Reducer<ToggleAction, Bool>.reduce { action, state in
switch action {
case .toggle: !state
}
}
// 3. From (Action) -> EndoMut<State> — for low-level composition
let raw = Reducer<MyAction, MyState>.reduce { action in
EndoMut { state in /* ... */ }
}
// 4. From (Action) -> Endo<State> — when the new state is a pure function
let endo = Reducer<MyAction, MyState>.reduce { action in
Endo { state in /* return new state */ }
}
Composition with @ReducerBuilder
let appReducer: Reducer<AppAction, AppState> = Reducer.compose {
authReducer.lift(.action(AppAction.prism.auth).state(\.authState))
profileReducer.lift(.action(AppAction.prism.profile).state(\.profileState))
}
Semigroup & Monoid
Reducer is a Semigroup under sequential composition and a Monoid with identity as the neutral element. combine(a, b) runs a’s mutation then b’s on the same inout State, so b observes a’s changes.
Note
Both ActionType and StateType are constrained to Sendable, matching the constraints on Behavior and Middleware. The stored reduce closure is @Sendable so Reducer itself satisfies Sendable without @unchecked.
The one place state changes — a pure function from an action to an in-place mutation.
Overview
A Reducer<ActionType, StateType> maps an action to an EndoMut<State> — an in-place endomorphism on your state. It is the only layer in SwiftRex allowed to mutate state, and it does so purely: no side effects, no async work, no environment. Effects live in Middleware; the two combine into a Behavior.
let counter = Reducer<CounterAction, Int>.reduce { action, state in
switch action {
case .increment: state += 1
case .decrement: state -= 1
case .reset: state = 0
}
}
Creating one
There are four reduce factories — pick whichever reads best; they all build the same value:
reduce { action, state in … } — the idiomatic inout form (zero-copy).
reduce { action, state in newState } — the pure-return form, converted to inout internally.
reduce { action in EndoMut { … } } — return an EndoMut directly; handy when composing endomorphisms or optimising a hot path.
reduce { action in Endo { … } } — the value-level Endo variant (bridges to EndoMut).
The algebra — a sequential monoid
Reducer is a Monoid. combine(_:_:) runs lhs then rhs on the same inout state (order matters; rhs sees lhs’s change); identity mutates nothing. This is precisely the structure of EndoMut’s monoid lifted over actions — see The Algebra. Build composite reducers with the @ReducerBuilder DSL:
let app = Reducer.compose {
counterReducer
historyReducer // runs after counterReducer, sees its mutation
}
Scaling a feature up
A reducer 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 reducer carries no environment, so only the .action and .state lanes matter:
lift(_:) — re-index the action (a Prism/\.case) and focus the state (a WritableKeyPath/Lens/AffineTraversal) in one scope.
liftCollection(_:) — route an addressed global action to one element of an Identifiable/custom-keyed collection or a dictionary; the state lane locates it (.state(\.rows), .state(\.rows, id: \.slug), .state(indexed: \.rows), .state(dictionary: \.configs)).
liftEach(_:) — the broadcast form: apply to every element, the action lane bridging a plain inbound prism into the per-element ElementAction.
let app = itemReducer.liftCollection(.action(AppAction.prism.row).state(\.rows))
In each case the lifted reducer sees the unwrapped element. There is no per-element effect stamping or supervision to carry — a reducer is pure — so these are the simplest of the collection hosts. A reducer has no 0-or-1 liftOptional host either (that lives on Behavior and Middleware): an absent focus would be a no-op mutation, which is already identity — reach for liftCollection(_:) or an AffineTraversal state lane on lift(_:) instead. See The Algebra for why lifting composes cleanly, and ElementAction for how element actions are addressed.
Becoming a Behavior
asBehavior() turns a Reducer into a Behavior whose effect is always empty — the bridge for combining pure reducers with effectful middleware under one type.
Topics
Creating a Reducer
Composing
Lifting to a Larger Scope
Bridging
Instance Properties
Instance Methods
func liftCollection<A, S, E>(Relay.Scope<A, S, E>) -> Reducer<A.G, S.G>Route-to-one collection lift: an addressed global action drives the one element the action lane locates. The lifted reducer 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>) -> Reducer<A.G, S.G>Broadcast collection lift: a global action drives every present element.
func liftEach(action:stateCollection:)Broadcasts to every element of a mutable collection. State container via Lens.
func liftEach(action:stateDictionary:)Broadcasts to every value of a dictionary. State container via Lens.
Type Methods
Relationships
Conforms To
See Also
Related Documentation