Effects and Dependencies
Handle side effects and inject dependencies.
Effects and Dependencies is a free Swift Academy lesson on CoddyKit — lesson 3 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Swift Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
Why Effects Exist
Reducers must stay pure: given a state and an action, they only mutate state. But real apps need side effects like network requests and timers. TCA models these as Effects the reducer returns, keeping the mutation pure while the async work runs separately.
This separation is what makes features testable.
import ComposableArchitecture
// Reducer stays pure; side effects are values it returns.
// Returning .none means no further work.Returning .none
When an action only changes state and needs no follow-up work, the reducer returns .none. This is the most common effect and signals there is nothing asynchronous to do.
You saw this already; it is the baseline every branch falls back to.
import ComposableArchitecture
Reduce { state, action in
switch action {
case .incrementTapped:
state.count += 1
return .none
}
}Effect.run for Async Work
To perform asynchronous work you return .run. Its closure receives a send handle you use to feed results back into the system as new actions.
Here a fetch action loads data and sends a response action when done.
import ComposableArchitecture
Reduce { state, action in
switch action {
case .reloadTapped:
return .run { send in
let value = try await loadNumber()
await send(.responseReceived(value))
}
case let .responseReceived(value):
state.count = value
return .none
}
}Feeding Results Back as Actions
An effect cannot mutate state directly; instead it sends actions through send. The reducer then handles those actions and updates state. This loop keeps every mutation inside the reducer.
So a network response becomes a response action that the reducer stores.
import ComposableArchitecture
.run { send in
let result = try await fetch()
await send(.dataLoaded(result))
}
// .dataLoaded is then handled by the reducer to set state.Handling Errors in Effects
Async work can throw. You can catch errors inside the effect and send a failure action, letting the reducer move into an error state cleanly.
The optional catch closure on .run receives the thrown error.
import ComposableArchitecture
.run { send in
let value = try await loadNumber()
await send(.responseReceived(value))
} catch: { error, send in
await send(.loadFailed)
}Why Dependencies Matter
If a reducer calls a network client or reads the date directly, it becomes hard to test and to control. TCA solves this with dependencies: injectable services your reducer accesses through @Dependency.
In tests you swap them for predictable fakes.
import ComposableArchitecture
// Instead of calling APIClient.shared directly,
// inject it as a dependency so tests can replace it.The @Dependency Property Wrapper
Inside a reducer you declare a dependency with @Dependency, naming it by its key path in the dependency values. The library provides built-in ones like a clock and a uuid generator.
Here the reducer pulls in the continuous clock.
import ComposableArchitecture
@Reducer
struct TimerFeature {
@ObservableState
struct State: Equatable { var seconds = 0 }
enum Action { case start, tick }
@Dependency(\.continuousClock) var clock
var body: some ReducerOf<Self> {
Reduce { state, action in .none }
}
}Using a Dependency in an Effect
Once declared, you use the dependency inside .run. Because the clock is injected, tests can supply a test clock that advances time on demand.
Here the timer ticks every second using the injected clock.
import ComposableArchitecture
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .start:
return .run { send in
for await _ in self.clock.timer(interval: .seconds(1)) {
await send(.tick)
}
}
case .tick:
state.seconds += 1
return .none
}
}
}Defining Your Own Dependency
You register custom dependencies by conforming a value to DependencyKey and providing a liveValue. You then add a computed property to DependencyValues so it gets a key path.
Here an API client becomes a first-class dependency.
import ComposableArchitecture
struct NumberClient {
var fetch: () async throws -> Int
}
extension NumberClient: DependencyKey {
static let liveValue = NumberClient {
try await loadNumber()
}
}
extension DependencyValues {
var numberClient: NumberClient {
get { self[NumberClient.self] }
set { self[NumberClient.self] = newValue }
}
}Injecting the Custom Dependency
With the key path in place, the reducer declares the client with @Dependency(\.numberClient) and calls it inside effects. Production uses the live value; tests override it.
Here a reload action fetches through the injected client.
import ComposableArchitecture
@Reducer
struct NumberFeature {
@ObservableState
struct State: Equatable { var value = 0 }
enum Action { case reload, response(Int) }
@Dependency(\.numberClient) var numberClient
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .reload:
return .run { send in
let n = try await numberClient.fetch()
await send(.response(n))
}
case let .response(n):
state.value = n
return .none
}
}
}
}Cancelling Effects
Long-running effects like timers should be cancellable. You tag an effect with a cancellable id and cancel it later from another action with .cancel.
Here the timer can be stopped on demand.
import ComposableArchitecture
enum CancelID { case timer }
// Start a cancellable effect:
.run { send in /* ... */ }
.cancellable(id: CancelID.timer)
// Stop it from another action:
.cancel(id: CancelID.timer)Quick Check: Effects and Results
Recall how an effect communicates its result back to the reducer.
Recap: Effects and Dependencies
You learned how TCA handles side effects and injection:
- Reducers stay pure and return
Effectvalues;.nonemeans no work. .runperforms async work and feeds results back viaawait send(.action).- Errors are handled with the effect's
catchclosure. @Dependencyinjects services like the clock or your own clients, declared viaDependencyKeyandDependencyValues.- Long-running effects can be made
cancellableby id.
Next you will compose features together and test them exhaustively.
import ComposableArchitecture
// Recap: pure reducer + Effect.run for async work
// + @Dependency for testable, injectable services.Frequently asked questions
Is the “Effects and Dependencies” lesson free?
Yes — the full text of “Effects and Dependencies” is free to read here on the web, and the Swift Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Swift Academy course, upgrade to CoddyKit PRO.
What will I learn in “Effects and Dependencies”?
Handle side effects and inject dependencies. You practise Swift Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start Swift Academy?
No prior experience is required. Swift Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 3 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Effects and Dependencies” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this Swift Academy lesson?
Yes. Every Swift Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- State, Action, and Reducer
- The Store and SwiftUI Integration
- Effects and Dependencies
- Composing and Testing Features