Class
Store
The sole owner of mutable State and the central coordinator of the three-phase dispatch pipeline.
@MainActor final class Store<Action, State, Environment> where Action : Sendable, State : Sendable, Environment : Sendable
Mentioned In
Overview
Create one Store per application and pass it — or narrowed StoreProjection values — to your features. The Store is @MainActor, so all state reads, mutations, and observer notifications happen on the main thread without any manual actor-hopping.
Dispatch pipeline
Every dispatched action runs through these steps in runPhases:
1. behavior.handle(action, stateAccess) — all Behaviors; stateAccess = pre-mutation state
2. stateObservers.willChange fired — ObservableObject fires objectWillChange here
consequence.mutation.runEndoMut(&state) — zero-copy inout; refcount stays at 1
stateObservers.didChange fired — @Observable / push-based observers
3. consequence.produce.runReader(env) — Reader runs; stateAccess = post-mutation state
4. engine.schedule(component) per component — action-driven effects (produce / Cmd)
5. engine.reconcile(behavior.supervisor(state)) — state-driven channels (Sub); only if state changed
Grouping all willChange notifications before the mutation (and all didChange notifications after) means observers never see a partially-mutated state. withAnimation { store.dispatch(...) } works correctly because SwiftUI animation transactions are thread-local and the mutation lands on @MainActor inside the transaction.
Serialized, never-nested processing
Actions are serialized through a FIFO queue guarded by an isProcessing flag, so runPhases never runs nested. A synchronous dispatch(_:source:) runs its action — and any actions dispatched synchronously while it runs (for example a didChange observer that re-dispatches) — within the same run loop turn, draining the queue in order. Because the pipeline is never re-entered mid-action, observers always see a fully-committed state.
Actions produced by effects re-enter through a Task hop (see makeSend), so they are always processed on a later turn rather than inline — keeping effect work off the run loop turn that drove the original UI dispatch.
Initialisation
Three convenience initialisers cover common patterns:
// Full form: custom Behavior, any Environment
let store = Store(
initial: AppState.initial,
behavior: appBehavior,
environment: AppEnvironment.live
)
// Reducer only (Environment == Void, no side effects)
let store = Store(
initial: CounterState(),
reducer: counterReducer
)
// Separate Reducer + Middleware (composed internally)
let store = Store(
initial: AppState.initial,
reducer: appReducer,
middleware: appMiddleware,
environment: AppEnvironment.live
)
Effect lifecycle
Scheduling is delegated to a shared EffectEngine — the same engine TestStore drives with a controllable clock. It tracks each running Effect.Component keyed by id (or a fresh anonymous key for id-less components), honouring delay, coalesce (debounce/throttle), exclusive (replace), the cancel-only sentinel, and the pipeable channel path.
SubscriptionToken cancels on release, so the engine’s registry behaves like a Set<AnyCancellable>: replacing the token under a key cancels the effect it displaced, and deallocating the Store releases the engine — cancelling every in-flight effect — so no explicit deinit is needed.
Dispatching from inside an effect re-enters the pipeline on @MainActor via Task, and the isProcessing guard defers it behind any in-progress action, so reentrancy is always safe — effects never interleave with, or nest inside, an in-progress dispatch.
Note
@unchecked Sendable is used because the mutable stored properties (state, the lazy engine, observers, etc.) are only accessed on @MainActor, but Swift cannot statically prove this for a final class without nonisolated(unsafe) annotations on each property.
The single interpreter — the only place state mutates and effects run.
Overview
Store<Action, State, Environment> owns your app’s state and is the sole executor of effects. You dispatch actions; for each one it runs the Behavior in three phases — compute the Consequence against pre-mutation state, apply the mutation in place (bracketed by willChange/didChange notifications), then resolve and schedule the effect against post-mutation state. Actions produced by effects loop back. Its whole surface is @MainActor, so withAnimation { store.dispatch(...) } works with no special API.
The Store is the interpreter for the inert values the rest of the library builds (IO at the program’s edge). See The Algebra for the guarantees this yields: one notification per state-changing action, zero-copy mutation, committed-state effects, and FIFO-safe re-entrancy.
Creating one
let store = Store(initial: AppState(), behavior: appBehavior, environment: env)
When Environment == Void a convenience initialiser omits it; another accepts an injected Clock/Date/UUID for deterministic time in tests.
Dispatching & observing
dispatch(_:source:) enqueues an action (synchronous from @MainActor). observe(willChange:didChange:) registers a @MainActor observer and returns a SubscriptionToken you must retain — releasing it cancels the observation.
Narrowing for views
projection(action:state:) maps the store to a local action/state slice (a stateless StoreProjection); buffer() wraps it in a deduplicating StoreBuffer. Run-away dispatch loops are cut off via StoreHooks.
Topics
Dispatching
Observing
Narrowing for Views
Diagnostics
Initializers
convenience init(initial: State, behavior: Behavior<Action, State, Environment>)Creates a Store from a Behavior when the environment is Void.
convenience init(initial: State, behavior: Behavior<Action, State, Environment>, environment: Environment)Creates a Store with a pre-composed Behavior and an environment, driving EffectScheduling timing with a live ContinuousClock.
init<C>(initial: State, behavior: Behavior<Action, State, Environment>, environment: Environment, clock: (Environment) -> C)Creates a Store extracting the scheduling clock — and optionally the RNG — from the environment.
convenience init(initial: State, reducer: Reducer<Action, State>)Creates a Store with a Reducer only (no side effects, Environment == Void).
convenience init(initial: State, reducer: Reducer<Action, State>, middleware: Middleware<Action, State, Environment>, environment: Environment)Creates a Store by composing a Reducer and a Middleware internally.
Instance Properties
Relationships
Conforms To
See Also
Related Documentation