How to integrate Redux in your MVVM architecture
For last couple years, I’ve been experimenting different architectures to understand pros and cons of each one of them. Redux architecture is definitely one that peek my curiosity. In this new post, I’ll share my finding pairing Redux with MVVM, another pattern I’m familiar with and more importantly why you probably shouldn’t pair them.
Motivation
Earlier this year, I already have a try at Redux architecture, using ReSwift at that time. If you are not familiar with it, I would say to start there.
If it worked great for a small app, I had some worries with a bigger application:
- With one Store, each action needs to go to the very top of the funnel to be reduced until computed into new event.
- The app state stays obscure to me, I’m worried it became a massive class capturing it all. It felt going against the single responsibility principle.
- Finally, most of code is pure functions. That makes it easy to tests but it felt to me harder to organize code. I’m more familiar with methods to work with but maybe that’s my OOP legacy talking.
However, I still quite like the fundamentals of that pattern, “computing a new state based on a current state and new action”. That removes any inconsistent state that we have to face sometimes.
For those reasons, I thought about coupling a redux like pattern with MVVM. Let’s see how to implement this.
Requirements
For this example, I’m going to base on a very simple example, a view listing some content where user can select one. Here is its viewModel.
protocol ViewModelType {
associatedtype Input
associatedtype Output
var input: Input { get }
var output: Output { get }
}
struct MyViewModel: ViewModelType {
var input: MyViewModel.Input
var output: MyViewModel.Output
struct Input {
let reload: (Void) -> (Void)
let selectContent: (String) -> (Void)
}
struct Output {
let content: Observable<[String]>
let showDetail: Observable<String>
}
init(content: [String]) {
let showDetail = Observable<String>("")
let observableContent = Observable<[String]>(content)
let selectContent = { selectedContent in
showDetail.value = selectedContent
}
self.input = Input(selectContent: selectContent)
self.output = Output(content: observableContent, showDetail: showDetail)
}
}
Note that I’m using Input/Output
to explicitly define the incoming and outcomes information of the ViewModel. You can read more this alternative pattern here.
Regarding Observable
type, it’s only a observer design pattern wrapping a closure system that I’ve also used in the past. If you are familiar with functional reactive programming framework, it will look familiar.
The viewModel works as following:
// instantiating + mocking data
let viewModel = MyViewModel(content: ["hello", "world"])
// observe outputs
viewModel.output.content.addObserver(observer: observer) { content in
print("New content")
}
viewModel.output.showDetail.addObserver(observer) { selectedContent in
print("Show detail for \(selectedContent)")
}
// trigger new input
viewModel.input.reload(())
viewModel.input.selectContent("hello")
Now we’ve got the base, let’s see how to integrate Redux pattern to it.
Redux + MVVM
First, I don’t want to lose too much of both patterns, so the principle would stay the same.
Ideally an action would be triggered from the View layer, the ViewModel would forward that action to a Reducer, creating a new State. It could be something like that:
class ReduxViewModel: NSObject, ViewModelType {
var input: ReduxViewModel.Input
var output: ReduxViewModel.Output
enum Action {
case landing
case select(content: String)
}
struct Input {
let sendAction : Observable<Action>
}
struct Output {
let state: Observable<ReduxViewModelState>
}
...
}
However, one issue with Redux architecture which is flagged by many other developers is that we don’t know what UI elements to refresh between two states. You might have to either find a way to diff it, or to almost rebuild you whole UI, which isn’t so great.
Rather than settling down with this issue, I would prefer computing any needed output on the ViewModel side, that’s what its made for after all.
So I would create a struct to define the available states:
struct ReduxViewModelState {
enum Change {
case loadingContent
case content([String])
}
var onChange: ((Change) -> Void)?
mutating func loadContent() {
// mock api
onChange?(.content([“hello”, “world”]))
}
mutating func prepareLoading() {
onChange?(.loadingContent)
}
}
Then I would integrate it into the ViewModel:
class ReduxViewModel: NSObject, ViewModelType {
private let state: ReduxViewModelState
var input: ReduxViewModel.Input
var output: ReduxViewModel.Output
enum Action {
case landing
case select(content: String)
}
struct Input {
let sendAction : Observable<Action>
}
struct Output {
let content: Observable<[String]>
let showDetail: Observable<String>
}
override init() {
let showDetail = Observable<String>(“”)
let observableContent = Observable<[String]>([])
var state = ReduxViewModelState()
let sendAction = Observable<Action>(.landing)
self.input = Input(sendAction: sendAction)
self.output = Output(content: observableContent, showDetail: showDetail)
self.state = state
super.init()
// reducing new action to new state
sendAction.addObserver(object) { newAction in
switch newAction {
case .landing:
state.loadContent()
case .select(let selectedContent):
showDetail.accept(selectedContent)
break
}
}
// reduce new state to output
state.onChange = { newState in
switch newState {
case .content(let newContent):
observableContent.accept(newContent)
case .loadingContent:
state.loadContent()
case .showError(let errorMessage):
// TODO
break
}
}
}
}
The key relies in the last bit:
- the ViewModel is responsible to reduce any action into a new state.
- the ViewModel also compute new state into new output for the view.
The usage would stay very similar to the previous one
let reduxViewModel = ReduxViewModel()
let observer = NSObject()
// output binding
reduxViewModel.output.content.addObserver(object) { content in
print(“New content \(content)”)
}
reduxViewModel.output.showDetail.addObserver(object) { selectedContent in
print(“Show detail for \(selectedContent)”)
}
// new action as input
reduxViewModel.input.sendAction.accept(.landing)
reduxViewModel.input.sendAction.accept(.select(content: “2”))
Conclusion
It seems working as expected, we’ve explicitly defined available states and actions for our view, we have full control of income and outcome of what’s happening. So is it a success?
Well, to me, not really.
First, the main advantage of Redux architecture is to be able to dispatch state to any observer through the app using a single container. Without it, we limit ourselves to the ViewModel only: if we want to dispatch different behavior (analytics, logging, storage, etc.) to a same event, we’ll have to find another way.
On the other side, the ViewModel still holds most of the logic, it has to reduce to new states and compute the UI changes accordingly, something that looked easier in our version before. It doesn’t look easier to test or maintain.
In short, it does not feel right to integrate Redux and MVVM: going too far with one of the pattern would go against the other. I can see more issues to force them coexisting than getting any benefits when paired.
Of course, it’s only my opinion, but it’s also good to ask ourselves what’s the point: not everything need to work together, I believe it’s better to understand the limit of each tool and how they can interact with each other rather than missing the point and paying a more expensive code debt.
What about you? Did you try Redux? Did you pair it with another pattern?