@danabramov recently took the JS community by suprise by proposing a radically simple idea for defining application state. A single, simple, serializable object could define the state for an entire native web application. That is to say, plug a state object into your app and it should be able to render perfectly into that state. State becomes immutable in this strategy, and we re-render on state changes.
Most of us web developers are very familiar with maintaining state all over the place. For example, we often maintain state in controllers, in the DOM, and even in the
window. For some of us, we even maintain state in objects that are designed entirely to maintain state. Think ampersand-state or backbone Model. The proposal to use a single store of state, hereby SSS (because why not!), is an intuitive and desirable condition for developers to have in their app.
If we buy into the immutable proposition for our native web apps that Dan proposes, does that mean that there's no room for our existing tools?
As often the case, the answer to above question depends on the rules that the developer or team decides. I'm a big fan of ampersand-state, so I will speak in the context of that. For those who are not familiar with it, let me elaborate on some of its features. Let's see how it was used traditionally before I propose how it can be used in the context of a SSS application. If you are familiar with backbone Model, ampersand-state overlaps heavily, so please feel free to skim!
From the docs:
ampersand-state: An observable, extensible state object with derived watchable properties.
Pretty simple definition. Nothing immediately raises red flags that says this tool violates a redux-like pattern, beyond the word "state" itself. Let's talk about some major features as they pertain to data-modeling and application interaction:
defined, assertable properties
you can specify properties on your data-model. Let's use a
File as an example. I can define
pathname as attributes. I can declare their types, and mandate that they are present. What value does this give me?
sizeis a number in kilobytes, it will Error out if I accidentally enter string.
_idwhen putting data into the database. In the
Fileexample, I really want to make sure that I don't put the same file in twice. Therefore, I specify
_idas a derived attribute to proxy
sha. There are other ways to handle this, but boy, that was easy!
These are only some of the features it offers. This isn't an article on &-state though. Per review of the above, you can see that pre-2015-bleeding-edge tooling exists to give your data structure and power. These are useful features that I shouldn't dispose of. The immutable app model proposed by
redux has a simple mantra--let state be expressed in a simple store. Is there a way I can still exploit the benefits outlined above, but not let my Models define the actual application state? Yes, I argue that you can.
Let your data models be intermediaries. To best see this in action, let's study the following two diagrams. The TOP is the default
redux flow. The BOTTOM is the redux flow using models as an intermediary data model, such as &-state.
Consider the bottom image. State is read from the store, then some bit of that state may be transformed into a rich Model object (see the constructor). The
props to my smart component are still passed to the dumb component as usual. The key difference is that UI changes that impact your data-models now don't go directly into the app-state via actions. Instead, UI events that result in data-model state changes first get set into the model, and then actions consume data from the data-model, not the event. Your model must provide a one-way flow from the UI into actions/actionCreators. You should almost never have to pass your data model down to your dumb components, otherwise you may have missed the point. There may be cases where your dumb components need access to a derived attribute from your model. These cases can often be prepped in your smart controller and passed through via composition.
"OK. They both methods achieve the same end goal, but your method is more expensive. I still don't see the advantage," you say. Consider if you have multiple views. One view is for uploading the
File. Another view allows people to tag the
File with data. In the simple approach, a user in one view may dispatch an action setting the
File's state to have a
file.sha, but in the other view someone may set
file.fileSha. Assuming both state updates were dispatched, both
fileSha exist in state, redundantly. A rigid data-model prevents that case from existing. Additionally, if you are referencing a computed value such as my
_id example earlier, updates incurred because of dependent value changes happen automatically. I don't need to manually apply the recalculation logic in both view controllers--the model does it for me, and when I dispatch an action with my serialized model, I know I'm getting the latest and greatest.
Here's another example of that failure mode. Suppose
path is a composite property of
filename. If one view updates
dir, but forgets to update
path, you're in trouble! Cases like this could handled while
selecting the component state to compute the derived value. Also a memoized getter on
path could sometimes work. However, you have now defined Model definition logic inside of your controller. Boo! Yuck! Similarly, the validation steps discussed above (e.g. protect against crufty attrs like
fileSha) could take place in the action definition function. A weakness of that strategy may be performance penalties or unnecessary complexity, especially given the case where you are just intending to patch model attribute. We won't dive further into that!
There are weaknesses in my proposed strategy.
I have found neither of the above weaknesses to be a significant performance hit. In profiling a slow application, CPU times in my model internals were not visible in the "CPU-%-by-fn" sorted list. All model internal times were easily dwarfed by React internals, controller activity, and utility fns. Of course, if I start to render huge datasets, I may need to adjust my strategy slightly. I have also been diligent about purging unused state in the SSS by hooking into React lifecycle events, primarily
Formal Model definition structures provide great value