Skip to content

The Composable Architecture

Web & HTTPState ManagementSwift

What it is

TCA is a state management library for SwiftUI and UIKit, structuring applications around reducers, effects and composable feature modules.

A feature declares State, Action and a Reducer. Side effects are returned as values rather than performed inline, which is what makes the whole flow testable.

Installation

.package(url: "https://github.com/pointfreeco/swift-composable-architecture", from: "1.15.0")

Getting started

The smallest useful thing you can do with it, and what each part means.

A reducer feature
@Reducer
struct BookList {
    @ObservableState
    struct State: Equatable {
        var books: [Book] = []
        var isLoading = false
        var errorMessage: String?
    }

    enum Action {
        case onAppear
        case booksResponse(Result<[Book], Error>)
    }

    @Dependency(\.bookClient) var bookClient

    var body: some ReducerOf<Self> {
        Reduce { state, action in
            switch action {
            case .onAppear:
                state.isLoading = true
                return .run { send in
                    await send(.booksResponse(Result { try await bookClient.fetch() }))
                }

            case let .booksResponse(.success(books)):
                state.isLoading = false
                state.books = books
                return .none

            case let .booksResponse(.failure(error)):
                state.isLoading = false
                state.errorMessage = error.localizedDescription
                return .none
            }
        }
    }
}
The reducer never performs I/O directly — it returns an Effect describing the work. That separation is why a test can run the reducer synchronously and assert on every intermediate state.

Advanced usage

Where the library earns its place over a simpler alternative.

Exhaustive testing
@Test
func loadsBooks() async {
    let store = TestStore(initialState: BookList.State()) {
        BookList()
    } withDependencies: {
        $0.bookClient.fetch = { [Book(id: 1, title: "Dune")] }
    }

    await store.send(.onAppear) { $0.isLoading = true }

    await store.receive(\.booksResponse.success) {
        $0.isLoading = false
        $0.books = [Book(id: 1, title: "Dune")]
    }
}
TestStore is exhaustive by default: if the reducer changes any state you did not assert, or leaves an effect running, the test fails. That strictness catches a great deal.

Errors and fixes

The failures you are most likely to hit, and what actually resolves them.

An effect is still running at the end of the test
TestStore requires all effects to finish. Await the received actions, or cancel the effect explicitly.
Compile times grow sharply
Large reducer bodies strain type inference. Split features into smaller reducers composed with Scope.

Best practices

  • Keep effects out of reducers — return .run rather than awaiting inline.
  • Register side effects as dependencies so tests can substitute them.
  • Compose small features with Scope rather than one large reducer.
  • Be realistic about the learning curve; it is a significant commitment for a small app.

Background

Why it exists, and what it was reacting to.

Built by Point-Free, TCA brings Elm-style unidirectional data flow to Swift with an emphasis on exhaustive testability — every state change is a value transformation you can assert on.