Skip to main content
The Two Counters example demonstrates how to take small features and compose them into larger ones. This is a fundamental pattern in TCA for building modular, reusable components.

Overview

This example shows how to:
  • Compose child features using Scope
  • Embed child state in parent state
  • Route child actions through parent actions
  • Share views across feature boundaries

Implementation

import ComposableArchitecture
import SwiftUI

@Reducer
struct TwoCounters {
  @ObservableState
  struct State: Equatable {
    var counter1 = Counter.State()
    var counter2 = Counter.State()
  }

  enum Action {
    case counter1(Counter.Action)
    case counter2(Counter.Action)
  }

  var body: some Reducer<State, Action> {
    Scope(state: \.counter1, action: \.counter1) {
      Counter()
    }
    Scope(state: \.counter2, action: \.counter2) {
      Counter()
    }
  }
}

Key Concepts

State Composition

The parent state embeds child state as properties:
@ObservableState
struct State: Equatable {
  var counter1 = Counter.State()
  var counter2 = Counter.State()
}
Each counter maintains its own independent state.

Action Composition

Parent actions contain child actions:
enum Action {
  case counter1(Counter.Action)
  case counter2(Counter.Action)
}
This creates a clear routing path for actions through the feature hierarchy.

The Scope Reducer

The Scope reducer connects parent and child domains:
Scope(state: \.counter1, action: \.counter1) {
  Counter()
}
This tells TCA:
  • Where to find child state in parent state (\.counter1)
  • How to embed child actions in parent actions (\.counter1)
  • What reducer to run for that domain (Counter())

View Scoping

Views use scope to focus on child domains:
CounterView(
  store: store.scope(
    state: \.counter1,
    action: \.counter1
  )
)
This creates a Store<Counter.State, Counter.Action> from the parent Store<TwoCounters.State, TwoCounters.Action>.

Benefits of Composition

Reusability

The Counter feature and CounterView are completely reusable. They have no knowledge of the parent feature.

Modularity

Each counter is isolated. Changes to one counter don’t affect the other.

Testability

You can test child features in isolation or test the composed feature:
@Test
func testIndependentCounters() async {
  let store = TestStore(initialState: TwoCounters.State()) {
    TwoCounters()
  }

  // Counter 1 increments
  await store.send(.counter1(.incrementButtonTapped)) {
    $0.counter1.count = 1
  }

  // Counter 2 increments independently
  await store.send(.counter2(.incrementButtonTapped)) {
    $0.counter2.count = 1
  }

  // Counter 1 decrements
  await store.send(.counter1(.decrementButtonTapped)) {
    $0.counter1.count = 0
  }
}

Parent Logic

Parents can observe and react to child actions:
var body: some Reducer<State, Action> {
  Scope(state: \.counter1, action: \.counter1) {
    Counter()
  }
  Scope(state: \.counter2, action: \.counter2) {
    Counter()
  }
  Reduce { state, action in
    switch action {
    case .counter1(.incrementButtonTapped):
      // Parent can react to counter1 incrementing
      return .none
    case .counter2(.decrementButtonTapped):
      // Parent can react to counter2 decrementing
      return .none
    default:
      return .none
    }
  }
}

Source Code

View the complete example in the TCA repository:

Next Steps

Build docs developers (and LLMs) love