Michel Weststrate - @mweststrate - ReactiveConf 2016
MobX - Mendix
Developers are too smart
.appear[(and too expensive)]
.appear[to have them do stupid adminstrative tasks]
.appear[(that can be done way better by computers anyway)]
.appear[Manual releases → Continuous Deployment]
.appear[Manipulating the DOM → Components + VDOM]
.appear[Managing data flow → Transparent Reactive Programming]
.appear[
const person = {
name: "michel",
age: 31
}
].appear[
const App = ({ person }) => <h1>{ person.name }</h1>
].appear[
ReactDOM.render(<App person={person} />, document.body)
].appear[
person.name = "@mweststrate"
]
.layer1[ .appear[
const person = observable({
name: "michel",
age: 31
})
].appear[
const App = observer(({ person }) => <h1>{ person.name }</h1>)
ReactDOM.render(<App person={person} />, document.body)
person.name = "@mweststrate"
The view is a function of the state
view = f(state)
The view is a transformation of the state
view = f(state)
The view is a live transformation of the state
mobx.autorun(() => {
view = f(state)
})
.appear[ Second most popular state management library. http://stateofjs.com/2016/statemanagement ]
It's so simple and fast :)
It's magic :(
It's unopinionated :)
It's unopinionated :(
It uses mutable data :)
It uses mutable data :(
- State snapshots
- Replayable actions
- State hydration
- Traceability
- Time travelling
- Excels at complex, coupled domains
- And complex, deep calculations
- Mimimal boilerplate
- Efficient
- Unopinionated
- Encourages strong typing
The relevance of each benefit is different in each project.
.appear[ What are the driving principles? ]
.appear[
Redux
Predictability through transactional state
]
.appear[
MobX
Simplicity through minimally defined,
automatically derived state
]
A minimally defined, snapshot-able state container with replayable, KISS actions and efficient, transparent reactive derivations
class: fullscreen
Demo
.boring[
const states = []
] .appear[
autorun(() => {
]
snapshot = serialize(state)
states.push(snapshot)
.appear[
})
]
A snapshot is a live transformation of the state
- .appear[No standardized serialization .appear[(“serializr” package helps)]]
- .appear[Deep serializing state is expensive]
- .appear[No structural sharing]
- .appear[Trees are easy to serialize]
- .appear[A snapshot is a derived value]
- .appear[Rendering a tree with structural sharing?
Solved problem]
class Person {
firstName = "Michel"
lastName = "Weststrate"
get fullName() {
console.log("calculating!")
return [this.firstName, this.lastName]
}
}
person.firstName = "John"
console.log(person.fullName)
// calculating!
console.log(person.fullName)
// calculating!
Pull Based: Recompute every time value is needed
class Person {
@observable firstName = "Michel"
@observable lastName = "Weststrate"
@computed get fullName() {
console.log("calculating!")
return [this.firstName, this.lastName]
}
}
person.firstName = "John"
// calculating!
console.log(person.fullName)
console.log(person.fullName)
Push Based: Recompute when a source value changes
.boring[
class Todo {
@observable id = 0
@observable text = ""
@observable completed = false
]
.appear[
@computed get json() {
return {
id: this.id,
text: this.text,
completed: this.completed
}
}
]
.boring[
}
]
.boring[
class TodoStore {
@observable todos = []
]
@computed json() {
return this.todos.map(
todo => todo.json
)
}
.boring[
}
]
class: fullscreen stacked whitebg
.appear[]
.appear[
]
.appear[
]
.appear[
]
.appear[
]
.appear[
]
Opinionated, MobX powered state container
https://github.com/mobxjs/mobx-state-tree
.appear[state is a tree of models]
.appear[models are mutable, observable, rich]
.appear[snapshot: immutable representation of the state of a model]
.appear[
const myModelFactory = createFactory({
/* exampleModel */
// properties
// computed values
// actions
})
]
.appear[
// returns fn:
snapshot => observable({...exampleModel, ...snapshot })
]
.boring[
import {createFactory} from "mobx-state-tree"
]
const Box = createFactory({
name: "A cool box instance",
x: 0,
y: 0,
get width() {
return this.name.length * 15;
}
})
.appear[
const box1 = Box({ name: "Hello, Reactive2016!" })
.boring[
import {createFactory, mapOf, arrayOf} from "mobx-state-tree"
]
const Store = createFactory({
boxes: mapOf(Box),
arrows: arrayOf(Arrow),
selection: ""
})
Representation of the state of a model
at a particular moment in time
getSnapshot(model): snapshot
applySnapshot(model, snapshot)
onSnapshot(model, callback)
const states = [];
let currentFrame = -1;
onSnapshot(store, snapshot => {
if (currentFrame === states.length -1) {
currentFrame++
states.push(snapshot);
}
})
function previousState() {
if (--currentFrame >= 0)
applySnapshot(store, states[currentFrame])
}
const todoEditor({todo}) => (
<TodoEditForm
todo={clone(todo)}
onSubmit={
(modifiedTodo) => {
applySnapshot(todo, getSnapshot(modifiedTodo))
}
}
/>
)
.appear[
function clone(model) {
return getFactory(model)(getSnapshot(model))
}
]
const todo = clone(exampleTodo)
todo.markCompleted()
assert.deepEqual(getSnapshot(todo), {
title: "test", completed: true
})
expect(getSnapshot(todo)).toMatchSnapshot()
Demo
onSnapshot(store, (data) => {
socketSend(data)
})
onSocketMessage((data) => {
applySnapshot(store, data)
})
JSON-patch rfc6902
Changes need to be broadcasted!
onPatch(model, calback)
applyPatch(model, jsonPatch)
Demo
onPatch(store, (data) => {
socketSend(data)
})
onSocketMessage((data) => {
applyPatch(store, data)
})
onPatch(store, patch => console.dir(patch))
onPatch(store.box.get("0d42afa6"), patch => console.dir(patch))
.appear[
store.box.get("0d42afa6").move(5, 0)
] .appear[
// output:
{ op: "replace", path: "/boxes/0d42afa6/x", value: 105 }
{ op: "replace", path: "/x", value: 105 }
class: fullscreen stacked whitebg
.appear[]
.appear[
]
.appear[
]
.appear[
]
What if an action description is the effect,
instead of the cause of a function call?
.boring[
const Box = createFactory({
x: 0,
y: 0,
]
move: action(function(dx, dy) {
this.x += dx
this.y += dy
})
.boring[
})
] .appear[
box1.move(10, 10)
onAction(model, callback)
applyAction(model, actionCall)
onAction(store, (action, next) => {
console.dir(action)
return next()
})
store.get("ce9131ee").move(23, -8)
.appear[
// prints:
{
"name":"move",
"path":"/boxes/ce9131ee",
"args":[23,-8]
}
Demo
onAction(store, (data, next) => {
const res = next()
socketSend(data)
return res
})
onSocketMessage((data) => {
applyAction(store, data)
})
function editTodo(todo) {
const todoCopy = clone(todo)
const actionLog = []
onAction(todoCopy, (action, next) => {
actionLog.push(action)
return next()
})
showEditForm(todoCopy, () => {
applyActions(todo, actionLog)
})
}
- Based on MobX actions
- Unlock part of the state tree for editing
- Emit action events, apply middleware
- Straight forward
- Bound
const myFavoriteBox = store.boxes.get("abc123")
store.selection = myFavoriteBox
.appear[
// Throws: element is already part of a state tree
]
.appear[
store.selection = myFavoriteBox.id
]
.boring[
const Store = createFactory({
boxes: mapOf(Box),
]
selectionId: '',
get selection() {
return this.selectionId ? this.boxes.get(this.selectionId) : null
},
set selection(value) {
this.selectionId = value ? value.id : null
}
.boring[
})
const Store = createFactory({
boxes: mapOf(Box),
selection: referenceTo("/boxes/id")
})
.appear[
const myFavoriteBox = store.boxes.get("abc123")
store.selection = myFavoriteBox
]
A minimally defined,
snapshot-able .appear[ √]
state container .appear[ √]
with replayable actions .appear[ √]
and efficient, transparent reactive derivations .appear[ √]
.appear[_ & ... patches, middleware, references, dependency injection..._]
Demo
redux actions
redux dispatching
redux provider & connect
redux devtools
.appear[
redux store
redux reducers ] .appear[
mobx-state-tree factories
mobx-state-tree actions
.boring[
const initialState = {
todos: [{
text: 'learn mobx-state-tree',
completed: false,
id: 0
}]
}
]
const store = TodoStore(initialState)
const reduxStore = asReduxStore(store)
render(
<Provider store={reduxStore}>
<App />
</Provider>,
document.getElementById('root')
)
.boring[
connectReduxDevtools(store)
]
function asReduxStore(model) {
return {
getState : () => getSnapshot(model),
dispatch : action => {
applyAction(model, reduxActionToAction(action))
},
subscribe: listener => onSnapshot(model, listener),
}
}
const Todo = createFactory({
text: 'Use mobx-state-tree',
completed: false,
id: 0
})
const TodoStore = createFactory({
todos: arrayOf(Todo),
COMPLETE_TODO: action(function ({id}) {
const todo = this.findTodoById(id)
todo.completed = !todo.completed
}),
.boring[
findTodoById: function (id) {
return this.todos.find(todo => todo.id === id)
}
})
Demo
.appear[Try mobx-state-tree]
.appear[Transactional state is just reactive transformation away]
.appear[
egghead.io/courses/mobx-fundamentals
@mweststrate
import cool from "my-cool-store"
const app = Elm.Main.embed(myHtmlElement);
app.ports.myPort.subscribe(data => {
applySnapshot(cool.part.of.the.state, data)
})
onSnapshot(cool.part.of.the.state, snapshot => {
app.ports.myPort.send(snapshot)
})