Skip to content
Go back

The Composable Architecture - Best Practices

The team at The Browser Company is a major adopter of The Composable Architecture (TCA) framework. Based on our team’s experiences and insights from the wider community, I’ve developed a new set of best practices that can benefit your TCA projects.

Here are some of the key practices to consider using in your projects.

Reducers

  .reducer(.onResultsUpdated(await self.search(query: query)) 
}

State Modeling

Testing

let windows = WindowsState.stub(windows: [.init(window)], sidebar: .stub(globalSidebarContainer: sidebar))
let store = TestStore(
  initialState: windows,
  prepareDependencies: {
    $0.uuid = .incrementing
  }
)

// Do
let store = TestStore(
  initialState: .stub(windows: ...),
  prepareDependencies: {
    $0.uuid = .incrementing
  }
)

Dependencies

Protocol-oriented interfaces for clients are discouraged in our codebase

// Don't
protocol AudioPlayer {
  func loop(_ url: URL) async throws
  func play(_ url: URL) async throws
  func setVolume(_ volume: Float) async
  func stop() async
}

// Do
struct AudioPlayerClient {
  var loop: (_ url: URL) async throws -> Void
  var play: (_ url: URL) async throws -> Void
  var setVolume: (_ volume: Float) async -> Void
  var stop: () async -> Void
}

This allows us to describe the bare minimum of the dependency in tests. For example, suppose that one user flow of the feature you are testing invokes the play endpoint, but you don’t think any other endpoint will be called. Then you can write a test that overrides only one endpoint and uses the default .failing version for all the others. By doing so, you can ensure that your tests are focused and efficient, making it easier to identify and resolve any issues that arise.

let model = withDependencies {
  $0.audioPlayer.play = { _ in await isPlaying.setValue(true) }
} operation: {
  FeatureModel()
}

This allows your test suite to continue running even if a failure occurs. If an issue is detected, it’s reported via XCTFail along with the name of the endpoint that caused the failure. This is preferable to a fatalError, which can cause the test to crash and drop into the debugger, interrupting the rest of the test suite.

// DON'T
searchHistory: unimplemented("\(Self.self).searchHistory")

// DO
searchHistory: unimplemented("\(Self.self).searchHistory", placeholder: [])

View layer

viewStore = .init(store.scope(state: \.overlayViewState))

// DO
viewStore = .init(store, observe: \.overlayViewState)

Bugs & Workarounds

Conclusion

The Browser Company is a major adopter of The Composable Architecture (TCA) framework. We have shared best practices based on our team’s experiences and insights from the wider community.

These practices are designed to help you optimize the performance and stability of your TCA projects and make them more maintainable in the long term. By adopting these practices, you can build high-quality TCA projects that meet your users’ needs and exceed their expectations.

References:


Share this post on:

Previous Post
How to avoid burnout as a software engineer?
Next Post
Widget Architecture - Part 3