Skip to main content

State Management

The Composable Architecture uses value types (structs and enums) to model application state in a predictable and testable way. State is immutable from the outside and can only be modified by reducers in response to actions.

Value Types for State

TCA encourages using Swift structs and enums to define your feature’s state:
State.swift
@ObservableState
struct State {
  var count: Int = 0
  var isLoading: Bool = false
  var error: String?
}

Benefits of Value Types

Predictability

State changes are explicit and traceable through actions

Testability

Easy to construct any state for testing scenarios

Thread Safety

Value semantics eliminate shared mutable state issues

Time Travel

Snapshots enable undo/redo and debugging features

Observable State

The @ObservableState macro enables SwiftUI views to observe state changes efficiently:
Feature.swift
@Reducer
struct Feature {
  @ObservableState
  struct State {
    var username: String = ""
    var isLoggedIn: Bool = false
  }
  
  enum Action {
    case usernameChanged(String)
    case loginButtonTapped
  }
  
  var body: some Reducer<State, Action> {
    Reduce { state, action in
      switch action {
      case let .usernameChanged(username):
        state.username = username
        return .none
        
      case .loginButtonTapped:
        state.isLoggedIn = true
        return .none
      }
    }
  }
}
The @ObservableState macro conforms your state to the ObservableState protocol, which enables observation through Swift’s Observation framework (iOS 17+) or the Perception package (iOS 13-16).

How Observable State Works

The macro adds the following to your state type:
  • _$id: ObservableStateID - A unique identifier that changes when state mutates
  • _$willModify() - Called before any property modification
  • Observation registrar for tracking access and mutations
// Simplified macro expansion
@ObservableState
struct State {
  var count: Int = 0
  
  // Generated by macro:
  var _$id = ObservableStateID()
  var _$observationRegistrar = ObservationRegistrar()
  
  mutating func _$willModify() {
    _$id._$willModify()
  }
}

State Composition

Compose larger state from smaller pieces:
AppState.swift
@ObservableState
struct AppState {
  var profile: Profile.State
  var settings: Settings.State
  var search: Search.State
}

Optional State

Use optionals to represent state that may not exist:
@ObservableState
struct State {
  var user: User?
  @Presents var alert: AlertState<Action.Alert>?
}
Use the @Presents macro instead of manually wrapping state in PresentationState when using @ObservableState.

Collection State

Manage collections of child features:
import IdentifiedCollections

@ObservableState
struct State {
  var todos: IdentifiedArrayOf<Todo.State> = []
}

struct Todo: Reducer {
  @ObservableState
  struct State: Identifiable {
    let id: UUID
    var description: String
    var isComplete: Bool
  }
  // ...
}

State Mutations

State should only be mutated inside reducers. Direct mutation from views or effects will not trigger observations and can lead to inconsistent state.

Correct: Mutate in Reducer

var body: some Reducer<State, Action> {
  Reduce { state, action in
    switch action {
    case .incrementButtonTapped:
      state.count += 1  // ✅ Correct
      return .none
    }
  }
}

Incorrect: Direct Mutation

struct MyView: View {
  let store: StoreOf<Feature>
  
  var body: some View {
    Button("Increment") {
      store.state.count += 1  // ❌ Won't compile - state is read-only
    }
  }
}

Accessing State

Access state from the store in different contexts:

In SwiftUI Views

struct FeatureView: View {
  let store: StoreOf<Feature>
  
  var body: some View {
    // Direct property access
    Text("Count: \(store.count)")
  }
}

Using withState

For non-observable contexts or when you need a snapshot:
store.withState { state in
  print("Current count: \(state.count)")
  return state.count * 2
}

Enum State

Model exclusive states using enums:
@ObservableState
enum LoadingState<Data> {
  case idle
  case loading
  case loaded(Data)
  case failed(Error)
}

@ObservableState
struct State {
  var data: LoadingState<[Item]> = .idle
}

Best Practices

1

Keep State Minimal

Only store the minimum necessary state. Derive computed values in views or using computed properties.
@ObservableState
struct State {
  var firstName: String = ""
  var lastName: String = ""
  
  // Computed property - not stored
  var fullName: String {
    "\(firstName) \(lastName)"
  }
}
2

Use Value Types

Prefer structs and enums over classes for state. This ensures copy-on-write semantics and value equality.
3

Make State Codable

When appropriate, conform state to Codable for persistence:
@ObservableState
struct State: Codable {
  var settings: UserSettings
  var lastSyncDate: Date?
}
4

Normalize Collections

Use IdentifiedArray instead of Array for collections of identifiable items:
// ❌ Avoid
var users: [User] = []

// ✅ Prefer
var users: IdentifiedArrayOf<User> = []
  • Reducers - Learn how state is modified
  • Store - Understand the runtime container for state
  • Composition - Compose state from smaller pieces

Build docs developers (and LLMs) love