Structure
Effect
A lazy, push-based description of side-effectful work that produces zero or more actions.
struct Effect<Action> where Action : Sendable
Mentioned In
Overview
Effect<Action> is the currency of async work in SwiftRex. It is a pure value that describes work without starting it — the Store is the sole executor. This design makes effects trivially testable (just inspect the returned value) and composable via the Monoid instance.
Push-based, not AsyncStream
Effect uses a closure-based, push-based model rather than Swift’s AsyncStream. This choice has concrete consequences:
Lazy: subscribing the closure starts the work; holding the value does nothing.
Multi-consumer: the same Effect value can be subscribed multiple times, each subscription getting its own independent stream of values.
Cross-platform: the model works on Linux, Windows, and WASM where Swift Concurrency may be unavailable or constrained.
Framework-agnostic: bridge targets (SwiftRexCombine, SwiftRexRxSwift, SwiftRexReactiveSwift, SwiftRexReactiveConcurrency) wrap their respective publisher/observable/signal types into this single representation.
The subscribe closure
Each component’s subscribe closure receives two callbacks and returns a SubscriptionToken:
send: called for each action produced. The DispatchedAction carries the original call-site provenance so logging middleware sees where each action originated.
complete: called exactly once when the component is done producing values. Cancelled components must not call complete.
The returned SubscriptionToken lets the Store cancel the subscription.
Array-of-components internal representation
Combining two effects (via combine(_:_:) or the Monoid instance) appends their component arrays. The Store schedules each component independently under its own EffectScheduling policy, so cancelling by id never affects components with a different id.
Scheduling
Apply scheduling(_:) to override the default immediately policy on all components at once, or build components with individual policies using the Effect.init(subscribe:scheduling:) SPI. For most production code, use the dedicated factories (just(_:scheduling:file:function:line:), sequence(_:scheduling:file:function:line:)).
Composing effects
// Two effects run concurrently — appends their component arrays
let combined = Effect.combine(fetchUsers, fetchSettings)
// Chain via Monoid fold
let all: Effect<AppAction> = mconcat([fetchUsers, fetchSettings, trackEvent])
// Map to change the action type (Functor)
let mapped = userEffect.map { AppAction.users($0) }
Note
Creating an Effect starts no work. Only the Store calls subscribe.
A lazy, framework-agnostic description of side-effecting work — it runs only when the Store executes it.
Overview
An Effect<Action> is the unit of side effect a Middleware (or a Behavior) returns. It is inert until run: constructing one starts nothing — no Task, no network call — until the Store subscribes to it. That is what keeps the reducer/behavior layer pure. An effect produces zero or more actions that loop back into the Store.
Effect exposes no Task, AsyncStream, Publisher, or Observable in its API — those live in the companion products (SwiftRex.SwiftConcurrency, SwiftRex.Combine, SwiftRex.RxSwift, SwiftRex.ReactiveSwift, SwiftRex.ReactiveConcurrency), each adding asEffect() bridges.
Creating one
Recreate vs. pipe
The factories above are recreate-on-dispatch: each dispatch starts fresh work, and an id-scoped EffectScheduling (replace / debounce / throttle) cancels the displaced run. That is the right model for one-shot work — a search request, a save.
For long-lived work that must not be torn down — a WebSocket, a location or audio pipeline — use channel(value:scheduling:file:function:line:_:). The Store opens it once and pipes every subsequent value into the same running effect, with debounce/throttle gating the value delivery rather than the effect’s lifetime. See ChannelHandler.
Usually you don’t open a channel imperatively from an effect — you declare it from state with supervise and let the engine open and cancel it as the state changes. The Effect entry points here (open(_:), broadcast(_:channel:file:function:line:), cancel(id:)) are the action-driven complements: send into a live channel, or own its lifetime by hand. See State-Driven Effects and Channels.
Scheduling
Attach an EffectScheduling policy with scheduling(_:): run immediately, or under an id so the Store can debounce, throttle, replace, or cancel it in flight. The mutable bookkeeping (timers, in-flight handles) lives in the Store, never in the effect.
api.search(query).asEffect(AppAction.results)
.scheduling(.debounce(id: "search", delay: .milliseconds(300)))
The algebra — a parallel monoid
Effect is a Monoid: combine(_:_:) merges two effects so the Store runs them concurrently, and empty is the identity. It is a Functor via map(_:). See The Algebra.
Topics
Creating an Effect
static func just(Action, scheduling: EffectScheduling, file: String, function: String, line: UInt) -> Effect<Action>Creates an effect that dispatches a single action and immediately signals completion.
static func sequence([Action], scheduling: EffectScheduling, file: String, function: String, line: UInt) -> Effect<Action>Creates an effect that dispatches a sequence of actions in order, then signals completion.
static var empty: Effect<Action>An effect that does nothing — no actions produced, no work started.
Pipeable channels
static func channel<Value>(value: Value, scheduling: EffectScheduling, file: String, function: String, line: UInt, ((Action) -> Void, () -> Void) -> ChannelHandler<Value>) -> Effect<Action>Creates a pipeable effect: a long-lived unit of work the Store starts once and then feeds subsequent values into, rather than cancelling and recreating on every dispatch.
static func channel<Value>(value: Value, scheduling: EffectScheduling, bufferingPolicy: AsyncStream<Value>.Continuation.BufferingPolicy, file: String, function: String, line: UInt, consume: (AsyncStream<Value>, (Action) -> Void, () -> Void) async -> Void) -> Effect<Action>Creates a pipeable channel(value:scheduling:file:function:line:_:) whose body consumes the piped values as an AsyncStream — the rich layer over the raw ChannelHandler sink, for when you want for await and async-sequence operators instead of a per-value callback.
static func open(Channel<Action>) -> Effect<Action>Opens a Channel imperatively — the action-driven way to start a long-lived resource, with its lifetime managed by you (via cancel(id:)) instead of by a reaction’s reconcile.
static func broadcast<Value>(Value, channel: some Hashable & Sendable, file: String, function: String, line: UInt) -> Effect<Action>Sends value into the live channel registered under channel, without opening anything.
static func cancel<H>(id: H) -> Effect<Action>Creates a sentinel effect that cancels whatever runs under id — a one-shot effect, a debounced/throttled effect, or a long-lived Channel — without starting anything new.
struct ChannelHandlerThe body of an channel(value:scheduling:file:function:line:_:) returns this handler: a per-value receive sink the Store calls for every piped value, plus a cancel torn down when the channel is displaced or the Store deallocates.
Scheduling
Transforming & Composing
Type Methods
See Also
Related Documentation