avatarOliver Foggin

Summary

Oliver Foggin introduces a SubscriberReducer in The Composable Architecture (TCA) for simplifying the handling of multiple data streams in an app, reducing repetitive code and cognitive load.

Abstract

In a recent article, Oliver Foggin discussed the use of dependencies in TCA for shared data management. Building upon that, he addresses the challenge of managing multiple AsyncStream subscriptions within reducers, which can lead to code duplication and maintenance difficulties. To solve this, he introduces a custom SubscriberReducer that encapsulates the subscription logic, allowing developers to subscribe to data streams with a single line of code. This approach not only simplifies the reducer code but also enhances readability and maintainability. Foggin's SubscriberReducer is generic and comes with helper functions to easily map stream elements to actions, with optional transformations for type mismatches. The solution has been beneficial in his current project, streamlining the codebase and reducing cognitive overhead.

Opinions

  • The author believes that handling multiple data streams can clutter reducer code, making it harder to maintain.
  • Foggin values the ease of subscribing to data streams and keeping reducers in sync with updated data.
  • He emphasizes the importance of refactoring redundant subscription logic to improve code maintainability.
  • The custom SubscriberReducer is seen as a significant improvement for developers working with TCA, as it reduces cognitive load and simplifies complex logic.
  • Foggin encourages the TCA community to adopt similar patterns to offload repetitive tasks and enhance code readability.

Using custom reducers to make shared data even easier with TCA

You can jump straight into the code here… https://github.com/oliverfoggin/swift-composable-subscriber

In my recent article on creating and subscribing to shared data in a TCA app I detailed how to use dependencies to store shared data that can be subscribed to from anywhere in your app.

This means that anywhere in your app a reducer can return a run Effect from its body and loop on the values from an AsyncStream and return some other action to deal with the updated values…

case .task:
  return .run { send in 
    for await value in await someDependency.valueStream() {
      await send(.newValueReceived(value))
    }
  }

This makes it really easy to subscribe to any stream of data in your app and even keeps your reducers in sync when the data is updated.

I then received a question on Mastodon…

https://hachyderm.io/@ben_lings/111578761999610292

And I was thinking about this in the app I’m working on currently. There are a few reducers that rely on multiple streams of data. This very quickly started to flood the reducer body with these same lines over and over again. We needed a way to refactor this out to make it easier to maintain whilst still giving us the control that we have here.

This is where a custom reducer can come in to help us.

The problem we are trying to solve can be described as…

  • When an Action occurs…
  • We should subscribe to some AsyncStream
  • And when it receives a new value we should run some other Action.

We can create this by creating our own custom Reducer.

I created a SubscriberReducer that is generic over…

  • Parent: Reducer it needs to know about the Action and State of the parent.
  • TriggerAction a specific action that will be used to trigger the subscription.
  • StreamElement the type of element that the AsyncStream will yield.
  • Value the type of the value that we want to handle (this allows us to provide a transform if necessary).

By doing this we can create several helper functions like…

.subscribe(to: myDependency.valueStream, on: \.task, with: \.newValueReceived)

This takes the five lines above and reduces it to a single line. And allows us to add multiple subscriptions very easily.

If the AsyncStream type matches the action then we can just do like above…

Reduce { state, action in 
  // this is ther regular reducer
}
.subscribe(to: myDependency.valueStream, on: \.task, with: \.newValueReceived)

If the type of the AsyncStream doesn’t match the action then you can provide a simple transform…

Reduce { state, action in 
  // this is ther regular reducer
}
.subscribe(to: myDependency.intStream, on: \.task, with: \.newStringReceived) { intValue in 
  "\(intValue)" // convert the int to a string for the action
}

And even if we have more complex logic that we need to run we can do that also…

Reduce {
  // this is ther regular reducer
}
.subscribe(
  to: myDependency.stream,
  on: \.some.trigger.action
) { send, streamElement in
  // here you have the usual send argyment that you can use to send actions as you wish
  await send(.responseAction)
  await otherDependency.doSomethingElse(with: streamElement)
}

This has been great for us as it has allowed us to greatly simplify the code involved when we have multiple subscriptions firing off. And it makes it much easier to read also…

Reduce { state, action in 
  // this is ther regular reducer
}
.subscribe(to: myDependency.userStream, on: \.task, with: \.newUserDataReceived)
.subscribe(to: myDependency.chatStream, on: \.task, with: \.newChatMessageReceived)
.subscribe(to: myDependency.reactionStream, on: \.task, with: \.newReactionReceived) {
  Reaction(payload: $0)
}

Custom reducers can really help shift a lot of the cognitive load of your app so that you don’t have to worry about it at code time and it makes for easier reading and understanding too.

What custom reducers have you been able to create to help you offload repetitive work and reduce cognitive load?

I hope this has helped you. You can find my code and SPM package on GitHub https://github.com/oliverfoggin/swift-composable-subscriber

iOS Development
Swift
iOS App Development
iOS
Recommended from ReadMedium