@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
window. For some of us, we even maintain state in objects that are
designed entirely to maintain state. Think
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:
Fileas an example. I can define
pathnameas 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
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
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
filename. If one view updates
dir, but forgets to
path, you're in trouble! Cases like this could handled while
selecting the component state to compute the derived value. Also a memoized
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