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.
swift
@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
}
}
}
}Advanced usage
Where the library earns its place over a simpler alternative.
swift
@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")]
}
}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.
