From Plain JS to Mobx (MST)
JS
GitHub: https://bit.ly/2YvZ6hh
Prepared by Vladimir Cores (Minkin)
What?
simple
image
gallery
Contains
- preloader
- thumbnails grid
- full image view
Entities
- Spinner
- Gallery
- Thumb
- LightRoom
- Image
Spinner
Steps:
1. Prototype
2. Classes
3. Webpack
4. React
5. Mobx
6. Mobx - optimization
7. Mobx - multi store
8. Mobx - state tree
Step 1. Prototype
All JavaScript objects inherit properties and methods from a prototype.
DOM Tree structure
Steps:
- Independent objects
- Prototype
- MVC
Layer of abstraction - DomElement
ONE GENERAL API
TO RULE THEM ALL
Every visual element "extended" from DomElement and become a collection of others DomElements - Tree structure.
Steps to Prototype
.call(this, ...args) method allows you to invoke the function specifying in what context the function will be invoked (link).
1. Inheriting properties:
2. Inheriting methods:
objects can have a prototype = new Object, which acts as a template object that it inherits methods and properties from (link).
1
2
MVC
MVC - Model
Model is a core of any application, responsible for maintaining and storing data. Development must starts with data design.
In out case all data is static and come from single source, no persistence data and no need to save state outside of the app.
see in folder: data/generator/
MVC - View
View does not have business logic and parts don't know about each other, only through chain of domElements.
View - Gallery
self-representation
inheritance
chain reaction through domElements
----------> internal state
View
Thumb
Note:
No reference to ValueObject,
only plain values
MVC - Controller
Controller - setup and orchestrates the views, make decisions on user input (business logic).
Controller is a glue between data and view.
MVC - Controller
Because data is only for GALLERY then only one GalleryController is in use.
1
2
1. Gallery view passed from outside, and lightroom view will be created dynamically and appended to parent.
2. Model is a part of the controller, initialized at a time when json data is ready by calling setup method.
Controller - setup gallery
1
2
2. GalleryController subscribe and handle user events
1. All children created outside of the gallery view. Thumbs don't know anything about Gallery and about data associated with it, only plain, "raw" values.
Thus children might be easily swapped with other visual elements, or they might be combined.
Controller - setup LigthRoom
1. Again only GalleryController knows about structure of visual objects and how to setup them - addElement is a DomElement API.
1
2. Method update is also part of DomElement
2
Controller - event handling
1
2
3
delegate visual change to view
delegate visual change to view
Changes propagation
DomElement
Visual change - Thumb.select
Propagate change
Visual change - Image.update
GalleryController
Update propagated through DomElement API, the Lightroom has only one element - Image
6.7 kb (not minimized)
Step 2. Classes
JavaScript classes are syntactic sugar over existing prototype-based inheritance, plus constructor function.
... classes is a new way of writing the constructor functions by utilizing the power of the prototype. (link)
- ES6 feature
Class difference from prototype
constructor requires new keyword to work.
1
VS
Class methods are non-enumerable. In JavaScript each property of an object has enumerable flag. A class sets this flag to false for all the methods defined on its prototype.
2
Enumerable properties show up in for...in loops unless the property's key is a Symbol (link).
Class difference from prototype
Class methods are Nonenumerable
If you don’t add a constructor to a class, a default empty constructor will be added automatically.
3
Class difference from prototype
Code inside a class is always in strict mode.
4
You can't, for example, use undeclared variables.
this helps to write error free code, by throwing errors on mis-typings, or syntactical errors done while writing code, or even removing some code by mistake, which is referenced from somewhere else.
Class difference from prototype
Class declarations are not hoisted.
5
This means, you can’t use a class before it is declared, it will return not defined error.
Class difference from prototype
Classes don’t allow the property value assignments like constructor functions or object literals.
6
Class difference from prototype
You can only have functions or getters / setters. So no property:value assignments directly in the class.
classObject[expression]
Class Features
1. Constructor
2. Static Methods
3. Getters/Setters
4. Inheritance
5.9 kb (not minimized)
Step 3. Webpack
When webpack processes your application, it internally builds a dependency graph which maps every module your project needs and generates one or more bundles.
- Static module bundler
Webpack Module:
- An ES2015 import statement
- A CommonJS require() statement
- An AMD define and require statement
- An @import statement inside of a css/sass/less file.
- An image url in a stylesheet (url(...)) or html (<img src=...>) file.
Supported Module Types
Loaders describe to webpack how to process non-JavaScript modules and include these dependencies into your bundles. Out of the box, webpack only understands JavaScript and JSON files.
- CoffeeScript
- TypeScript
- ESNext (Babel)
- Sass
- Less
- Stylusa
Core Concepts:
-
Entry
-
Output
-
Loaders
-
Plugins
-
Mode
-
Browser Compatibility
SETUP
npm init -y
npm install --save-dev webpack webpack-cli
scripts": {
"start": "webpack-dev-server --open",
"watch": "webpack --watch",
"build": "webpack"
},
Out of the box, webpack won't require you to use a configuration file. However, it will assume the entry point of your project is src/index and will output the result in dist/main.js minified and optimized for production.
SETUP
const webpack = require('webpack');
const path = require('path');
const config = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'main.js'
}
}
module.exports = config;
Create a webpack.config.js file in the root folder and webpack will automatically use it.
SETUP
Put index.html into dist folder with
<script src="main.js"></script>
SETUP
BABEL ES6
npm install babel-core babel-loader babel-preset-es2015 webpack --save-dev
module: {
rules: [
{
test: /\.m?js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: ['es2015']
}
}
}
]
}
40 kb (not minified)
4 kb (minified)
Main
// MAIN PROCESS
(function () {
const domRoot = document.getElementById("Root")
const gallery = new Gallery(domRoot)
const spinner = new Spinner(domRoot)
const galleryController = new GalleryController(gallery)
new GalleryLoader().load(function (jsonData) {
galleryController.setup(jsonData)
galleryController.select(0)
gallery.show()
spinner.hide()
})
spinner.show()
})()
Step 4. React
A declarative library that keeps the DOM in sync with data.
component, props, state, context, refs, hooks and lifecycle
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
loading: true,
data: null
}
}
componentDidMount() {
new GalleryLoader().load((data) => {
this.setState({
data: data,
loading: false
})
})
}
renderGallery() {
let images = this.state.data.images
let Controller = GalleryController(images)
return <Controller/>
}
render() {
return (<div> {this.state.loading ?
<Spinner/> : this.renderGallery()} </div>)
}
}
ReactDOM.render(<App/>, document.getElementById('Root'))
1
2
3
4
React elements are immutable.
Once you create an element,
you can’t change its children or attributes.
React Only Updates
What’s Necessary
React DOM compares the element and its children to the previous one, and only applies the DOM updates necessary to bring the DOM to the desired state.
Components:
- let you split the UI into independent, reusable pieces.
- can refer to other components in their output.
- composable with other components
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
loading: true,
data: null
}
}
}
// ====================================
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
function AppFunction() {
return (
<div>
<Welcome name="Sara" />
<Welcome name="Cahal" />
</div>
);
}
Props:
- are used to send (any) data to components.
- can't be changed in component (immutable)
State:
- local data, mutable (updates are merged)
- triggers re-render (setState)
- defines initial values
1
The Data Flows Down
-
Components don't know about each other
-
A component may pass its state down as props to its child components.
- Any data or UI derived from the state can only affect components “below” them in the tree.
“top-down” or “unidirectional” data flow
Life-cycle
componentDidMount() {
new GalleryLoader().load((data) => {
this.setState({
data: data,
loading: false
})
})
}
2
Life-cycle
componentDidMount()
-
Called once when component is constructed and gets added to the DOM (right after render).
- Could be used to fetch data and have it displayed right after rendering is done
shouldComponentUpdate(nextProps, nextState)
Called before every re-render but not initially
render() {
return (<div> {this.state.loading ?
<Spinner/> : this.renderGallery()} </div>)
}
3
Render
- only required method in a class component.
- will not be invoked if shouldComponentUpdate() returns false.
- make decisions about what should be shown as the result
renderGallery() {
let images = this.state.data.images
let Controller = GalleryController(images)
return <Controller/>
}
4
Controller
Intermediate component that act as a HOC in terms of setup and keep of the data, and listen for document events
4. GalleryController
- render
1. Parent-child composition
2. Data passed as props
3. State conditional rendering
3
2
1
4. GalleryController
- update
1. Life-cycle for event binding, since it happens only once no need to use .bind()
2. Visual update by setState()
1
2
Components
-
Visual representation of data from props.
- Simple logic to manage visual reflection of data.
162 kb (production, not minified)
~130 kb (production, minified)
Step 5. Mobx
Make sure that everything that can be derived from the application state, will be derived. Automatically.
-
Manage state independently of any view.
-
Transparently "hook up" the state to the UI.
- Reactivity.
Designed to:
Observable state
An observable state is any value that can be mutated and might serve as a source for computed values (or they also called formulas).
Observable values can be JS primitives, references, plain objects, class instances, arrays and maps.
Store
import GalleryLoader from './loader/GalleryLoader'
import {observable, action, computed} from 'mobx'
class GalleryStore {
@observable selectedIndex = 0
@observable data = null
constructor() {
this.requestData()
}
@action toggleLightRoom() {
this.lightRoomVisible = !this.lightRoomVisible
}
@action requestData() {
new GalleryLoader().load((data) => {
this.data = data
})
}
isSelected(index) {
return this.selectedIndex === index
}
@computed isDataLoading() {
return this.data == null
}
@computed get images() {
return this.data ? this.data.images : []
}
@computed get selectedImageVO() {
return this.images[this.selectedIndex]
}
}
export default GalleryStore
@observable
@action
function
@computed
Class object that contains properties and methods. Created only once!
Logic and state stay out of your components, inside a standalone testable unit that can be used in both frontend and backend.
Like Controller and Model together in MVC (link)
Store with function
import {observable, autorun, action} from "mobx";
var person = observable({
name: "John",
age: 42,
showAge: false,
get labelText() {
return this.showAge ? `${this.name} (age: ${this.age})` : this.name;
},
// action:
setAge(age) {
this.age = age;
}
}, {
setAge: action
// the other properties will default to observables / computed
});
Store with decorate
class Person {
name = "John"
age = 42
showAge = false
get labelText() {
return this.showAge ? `${this.name} (age: ${this.age})` : this.name;
}
setAge(age) { this.age = age; }
}
// when using decorate, all fields should be specified
// (a class might have many more non-observable internal fields after all)
decorate( Person, {
name: observable,
age: observable,
showAge: observable,
labelText: computed,
setAge: action
})
Store with decorators
import { observable, computed, action } from "mobx";
class OrderLine {
@observable price = 60;
@observable amount = 1;
@computed get total() {
return this.price * this.amount;
}
@action add() {
this.amount++;
}
}
Enable decorators - @
npm i --save-dev @babel/plugin-proposal-class-properties @babel/plugin-proposal-decorators
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', '@babel/preset-react'],
plugins: [
["@babel/plugin-proposal-decorators", { legacy: true }],
["@babel/plugin-proposal-class-properties", { "loose": true}]
]
}
}
webpack.config.js
Decorators
-
@observable - turn a property into an observable, observers will be notified and react to changes in those properties. The properties types can be objects, arrays or references.
-
@computed - values that will be derived automatically when relevant data is modified.
-
@observer (from mobx-react) - make react component reactive to the state change. Basically, it calls the component’s render function when the state changes.
-
@action - a method that changes the state (only).
- Provider and @inject - inject the store to the component (like connect in Redux).
(stage-2 ESNext feature)
@observable (properties)
When MobX creates an observable object, it introduces
observable properties (!) which by default use the
deep modifier.
The deep modifier basically recursively calls observable for any newly assigned value.
All complex values assigned to an observable will themselves be observable too.
Provider
import React from 'react'
import ReactDOM from 'react-dom'
import {Provider} from 'mobx-react'
import GalleryStore from './model/GalleryStore'
import GalleryController from './view/GalleryController'
ReactDOM.render(
<Provider store={new GalleryStore()}>
<GalleryController/>
</Provider>,
document.getElementById('Root')
)
Provider is a component that can pass stores (or other stuff) using React's context mechanism to child components.
Tip: it is recommended to use React.createContext instead
@inject('store')
export default class GalleryController extends React.Component {
get selectedImage() {
return this.store.selectedImageVO
}
onDocumentKeyboardNavigation = (event) => {
event = event || window.event
event.stopImmediatePropagation()
switch (event.keyCode) {
case Keyboard.ARROW_LEFT: this.store.selectNext(-1); break
case Keyboard.ARROW_RIGHT: this.store.selectNext(1); break
case Keyboard.ENTER: this.store.toggleLightRoom(); break
}
}
...
}
@inject
Can be used to pick stores from React's context and pass it as props to the target component.
injected store
@observer
It wraps the component's render function in mobx.autorun to make sure that any data that is used during the rendering of a component will force re-rendering when data has changed.
observer(React.createClass({ ...
MobX only tracks data from @observer component if this data directly used in render
Actions
Actions are sources of change, like user events or web-server response.
@Actions
Actions modify the observable state
An action indicates a function that will update state.
They are the inverse of reactions, which respond to state updates.
Computed values
Any value that can be computed by using a function that operates on and use other observable values.
Computed values are observable themselves and also called
derivations.
In spreadsheet terms: these are the formulas and charts of your application.
For @computed values MobX determines whether they are in use somewhere. This means that computed values can automatically be suspended
and garbage collected.
This saves tons of boilerplate and has a significant positive impact on performance.
Computed values are cached automatically. This means that reading a computed value will not re-run the computation as long as none of the involved observables has changed.
@computed
-
Computed values are automatically derived from your state if any values that affecting them changed.
-
@computed won't re-run if none of the data used in the previous computation changed.
- Won't re-run if is not in use by some other computed property or reaction, then it will be suspended.
If a computed value is no longer observed, for example the UI in which it was used no longer exists, MobX can automatically garbage collect it.
@computed with decorators
import {observable, computed} from "mobx";
class OrderLine {
@observable price = 0;
@observable amount = 1;
constructor(price) {
this.price = price;
}
@computed get total() {
return this.price * this.amount;
}
}
@computed with decorate
import {decorate, observable, computed} from "mobx";
class OrderLine {
price = 0;
amount = 1;
constructor(price) {
this.price = price;
}
get total() {
return this.price * this.amount;
}
}
decorate(OrderLine, {
price: observable,
amount: observable,
total: computed
})
@computed with observable.object
const orderLine = observable.object({
price: 0,
amount: 1,
get total() {
return this.price * this.amount
}
})
Both observable.object and extendObservable will automatically infer getter properties to be computed properties
become computed automatically
@computed setters
const orderLine = observable.object({
price: 0,
amount: 1,
get total() {
return this.price * this.amount
},
set total(total) {
// infer price from total
this.price = total / this.amount
}
})
Setters cannot be used to alter the value of the computed property directly, but they can be used as action ('inverse' of the derivation).
class Foo {
@observable length = 2;
@computed get squared() {
return this.length * this.length;
}
set squared(value) {
// this is automatically an action,
// no annotation necessary
this.length = Math.sqrt(value);
}
}
@computed with parameters
// Parameterized computed views:
// Create computed's and store them in a cache
import { observable, computed } from "mobx"
import { computedFn } from "mobx-utils"
class Todos {
@observable todos = []
getAllTodosByUser = computedFn(function (userId) {
return this.todos.filter(todo => todo.user === userId))
})
}
npm install mobx-utils --save
don't use arrow functions as the this would be incorrect.
@computed as function
import {observable, computed} from "mobx";
var name = observable.box("John");
var upperCaseName = computed(() =>
name.get().toUpperCase()
);
var disposer = upperCaseName.observe(change => console.log(change.newValue));
name.set("Dave");
// prints: 'DAVE'
Creates a stand-alone observable.
Reactions
- autorun ( reaction => { data /* do some stuff */ } )
- when (predicate: () => boolean, effect?: () => void, options?)
- reaction (() => data, (data, reaction) => { sideEffect }, options?)
Reaction: autorun ( reaction => { /* do some stuff */ } )
The provided function will be triggered once immediately
and then again each time one of its dependencies changes.
autorun((reaction) =>
{
console.log(`> autorun: selectedIndex has changed: ${this.selectedIndex}`)
if (this.selectedIndex === 5) {
reaction.dispose();
throw new Error("Selected value is 5")
}
},
{
onError(e) {
console.error(`> autorun: Error, reaction disposed: ${e}`)
},
scheduler: run => { setTimeout(run, 1000) }},
delay: 3
})
the function that should run automatically,
initiating effects, but doesn't produce a new value.
Reaction: when (predicate: () => bool, effect?: () => void, opts?)
observes & runs the given predicate until it returns true
then the effect is executed and the autorunner is disposed.
Returns a disposer to cancel the autorunner prematurely.
when(
() => this.lightRoomVisible && this.selectedIndex === 2,
() => {
console.log('> when: Lightroom show third item')
}
)
EXECUTED ONLY ONCE
If there is no effect function provided, when will return a Promise, this can be used in async / await
Reaction: reaction
reaction(
() => this.selectedIndex,
(selectedIndex, reaction) => {
console.log(`> reaction: selectedIndex = ${selectedIndex}`)
new GalleryLoader().updateSettings("selectedIndex", selectedIndex)
}
)
reaction(() => data, (data, reaction) => { sideEffect }, options?)
the side effect runs only after the data expression returns a new value.
-
Observer only subscribe to the data structures that were actively used during the last render.
-
@observer implements shouldComponentUpdate in the same way as PureComponent so that children are not re-rendered unnecessary.
-
Parent components won't re-render unnecessarily even when child components will.
- Props object and the state object of an observer component are automatically made observable.
Reaction: @observer
MobX tracks property access, not values
MobX only tracks synchronous data
autorun(() => {
setTimeout(
() => console.log(message.likes.join(", ")),
10
)
})
message.likes.push("Jennifer");
function upperCaseAuthorName(author) {
const baseName = author.name;
return baseName.toUpperCase();
}
autorun(() => {
console.log(upperCaseAuthorName(message.author))
})
message.author.name = "Chesterton"
WON'T
WILL
Computed values should always be preferred over reactions.
377 kb (production) / ~180 kb (minified)
440 kb (production) / ~220 kb (minified)
with decorators enabled
Step 6. Mobx - optimization
MobX cannot make primitive values observable.
Render optimization WRONG
renderThumbs = () => this.store.images.map((imageVO, index) => {
let thumbVO = imageVO.thumb
let thumbUrl = thumbVO.path + thumbVO.name;
return <Thumb
key = {index}
selected = {this.store.isSelected(index)}
width = {thumbVO.width}
height = {thumbVO.height}
url = {thumbUrl}
/>
})
render() {
console.log("re-render all")
return this.store.dataLoading ? <Spinner/> : this.renderGallery()
}
unpacked
values (!)
Render optimization
Props and the state
of an @observer component are
automatically made observable.
Render optimization OBSERVABLE PROPS
renderThumbs = () => this.store.images.map((imageVO, index) => {
return <Thumb key={index} data={imageVO.thumb}/>
})
thumb: {
width: IMAGE_THUMB_WIDTH,
height: IMAGE_THUMB_HEIGHT,
path: OUTPUT_DIR,
name: thumbName,
selected: false
}
passing object as prop, which is observable automatically
Render optimization USE OBJECT
@observer class Thumb extends DomElement {
get thumb() { return this.props.data }
get className() {
return super.className + (this.thumb.selected ? ' highlight' : '')
}
render() {
const style = {
width: this.thumb.width,
height: this.thumb.height,
backgroundImage: `url(${this.thumb.path + this.thumb.name})`
}
return (<div className={this.className} ref="dom" style={style}/>)
}
}
trackable property (in Mobx)
Render optimization SELECTED
@observable selectedIndex = 0
@observable selectedImage = { image:null }
@action selectNext(offset) {
let amountOfImages = this.images.length
let nextSelectedIndex = this.selectedIndex + offset
nextSelectedIndex = (nextSelectedIndex < 0 ?
amountOfImages - 1 : nextSelectedIndex) % amountOfImages
this.selectedImageVO.thumb.selected = false
this.selectedIndex = nextSelectedIndex
this.selectedImage.image = this.selectedImageVO
this.selectedImageVO.thumb.selected = true
}
@computed get selectedImageVO() {
return this.images[this.selectedIndex]
}
Render optimization RULE
The values are not observable, but the properties of an object are.
Step 7. Mobx - multi store
The main responsibility of stores is to move logic and state out of your components into a standalone testable unit that can be used in both frontend and backend JavaScript.
RootStore
instantiates all stores, and share references.
Thus an application might be broken into parts,
each of which could represent MVC structure
Each item in gallery/view might be extended from GalleryComponent, make it easy to access the shared store.
class RootStore {
constructor() {
this.userStore = new UserStore(this)
this.todoStore = new TodoStore(this)
}
}
class UserStore {
constructor(rootStore) {
this.rootStore = rootStore
}
getTodos(user) {
// access todoStore through the root store
return this.rootStore.todoStore.todos.filter(todo => todo.author === user)
}
}
class TodoStore {
@observable todos = []
constructor(rootStore) {
this.rootStore = rootStore
}
}
They will have access to the RootStore to be able to use each other
Step 8. Mobx - State Tree
Mobx-State-Tree is a library that forces a
strict architecture on state organization by
trees of models.
State Tree limiting state to be a tree of serializable values (model), and backing it with a structurally shared immutable tree (snapshot)
Living Tree
Tree is mutable,
it contains strictly protected objects enriched with runtime type information
Composition Tree
0. Named
1. Strongly typed
2. Identifier
3. Reference
4. Arrays of models
5. Optional
1
2
3
4
5
0
Types
-
model
-
array
-
map
-
string
-
integer
-
boolean
-
number
-
Date
-
custom
-
identifier
-
identifierNumber
-
late
-
literal
-
maybe
-
maybeNull
-
null
-
compose
-
enumeration
-
frozen
-
optional
-
reference
-
refinement
-
safeReference
-
snapshotProcessor
-
undefined
-
union
- complex
- primitive
- property
- utility
Each node in the tree is described:
-
type (the shape of the thing)
-
data (the state it is currently in)
is the same as:
endpoint: "http://localhost"
types.optional(types.string, "http://localhost")
Simulate inheritance by using type composition
const Shape = types.union(Box, Square)
const ShapeLogger = types.compose(Shape, Logger)
for example with:
Inheritance
.named(name) clones the current type, but gives it a new name
.props(props) produces a new type, and adds / overrides the specified properties
.actions(self => {.actions}) produces a new type, and adds / overrides the specified actions
.views(self => {get/set or functions}) produces a new type, and adds / overrides the specified view functions
types.model creates a chainable model type, where each chained method produces a new type:
Actions
nodes can only be modified by one of their actions, or by actions higher up in the tree
unprotect(tree) will disable the protected mode of a tree allowing anyone to directly modify the tree.
Actions API
- onAction listens to any action that is invoked on the model or any of its descendants.
- addMiddleware adds an interceptor function to any action invoked on the subtree.
- applyAction invokes an action on the model according to the given action description
Asynchronous Actions
Asynchronous actions are written by using generators and always return a promise.
Use flow()
Views
- with arguments
- without arguments
(computed values)
@observer components can react to them
computed value
-
State is a tree of models
-
Models are mutable, observable, rich
-
Snapshot immutable representation of the state of a model
Snapshots
From a living tree, immutable snapshots are automatically generated.
getSnapshot (node, applyPostProcess).
onSnapshot (model, callback)
applySnapshot (model, snapshot)
Snapshots API
don't contain any type information
and are stripped from all actions and volatile state.
Snapshots
a stream of JSON-patches describing
which modifications were made.
Patches
export interface IJsonPatch {
op: "replace" | "add" | "remove"
path: string
value?: any
}
- a single mutation can result in multiple patches
- emitted immediately when a mutation is made
- the path to the place where the event listener is attached
Patches API
- onPatch(model, listener)
- applyPatch(model, patch)
Patches can be reverse applied, which enables many powerful patterns like undo / redo
onPatch vs onAction
onPatch(storeInstance, patch => {
console.dir("Got change: ", patch)
})
storeInstance.todos[0].setTitle("Add milk")
// prints:
{
path: "/todos/0",
op: "replace",
value: "Add milk"
}
onAction(storeInstance, call => {
console.dir("Action was called: ", call)
})
storeInstance.todos[0].setTitle("Add milk")
// prints:
{
path: "/todos/0",
name: "setTitle",
args: ["Add milk"]
}
LifeCycle hooks for types.model
- afterCreate
- afterAttach
- beforeDetach
- beforeDestroy
- preProcessSnapshot
- postProcessSnapshot
all hooks should be defined as actions, except for preProcessSnapshot and postProcessSnapshot
Volatile state
- Volatile props will not show up in snapshots, and cannot be updated by applying snapshots.
- The can be read from outside the model.
- Can only be modified through actions.
- Preserved during the lifecycle of an instance.
const Todo = types
.model({})
.volatile(self => ({
localState: 3
}))
.actions(self => ({
setX(value) {
self.localState = value
}
}))
682 Kb / ~ 320 Kb (minified)
PlainJS to Mobx (MST)
By Vladimir Cores Minkin
PlainJS to Mobx (MST)
- 1,000