0Pricing
Swift Academy · Lesson

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 Effect values; .none means no work.
  • .run performs async work and feeds results back via await send(.action).
  • Errors are handled with the effect's catch closure.
  • @Dependency injects services like the clock or your own clients, declared via DependencyKey and DependencyValues.
  • Long-running effects can be made cancellable by 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

  1. State, Action, and Reducer
  2. The Store and SwiftUI Integration
  3. Effects and Dependencies
  4. Composing and Testing Features
← Back to Swift Academy