The composable architecture

Unidirectional Data Flow

Sorry, your browser does not support SVG.

Pointfree

https://www.pointfree.co/

Building blocks

State, Action and Environment

struct State {
    var currentText: String
    var subviewStates: IdentifiedArray<SubviewState>
    var backendResponse: Response
}
enum Action {
    case buttonTapped
    case textUpdated(String)
    case subviewAction(id: Subview.ID, action: SubviewAction)
}
struct Environment {
    let dependency: Dependency
    let subviewEnvironment: SubviewEnvironment
}

Reducer

let reducer = Reducer<State, Action, Environment>() {
    state, action, environment in
    switch action {
    case .textUpdated(let newText):
        state.currentText = newText
    case .buttonTapped:
        return environment.dependency.someBackgroundWork()
    case .backgroundWorkFinished(let result):
        state.backendResponse = result
    case .subviewAction(let id, .someSubviewAction):
        // Do spmething with subview
        break
    }
}

View

struct MyView: View {
    let store: Store<State, Action>
    var body: some View {
        WithViewStore(self.store) { viewStore in
            Button(viewStore.text) {
                viewStore.send(.buttonTapped)
            }
        }
    }
}

Combination

let reducer = Reducer<State, Action, Environment>.combine(
  Reducer {
    state, action, environment in
    switch action {
    case .textUpdated(let newText):
        state.currentText = newText
    // ...
    },
    subviewReducer.pullback(
          state: \.subviewState,
          action: /ViewAction.subviewAction,
          environment: \.subviewEnvironment
    )
})

Testing

Exhaustive testing with TestStore

  • Use TestStore.assert to run through an event flow of your view
  • CA forces you to assert every state change and effect
  • Use TestScheduler to advance queues and test asynchronous code

Example

testStore.assert(
  .send(.buttonTapped) {
      $0.isEditing = true
  },
  .receive(.updateContent),
  .do {
      testQueue.advance()
  },
  .receive(.contentReceivedFromAPI(let newContent)) {
      $0.content = newContent
      $0.description = "up to date"
  }
)

Conclusion

Merits

  • Boilerplate
  • Very opinionated
  • Requires a new and unusual style of designing dependencies and testing
  • Learning curve, initial pitfalls

Benefits

  • Forces you to
    • test extensively
    • seperate view and logic
    • seperate side effects from pure functions
  • Amazing composability
  • Great potential for modularisation