Volatile state
egghead.io lesson 15: Use Volatile State and Lifecycle Methods to Manage Private State
MST models primarily aid in storing persistable state. State that can be persisted, serialized, transferred, patched, replaced, etc. However, sometimes you need to keep track of temporary, non-persistable state. This is called volatile state in MST. Examples include promises, sockets, DOM elements, etc. - state which is needed for local purposes as long as the object is alive.
Volatile state (which is also private) can be introduced by creating variables inside any of the action initializer functions.
Volatile is preserved for the life-time of an object and not reset when snapshots are applied, etc. Note that the life time of an object depends on proper reconciliation, see the how does reconciliation work? section.
The following is an example of an object with volatile state. Note that volatile state here is used to track a XHR request and clean up resources when it is disposed. Without volatile state this kind of information would need to be stored in an external WeakMap or something similar.
const Store = types
.model({
todos: types.array(Todo),
state: types.enumeration("State", ["loading", "loaded", "error"])
})
.actions(self => {
let pendingRequest = null // a Promise
function afterCreate() {
self.state = "loading"
pendingRequest = someXhrLib.createRequest("someEndpoint")
}
function beforeDestroy() {
// abort the request, no longer interested
pendingRequest.abort()
}
return {
afterCreate,
beforeDestroy
}
})
Some tips:
Note that multiple
actions
calls can be chained. This makes it possible to create multiple closures with their own protected volatile state.Although in the above example the
pendingRequest
could be initialized directly in the action initializer, it is recommended to do this in theafterCreate
hook, which will only be called once the entire instance has been set up (there might be many action and property initializers for a single type).The above example doesn't actually use the promise. For how to work with promises / asynchronous flows, see the asynchronous actions section.
It is possible to share volatile state between views and actions by using
extend
..extend
works like a combination of.actions
and.views
and should return an object with aactions
andviews
field:
Here's an example of how to do your own volatile state using an observable:
// if your local state is part of a view getter (computed) then
// it is important to make sure that state used such getters are observable,
// or else the value returned by the view would become stale upon observation
const Todo = types.model({}).extend(self => {
const localState = observable.box(3)
return {
views: {
// note this one IS a getter (computed value)
get x() {
return localState.get()
}
},
actions: {
setX(value) {
localState.set(value)
}
}
}
})
And here's an example of how to do your own volatile state not using an observable (but if you do this make sure the local state will never be used in a computed value first and bear in mind it won't be reactive!):
// if not using an observable then make sure your local state is NOT part of a view getter or computed value of any kind!
// also changes to it WON'T be reactive
const Todo = types.model({}).extend(self => {
let localState = 3
return {
views: {
// note this one is NOT a getter (NOT a computed value)
// if this were a getter this value would get stale upon observation
getX() {
return localState
}
},
actions: {
setX(value) {
localState = value
}
}
}
})
model.volatile
Since the pattern above (having a volatile state that is observable (in terms of Mobx observables) and readable from outside the instance) is such a common pattern there is a shorthand to declare such properties. The example above can be rewritten as:
const Todo = types
.model({})
.volatile(self => ({
localState: 3
}))
.actions(self => ({
setX(value) {
self.localState = value
}
}))
The object that is returned from the volatile
initializer function can contain any piece of data and will result in an instance property with the same name. Volatile properties have the following characteristics:
- They can be read from outside the model (if you want hidden volatile state, keep the state in your closure as shown in the previous section, and only if it is not used on a view consider not making it observable)
- The volatile properties will be only observable, see observable references. Values assigned to them will be unmodified and not automatically converted to deep observable structures.
- Like normal properties, they can only be modified through actions
- Volatile props will not show up in snapshots, and cannot be updated by applying snapshots
- Volatile props are preserved during the lifecycle of an instance. See also reconciliation
- Changes in volatile props won't show up in the patch or snapshot stream
- It is currently not supported to define getters / setters in the object returned by
volatile
- Volatile prop values aren't limited to values of MST's types and can be assigned any value. This includes JS primitives such as
string
,number
,Symbol
, Object types such asFunction
, POJOs, classes - and platform API's likelocalStorage
,window.fetch
and basically anything you want.