TCA Study Note: Reducer
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 counterReducer
to 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))