Skip to content

Latest commit

 

History

History
1044 lines (756 loc) · 13.7 KB

slides.md

File metadata and controls

1044 lines (756 loc) · 13.7 KB

The Quest For Immer Mutable State Management

Michel Weststrate - @mweststrate - ReactiveConf 2016

MobX - Mendix

.appear[ ]

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

What Happened Next

.appear[ Second most popular state management library. http://stateofjs.com/2016/statemanagement ]


The Reactions

It's so simple and fast :)


The Reactions

It's magic :(


The Reactions

It's unopinionated :)


The Reactions

It's unopinionated :(


The Reactions

It uses mutable data :)


The Reactions

It uses mutable data :(


Immutable or Mutable Data?


Redux or MobX?


.background[ frozen ]


Immutable Data

.lighten.background[ frozen ]

  1. State snapshots
  2. Replayable actions
  3. State hydration
  4. Traceability
  5. Time travelling

Observable, Mutable Data

.lighten.background[ frozen ]

  1. Excels at complex, coupled domains
  2. And complex, deep calculations
  3. Mimimal boilerplate
  4. Efficient
  5. Unopinionated
  6. 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 ]


The Quest For

A minimally defined, snapshot-able state container with replayable, KISS actions and efficient, transparent reactive derivations


class: fullscreen

frozen


Demo


.boring[

const states = []

] .appear[

autorun(() => {

]

    snapshot = serialize(state)
    states.push(snapshot)

.appear[

})

]


A snapshot is a live transformation of the state


Problems

  1. .appear[No standardized serialization .appear[(“serializr” package helps)]]
  2. .appear[Deep serializing state is expensive]
  3. .appear[No structural sharing]

Solutions

  1. .appear[Trees are easy to serialize]
  2. .appear[A snapshot is a derived value]
  3. .appear[Rendering a tree with structural sharing?
    Solved problem]

MobX computed values


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


Snapshotting Observable Mutable Data

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

}

]


Snapshotting Observable Mutable Data

.boring[

class TodoStore {
    @observable todos = []

]

    @computed json() {
        return this.todos.map(
            todo => todo.json
        )
    }

.boring[

}

]


class: fullscreen stacked whitebg

.appear[snapshot] .appear[snapshot] .appear[snapshot] .appear[snapshot] .appear[snapshot] .appear[snapshot]


mobx-state-tree

Opinionated, MobX powered state container

https://github.com/mobxjs/mobx-state-tree


Core Concepts

.appear[state is a tree of models]

.appear[models are mutable, observable, rich]

.appear[snapshot: immutable representation of the state of a model]


Factories

.appear[

const myModelFactory = createFactory({
    /* exampleModel */

    // properties
    // computed values
    // actions
})

]

.appear[

// returns fn:
snapshot => observable({...exampleModel, ...snapshot })

]


Factories

.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!" })

]

Factories

.boring[

import {createFactory, mapOf, arrayOf} from "mobx-state-tree"

]

const Store = createFactory({
    boxes: mapOf(Box),
    arrows: arrayOf(Arrow),
    selection: ""
})

.lighten.background[ snapshots ]

Snapshots

Representation of the state of a model
at a particular moment in time


Snapshots

    getSnapshot(model): snapshot
    applySnapshot(model, snapshot)
    onSnapshot(model, callback)

Time Travelling

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])
}

Snapshots & Forms

const todoEditor({todo}) => (
    <TodoEditForm
        todo={clone(todo)}
        onSubmit={
            (modifiedTodo) => {
                applySnapshot(todo, getSnapshot(modifiedTodo))
            }
        }
    />
)

.appear[

function clone(model) {
    return getFactory(model)(getSnapshot(model))
}

]


Snapshots & Testing

const todo = clone(exampleTodo)

todo.markCompleted()

assert.deepEqual(getSnapshot(todo), {
    title: "test", completed: true
})

Snapshots & Jest

jest

expect(getSnapshot(todo)).toMatchSnapshot()

Demo


Snapshots & Syncing

onSnapshot(store, (data) => {
    socketSend(data)
})

onSocketMessage((data) => {
    applySnapshot(store, data)
})

.lighten.background[ frozne ]

Patches

JSON-patch rfc6902

Changes need to be broadcasted!


Patches

    onPatch(model, calback)
    applyPatch(model, jsonPatch)

Demo


Patches & Syncing

onPatch(store, (data) => {
    socketSend(data)
})

onSocketMessage((data) => {
    applyPatch(store, data)
})

Patches

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[patch] .appear[patch] .appear[patch] .appear[patch]


.lighten.background[ frozne ]

Actions


What if an action description is the effect,

instead of the cause of a function call?


Actions

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

]

Actions

    onAction(model, callback)
    applyAction(model, actionCall)

Actions & Middleware

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


Actions & Syncing

onAction(store, (data, next) => {
    const res = next()
    socketSend(data)
    return res
})

onSocketMessage((data) => {
    applyAction(store, data)
})

Actions & Forms

function editTodo(todo) {
    const todoCopy = clone(todo)
    const actionLog = []

    onAction(todoCopy, (action, next) => {
        actionLog.push(action)
        return next()
    })

    showEditForm(todoCopy, () => {
        applyActions(todo, actionLog)
    })
}

Actions

  • Based on MobX actions
  • Unlock part of the state tree for editing
  • Emit action events, apply middleware
  • Straight forward
  • Bound

.lighten.background[ ]

References


References

const myFavoriteBox = store.boxes.get("abc123")

store.selection = myFavoriteBox

.appear[

//  Throws: element is already part of a state tree

]

.appear[

store.selection = myFavoriteBox.id

]


References

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

})

]

References

const Store = createFactory({
    boxes: mapOf(Box),
    selection: referenceTo("/boxes/id")
})

.appear[

const myFavoriteBox = store.boxes.get("abc123")

store.selection = myFavoriteBox

]


mobx-state-tree

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..._]


.background[ frozen ]


Demo


redux actions

redux dispatching

redux provider & connect

redux devtools

.appear[

redux store

redux reducers ] .appear[

mobx-state-tree factories

mobx-state-tree actions

]

.background[ ]


.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[ ]

.appear[ ]

.appear[

egghead.io/courses/mobx-fundamentals

@mweststrate

]

What about ELM?

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