@mattiamanzati
STORE ITS STATE
MODIFY ITS STATE
VIEW ITS STATE
AGGREGATED
PERFORM
SIDE EFFECTS
{ name: "work", done: false }
onClick = () =>{ todo.done = true }
get totalCount(){
return this.todos.length
}
???
Change observables state value
Can be triggered from UI or side effects
Trigger functions when a condition changes
UI is a reaction of the store state
CELLS
USER INTERACTION
FORMULAS
SCREEN UPDATES
class App extends React.Component {
// observable values
currentText = "";
todos = [];
// actions
addTodo = () => {
this.todos.push({ name: this.currentText, done: false });
this.currentText = "";
};
toggleTodo = todo => {
todo.done = !todo.done;
};
setCurrentText = text => {
this.currentText = text;
};
// computed
get pendingCount() {
return this.todos.filter(todo => !todo.done).length;
}
// ...
}
class App extends React.Component {
// ...
// render observer
render() {
return (
<div className="App">
<h1>TODOs</h1>
<input
type="text"
value={this.currentText}
onChange={e => this.setCurrentText(e.target.value)}
/>
<button onClick={this.addTodo}>Add</button>
<ul>
{this.todos.map(item => (
<li onClick={() => this.toggleTodo(item)}>
{item.name} {item.done ? <i>DONE!</i> : null}
</li>
))}
</ul>
<p>There are {this.pendingCount} pending todos.</p>
</div>
);
}
}
class App extends React.Component {
// ...
}
const MyApp = observer(App);
decorate(App, {
currentText: observable,
todos: observable,
addTodo: action,
toggleTodo: action,
setCurrentText: action,
pendingCount: computed
});
const rootElement = document.getElementById("root");
ReactDOM.render(<MyApp />, rootElement);
class App extends React.Component {
@observable
currentText = ""
// ...
}
export default observer(App)
class App extends React.Component {
state = {
currentText: ""
}
// ...
}
class App extends React.Component {
@observable
currentText = ""
// ...
}
export default observer(App)
class Store {
@observable
todos = []
}
class Store {
todos = []
}
decorate(Store, {
todos: observable
});
class Store {
@observable
todos = []
}
const store = new Store()
const store = observable({
todos: []
})
How do I X?
Should I MVC?
Should I MVVM?
Should I MVP?
Should I Flux?
class Store {
// observable values
currentText = "";
todos = [];
// actions
addTodo = () => {
this.todos.push({ name: this.currentText, done: false });
this.currentText = "";
};
toggleTodo = todo => { todo.done = !todo.done; };
setCurrentText = text => { this.currentText = text; };
// computed
get pendingCount() {
return this.todos.filter(todo => !todo.done).length;
}
}
class Store {
// ...
}
class App extends React.Component {
// ...
}
const MyApp = observer(Store);
const store = new Store();
const rootElement = document.getElementById("root");
ReactDOM.render(<MyApp store={store} />, rootElement);
import { observer, Provider, inject }
from "mobx-react";
// ...
const MyApp = inject("store")(observer(App));
const store = new Store();
const rootElement = document.getElementById("root");
ReactDOM.render(
<Provider store={store}>
<MyApp />
</Provider>, rootElement);
VIEW
STORE
Renders the view and
calls actions on the store
Holds and modify domain state
Holds and modify application state
Implements business logic
Fetch from API
Implements view actions
Aggregates data for the view
class Todo {
name = "";
done = false;
}
decorate(Todo, {
name: observable,
done: observable
});
class Store {
// ...
addTodo = () => {
const todo = new Todo();
todo.name = this.currentText;
this.todos.push(todo);
this.currentText = "";
};
}
// domain todo model
class Todo {
name = "";
done = false;
toggle(){
this.done = !this.done
}
}
decorate(Todo, {
// ...
toggle: action
});
Observables should be changed trough actions! So it's a good idea to define dumb actions on our domain model!
// domain todo model
class Todo {
name = "";
done = false;
user_id = 0; // UHM... is this ok?
// ...
}
// domain user model
class User {
id = 0
name = ""
}
// domain todo model
class Todo {
name = "";
done = false;
user_id = 0;
// ...
}
// domain user model
class User {
id = 0
name = ""
}
// domain todo model
class Todo {
name = "";
done = false;
user = new User(0, "");
// ...
}
// domain user model
class User {
id = 0
name = ""
}
// domain todo model
class Todo {
name = ""
done = false
user_id = 0
get user(){
return store.getUserById(this.user_id) // <- WTF
}
set user(value){
this.user_id = value.id
}
}
decorate(Todo, {
//...
user_id: observable,
user: computed
})
class RootStore {
todoStore = null;
fetch = null;
apiKey = "";
constructor(fetch, apiKey){
this.fetch = fetch
this.apiKey = apiKey
this.todoStore = new Store(this)
}
}
class Todo {
store = null;
constructor(store){ this.store = store }
}
class Store {
rootStore = null;
constructor(rootStore){ this.rootStore = rootStore }
}
// domain todo model
class Todo {
name = ""
done = false
user_id = 0
store = null;
constructor(store){ this.store = store; }
get user(){
return this.store.rootStore
.userStore.getUserById(this.user_id)
}
set user(value){
this.user_id = value.id
}
}
import { observer, Provider,
inject } from "mobx-react";
// ...
const MyApp = inject("store")(observer(App));
const store = new RootStore(window.fetch);
const rootElement = document.getElementById("root");
ReactDOM.render(
<Provider store={store}>
<MyApp />
</Provider>, rootElement);
test("it should restore data from API", async t => {
const fakeFetch = () => Promise.resolve({
data: [{id: 1, name: "Mattia"}]
})
const store = new RootStore(fakeFetch)
await store.userStore.fetchAll()
t.equal(store.userStore.users[0].name, "Mattia")
})
VIEW
STORE
Renders the view and
calls actions on the store
Holds domain state
Create and modify domain state
Holds and modify application state
Implements business logic
Fetch from API
Aggregates data for the view
DOMAIN MODEL
class Todo {
// ...
constructor(store, data){
this.store = store
if(data) Object.assign(this, data)
}
}
class Todo {
// ...
get toJSON(){
return {
name: this.name,
done: this.done
}
}
}
// ...
decorate(Todo, {
toJSON: computed
})
JSON is just another view of the domain model,
so we can just derive it
derive
class User {
@serializable(identifier()) id = 0;
@serializable name = '';
}
class Todo {
@serializable name = '';
@serializable done = false;
@serializable(object(User)) user = null;
}
// You can now deserialize and serialize!
const todo = deserialize(Todo, {
name: 'Hello world',
done: true,
user: { id: 1, name: 'Mattia' }
});
const todoJSON = serialize(todo)
with serializr you get that with tagging your domain models
class TodoStore {
todos = []
fromCache(){
const cachedData = localStorage.getItem("todos")
|| "[]"
this.todos = JSON.parse(cachedData)
.map(data => new Todo(this, data)
}
getById = id => this.todos
.find(item => item.id === id)
}
decorate(TodoStore, {
fromCache: action
})
deserializing is memory intensive!
import {_interceptReads} from "mobx"
const todos = observable.map()
_interceptReads(todos, value => value + "! LOL")
todos.set(1, "Mattia")
console.log(todos.get("1")) // => Mattia! LOL
class TodoStore {
todos = []
_cache = {}
constructor(rootStore){
this.rootStore = rootStore
// ...
_interceptReads(this.todos, this.unboxTodo)
}
unboxTodo = data => {
if(this._cache[data.id]){
return this._cache[data.id]
}
this._cache[data.id] = new Todo(this, data)
return this._cache[data.id]
}
}
VIEW
REPOSITORY
Renders the view and
calls actions on the store
Holds domain state
Create and modify domain state
Fetch from API
Holds and modify application state
Implements business logic
Aggregates data for the view
STORE
DOMAIN MODEL
class Overview {
rootStore = null
currentText = ""
constructor(rootStore){ this.rootStore = rootStore }
get todos(){
// perform sorting and pagining here
return this.rootStore.todoStore.todos
}
handleAddClicked(){
this.rootStore.todoStore.addTodo(this.currentText)
this.currentText = ""
}
}
decorate(Router, {
todos: computed,
currentText: observable,
handleAddClicked: action
})
class Router {
uri = "/"
page = null
// ...
constructor(rootStore){
reaction(() => this.uri, uri => this.parseUri(uri))
}
openOverview(){
this.page = { name: "overview", data: { project_id: 1 }}
}
}
decorate(Router, {
uri: observable,
page: observable
})
class Router {
uri = "/"
page = null
constructor(rootStore){
this.page = this.loadHome()
reaction(() => this.uri, uri => this.parseUri(uri))
}
openOverview(){
this.page = fromPromise(
this.overviewStore.fetchData()
.then(data =>
Promise.resolve({ name: "overview", data })
)
)
}
}
class Router {
uri = "/"
page = null
constructor(rootStore){
this.page = this.loadHome()
reaction(() => this.uri, uri => this.parseUri(uri))
}
openOverview(){
this.page = fromPromise(
this.overviewStore.fetchData()
.then(data =>
Promise.resolve({ name: "overview", data })
)
)
}
}
<div>
{ this.page.case({
pending: () => "loading",
rejected: (e) => "error: " + e,
fulfilled: ({name, data}) => {
switch(name){
case "home": return <HomePage data={data} />;
case "overview": return <Overview data={data} />;
}
}
})}
</div>
VIEW
REPOSITORY
Renders the view and
calls actions on the presenter
Holds domain state
Create and modify domain state
Fetch from API
Holds and modify application state
Implements business logic
SERVICE
DOMAIN MODEL
PRESENTER
Aggregates data for the view
Call actions on the services
const Todo = types.model({
name: types.string,
done: types.boolean
}).actions(
self => ({
toggle: () => self.done = !self.done
})
)
const work = Todo.create()
work.toggle()
const serializedData = getSnapshot(work)
// serializedData = {name: "Work!", done: false}
applySnapshot(work, { name: "Work!", done: true })
// work.done = true
import {onSnapshot, applySnapshot} from "mobx-state-tree"
const appStates = []
onSnapshot(work, newSnapshot =>
appStates.push(newSnapshot)
)
function travelAt(index){
applySnapshot(store, appStates[index])
}
import {asReduxStore}
from 'mobx-state-tree/middleware/redux'
onAction(store, msg => console.log(msg)) // => msg stream
applyAction(store, { type: "toggle", payload: []}) // => apply an msg
const reduxStore = asReduxStore(store) // => MST store as redux store
@MattiaManzati - slides.com/mattiamanzati