GitHub: https://bit.ly/2YvZ6hh
Prepared by Vladimir Cores (Minkin)
Spinner
1. Prototype
2. Classes
3. Webpack
4. React
5. Mobx
6. Mobx - optimization
7. Mobx - multi store
8. Mobx - state tree
Steps:
ONE GENERAL API
TO RULE THEM ALL
Every visual element "extended" from DomElement and become a collection of others DomElements - Tree structure.
.call(this, ...args) method allows you to invoke the function specifying in what context the function will be invoked (link).
objects can have a prototype = new Object, which acts as a template object that it inherits methods and properties from (link).
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/
View does not have business logic and parts don't know about each other, only through chain of domElements.
self-representation
inheritance
chain reaction through domElements
----------> internal state
Note:
No reference to ValueObject,
only plain values
Controller - setup and orchestrates the views, make decisions on user input (business logic).
Controller is a glue between data and view.
Because data is only for GALLERY then only one GalleryController is in use.
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.
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.
1. Again only GalleryController knows about structure of visual objects and how to setup them - addElement is a DomElement API.
2. Method update is also part of DomElement
delegate visual change to view
delegate visual change to view
Propagate change
Update propagated through DomElement API, the Lightroom has only one element - Image
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)
constructor requires new keyword to work.
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.
Enumerable properties show up in for...in loops unless the property's key is a Symbol (link).
If you don’t add a constructor to a class, a default empty constructor will be added automatically.
Code inside a class is always in strict mode.
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 declarations are not hoisted.
This means, you can’t use a class before it is declared, it will return not defined error.
Classes don’t allow the property value assignments like constructor functions or object literals.
You can only have functions or getters / setters. So no property:value assignments directly in the class.
classObject[expression]
When webpack processes your application, it internally builds a dependency graph which maps every module your project needs and generates one or more bundles.
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.
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.
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.
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']
}
}
}
]
}
// 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()
})()
A declarative library that keeps the DOM in sync with data.
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'))
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:
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:
State:
“top-down” or “unidirectional” data flow
componentDidMount() {
new GalleryLoader().load((data) => {
this.setState({
data: data,
loading: false
})
})
}
componentDidMount()
shouldComponentUpdate(nextProps, nextState)
Called before every re-render but not initially
render() {
return (<div> {this.state.loading ?
<Spinner/> : this.renderGallery()} </div>)
}
renderGallery() {
let images = this.state.data.images
let Controller = GalleryController(images)
return <Controller/>
}
Intermediate component that act as a HOC in terms of setup and keep of the data, and listen for document events
1. Parent-child composition
2. Data passed as props
3. State conditional rendering
1. Life-cycle for event binding, since it happens only once no need to use .bind()
2. Visual update by setState()
Make sure that everything that can be derived from the application state, will be derived. Automatically.
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.
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)
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
});
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
})
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++;
}
}
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
(stage-2 ESNext feature)
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.
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
}
}
...
}
Can be used to pick stores from React's context and pass it as props to the target component.
injected store
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({ ...
Actions are sources of change, like user events or web-server response.
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.
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.
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.
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;
}
}
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
})
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
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);
}
}
// 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.
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.
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.
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(
() => 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.
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"
with decorators enabled
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 (!)
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
@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)
@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]
}
instantiates all stores, and share references.
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
- complex
- primitive
- property
- utility
is the same as:
const Shape = types.union(Box, Square)
const ShapeLogger = types.compose(Shape, Logger)
for example with:
.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
unprotect(tree) will disable the protected mode of a tree allowing anyone to directly modify the tree.
Asynchronous actions are written by using generators and always return a promise.
@observer components can react to them
computed value
don't contain any type information
and are stripped from all actions and volatile state.
a stream of JSON-patches describing
which modifications were made.
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 can be reverse applied, which enables many powerful patterns like undo / redo
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"]
}
all hooks should be defined as actions, except for preProcessSnapshot and postProcessSnapshot
const Todo = types
.model({})
.volatile(self => ({
localState: 3
}))
.actions(self => ({
setX(value) {
self.localState = value
}
}))