From Plain JS to Mobx (MST)

JS

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

0
 Advanced issues found
 

.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)

Made with Slides.com