Stop Putting State In Your View Models
One of the benefits of separating state from your decision-making code is that the decisions become reproducible.
You don’t want your decision-making functions to change their moment-to-moment behavior based on a history of events preceding them. That increases branching and multiplies the possible causes of misbehavior. Was a bug caused by bad state, bad logic, or a combination of the two?
If you’re not paying attention, it’s easy to let state creep into your view models and pollute your logic.
Consider an example.
Application State Inside A View Model
Imagine an app with a simple file download feature.
- Check if there is a file to download. If so, show a download button.
- When the download button is clicked, start the download.
See the state? Here it is again, but this time from an API interface:
And the relevant models:
Where is the state? Between the two service calls!
The UI flow steps can now be specified further (state included):
DownloadApi.search()and see if the returned
FileSearchResulthas a file to download.
- If no file exists, show the empty UI.
- If there is a file to download, remember the file ID
- When the download button is clicked, pass the file ID to the
DownloadApi.downloadFile()to start the download.
State itself is not a problem, but put in the wrong place (or multiple places at once) results in unstated possibilities that you’ll never explicitly model. If you’ve ever written something like this,
// This will never happen.
Then you might have failed to model all the real states, or you introduced implicit states by distributing it to multiple owners.
A (Bad) View Model
A naive place to store the
RemoteFile search result would be inside the View Model, just after it’s returned from
The internal state of this View Model can alter its outward behavior by the absence or presence of the nullable
RemoteFile. Even worse, the
startDownload() method is forced to deal with a poorly modeled possibility: somehow the internal state is bad, and there’s not an explicit reason.
Is this a logic error or a problem with the data layer? Is there a possibility of recovery, and if so how? And in the meantime, what do you tell the user?
Delegate State, Don’t Manage It
In the View Model, the
startDownload() method can be re-written:
RemoteFile? member field gone, the
startDownload() method needn’t worry about a nullable internal state. It’s passed in directly when the View Model needs to react to an event.
Who passes in the
RemoteFile? There are multiple viable options that I will mention shortly, but now that the state is pulled from the View Model, you can start to think about who really owns it. You can also understand and explicitly model the errors that before were implicit and poorly understood.
Where does the state go?
There’s two adjacent layers to the View Model:
- The View Layer (suitable for transient state)
- The Data Layer (suitable for persistent state)
In many circumstances it’s not wise to place much (or any) state inside the view layer. However, consider the nature of that state you’d like to save.
- Is it safe to store this state as a temporary interaction for the user? Think
CheckBox, or other UI elements that may need to be refreshed when the UI is recreated.
- Is it safe to lose this state? Does it represent something that should out-live the UI, or can it be recovered easily from source when new UI is created?
The Data Layer could explicitly write down anything you care about such that a View Model observing it can push new states to the UI, accurately reflecting whatever you need to persist. An example might be a file that multiple users can remotely update, or the state of a background download that doesn’t correspond to any UI. The presence or absence of a
RemoteFile can also be modeled in the data layer!
Alternatively, in the
DownloadViewModel example, a temporary UI element could reasonably hold state if we don’t mind losing it when the UI is destroyed.
Does putting state into the View Layer seem gross to you? Or can you imagine cases where you’d be perfectly comfortable delegating state to the UI in this manner?
Either way, good! Acknowledging the state’s existence means you can decide how to model it. Now you can confront each state explicitly, whereas before the nullable member field in the View Model was pushing complexity under the rug, only to pop out later as an error condition without an explanation.
Keep your decisions stateless, and your state decisionless.
Collin handles states at Livefront, and he thanks you for reading.