ReSwift - Introduction to Redux architecture in Swift
If you are not familiar with it, Redux a Javascript open source library designed to manage web application states. It helps a lot to make sure your app always behaves as expected and makes your code easier to test. ReSwift is the same concept but in Swift. Let’s see how.
Concept
ReSwift is described as a Redux-like implementation of the unidirectional data flow architecture. What it means is that it defines a funnel where each application state goes through until one reducer responds to it and execute the expected behavior. Each new state would then be reflected in the user interface.
The main goal of ReSwift code structure is to decouple responsibilities between each layer. We can keep a clean code and a light view layer, avoiding the massive view controller issue, while having a testable code base.
Here are its main components:
- A Store that will collect each state of you application.
- An Action that represents a state change. Itβs basically any user interaction that affect a data treatment.
- A Reducer that consumes an Action and create a new app state from it to your reflect in the user interface.
So far so good, but how does it look into the code? Let’s dive in.
Code Sample
After installing ReSwift via your favorite dependency manager, the first step is to create a global store for your application. We’ll also need the very first state of our application.
For this example, I’ll keep the app simple like a currency converter.
// define app state
struct AppState: StateType { }
// define global Store
let appStore = Store<AppState>(reducer: /* TODO */, state: nil)
We’ve got out store defined but still no reducer. We need one to transform an action into an AppState. Starting small, I’m going to create a state where the application fetch currencies.
struct AppState: StateType {
var fetchCurrencies: FetchCurrenciesState?
}
struct FetchCurrenciesState: StateType {
var currencies: Result<Data, Error>?
}
let appReducer: Reducer<AppState> = { action, state -> AppState in
return AppState(fetchCurrencies: nil)
}
let store = Store<AppState>(reducer: appReducer, state: nil)
Note that Reducer
is only an alias of (Action, ReducerStateType?) -> (ReducerStateType)
. That means you can also replace it with a function instead as following.
func appReducer(action: Action, state: AppState?) -> AppState {
return AppState(currencies: nil)
}
let store = Store<AppState>(reducer: appReducer, state: nil)
Great, but we still don’t have fetch any data.
That’s right. From our AppState
, we’ll need a new action and reducer to compute it to a new state FetchCurrenciesState
.
struct FetchCurrencyAction: Action { }
let currencyReducer: Reducer<FetchCurrenciesState?> = { action, state -> FetchCurrenciesState? in
switch action {
case let action as FetchCurrencyAction:
// mocking data
let result = Result<Data, Error>.success(Data())
return FetchCurrenciesState(currencies: result)
default:
return nil
}
}
let appReducer: Reducer<AppState> = { action, state -> AppState in
return AppState(fetchCurrencies: currencyReducer(action, state?.fetchCurrencies))
}
Our app store can now handle a new app state: if a FetchCurrencyAction
occurs, we’ll execute the currency reducer that (for now) mock data and return a new state. The way to see it is to dispatch a new action.
store.dispatch(FetchCurrencyAction())
So far, this works just fine in a playground project. But let’s see how easy it is to integrate to an iOS project. For instance, how to catch the update state from a UIViewController. Well, ReSwift got a subscription system to call back any subscriber for specific state.
class ViewController: UIViewController, StoreSubscriber {
override func viewDidLoad() {
super.viewDidLoad()
// subscribe to new states
store.subscribe(self)
// dispatch an new action
store.dispatch(FetchCurrencyAction())
}
func newState(state: AppState) {
guard let result = state.fetchCurrencies?.currencies else {
return
}
switch result {
case .success(_):
print("Got data!")
case .failure(_):
print("Got error")
}
}
}
Great, so far we defined different states, reducers and actions to get mocked data and notify our view when needed. However, our view would always be notified for any new state. If we trigger a different state and we still have valid currencies, it might still execute our previous code.
Good think is that we can filter our subscription by specifying which state we’re looking for.
class ViewController: UIViewController, StoreSubscriber {
override func viewDidLoad() {
super.viewDidLoad()
store.subscribe(self, transform: { $0.select({ $0.fetchCurrencies })})
store.dispatch(FetchCurrencyAction(query: "GBR"))
}
func newState(state: FetchCurrenciesState?) {
guard let result = state?.currencies else {
return
}
switch result {
case .success(_):
print("Got data!")
case .failure(_):
print("Got error")
}
}
}
Now that we have a sample code working let’s step back and analyse a bit more about ReSwift promises.
Code Analysis
If we aim for a clean code following single responsibility, there are couple flaws in the approach that make me doubt we’ll be able to follow it. Let’s see how we can work around.
First, most of ReSwift examples use one app store for their whole application. That can works well for small application, but I’m worried that it will be hard to scale into bigger one. For instance, if you have an e-commerce application, we would need to handle a lot of application states. I would like my code be efficient enough and avoid going through each reducers or refresh each view for an updated state.
One workaround I can see is to work with multiple app stores. Why not having one for each separated logic? For instance one for all user related state (register, login, guest visit), same for all cart states, etc.
Another point to look into is network requests. In my previous example, I mocked data but like any network requests, it should be executed asynchronously. That means currencyReducer
would probably run a web service and dispatch new action instead of returning a new state synchronously.
let currencyReducer: Reducer<FetchCurrenciesState?> = { action, state -> FetchCurrenciesState? in
if action is FetchCurrencyAction {
CurrencyService().fetchCurrency({ (result) -> (Void) in
store.dispatch(AnotherAction(result))
})
}
return nil
}
This becomes a problem because we can’t hardly inject this service. Also the currencyReducer
doesn’t properly return a new state which goes against the Redux approach.
There is a great thread discussing how to work around this one. One common way is to use a Middleware. It works as an extra layer, sitting between an action and its reducer. Although its syntax can be discussed.
let currencyMiddleware: Middleware<FetchCurrenciesState> = { dispatch, state in
return { next in
return { action in
print(action)
// TODO implement web service here
next(action)
}
}
}
Finally, a lot of properties are defined global which I’m not sure it’s necessary. I also feel there are a lot of code here and there that leave my project a bit messy. I believe it can be simplified and be owned by the right element. Why not keeping the app store under AppDelegate
and inject to related view only when needed.
If there is room for improvement, I still quite like its simple approach. ReSwift follows an observer design pattern, queueing each app state for the right element to reply to. And that works pretty well.
I’m still experimenting with it, but I can see how it can be useful paired with other code architectures. For instance, I can definitely picture ReSwift with MVVM where the ViewModel works as Reducer to get a new View state. I can also see it paired with RxSwift to subscribe to new app state.
If it doesn’t fit for every projects, it’s still very interesting to experiment and see what ReSwift and more generally App State approach can bring to your applications.
Happy coding