TCA Study Note: Reducer

Yenting Chen
3 min readMar 15, 2024

--

We want to make our button actions or those that update the AppState look readable.

Button("-") { self.store.value.count -= 1 }

We create an enum which lists our actions:

enum CounterAction {
case decrTapped
case incrTapped
}

We want to create a function that takes our current state and the action and then returns a whole new state.

func counterRuducer(state: AppState, action: CounterAction) -> AppState {
switch action {
case .decrTapped:
return AppState(count: state.count - 1, favoriteNumbers: state.favoriteNumbers)
case .incrTapped:
return AppState(count: state.count + 1, favoriteNumbers: state.favoriteNumbers)
}
}


func counterRuducer(state: AppState, action: CounterAction) -> AppState {
var copy = state
switch action {
case .decrTapped:
copy.count -= 1
case .incrTapped:
copy.count += 1
}
return copy
}

So, the button action would be like that:

Button("-") { self.store.value = counterRuducer(state: self.store.value, action: .decrTapped)}

The function name counterReducer is the inspiration from the reduce operation, which takes the initial result and the function, which goes from the result you are accumulating from each value of the array and then returns the new accumulation result.

[1, 2, 3].reduce(
initialResult: Result,
nextPartialResult: (Result, Int) throws -> Result
)

The refactored actions are still verbose. We want to make the actions and state to be managed in the same place and look simpler. so we want to make the action like this:

// Button("-") { self.store.value = counterRuducer(state: self.store.value, action: .decrTapped)}

Button("-") { self.store.send(.decrTapped)}

In the Store class, we create a function called send , which takes the action. So it will become:

class Store<Value, Action>: ObservableObject {

@Published var value: Value
let reducer: (Value, Action) -> Value

init(value: Value, reducer: @escaping (Value, Action) -> Value) {
self.value = value
self.reducer = reducer
}

func send(_ action: Action) {
self.value = reducer(self.value, action)
}
}

Recap:
The journey of the button actions

self.store.value.count -= 1

// to make it readable -> create a reducer function
self.store.value = counterRuducer(state: self.store.value, action: .decrTapped)}

//still verbose, to centralised it in store
self.store.send(.decrTapped)

Let’s look into the Store class.

class Store<Value, Action>: ObservableObject {

@Published var value: Value
let reducer: (Value, Action) -> Value

init(value: Value, reducer: @escaping (Value, Action) -> Value) {
self.value = value
self.reducer = reducer
}

func send(_ action: Action) {
self.value = reducer(self.value, action)
}
}

We can use the inout feature to make it more efficient.

// (A) -> A
(inout A) -> Void

So the Store class will become this:

class Store<Value, Action>: ObservableObject {

@Published var value: Value
let reducer: (inout Value, Action) -> Void

init(value: Value, reducer: @escaping (inout Value, Action) -> Void) {
self.value = value
self.reducer = reducer
}

func send(_ action: Action) {
reducer(&self.value, action)
}
}

To update other actions like saving a fav number or removing a fav number, we can add our use case in the enum CounterAction and rename it as AppAction .
So it would be like

enum AppAction {
case decrTapped
case incrTapped
case saveFavoriteTapped
case removeFavorite(indexSet: IndexSet)
}

and in the counterRuducer, we can add more cases, and then rename counterReducerto appRuducer

func appRuducer(state: inout AppState, action: AppAction) {
switch action {
case .decrTapped:
state.count -= 1
case .incrTapped:
state.count += 1
case .saveFavoriteTapped:
state.favoriteNumbers.append(state.count)
case let .removeFavorite(indexSet):
for index in indexSet {
state.favoriteNumbers.remove(at: index)
}
}
}
}

But we still want to make it modularity, so we should pull CounterAction and FavAction out from the AppAction

enum AppAction {
case counter(CounterAction)
case fav(FavAction)
}

enum CounterAction {
case decrTapped
case incrTapped
}

enum FavAction {
case saveFavoriteTapped
case removeFavorite(indexSet: IndexSet)
}

And then update some parts according to the change. So the action journey would be like this:

self.store.value.count -= 1

// to make it readable -> create a reducer function
self.store.value = counterRuducer(state: self.store.value, action: .decrTapped)}

//still verbose, to centralised it in store
self.store.send(.decrTapped)

//AppAction Modularity
self.store.send(.counter(.decrTapped))

--

--

No responses yet