In this talk, we will discover how to use MobX in our application by looking at a real world example and extracting patterns from it. We will discover how to structure how project maximising the reactiveness of our data model, reducing the amount of duplicated data and the amount of bugs. We will also cover hidden features of mobx like interceptReads to discover how to introduce some useful magic in our domain logic.
25. ObservablesObservables
Store your application state
State may change over time
Observable instance may be the same
ActionsActions
Change observables state value
Can be triggered from UI or side effects
9
31. ComputedsComputeds
Derived state data
Automatically updated synchronusly
Always up to date with current
observable state
ReactionsReactions
Trigger functions when a condition changes
UI is a reaction of the store state
10
48. Few notes:Few notes:
Component state managed byComponent state managed by
MobX or not?MobX or not?
class App extends React.Component {
@observable
currentText = ""
// ...
}
export default observer(App)
class App extends React.Component {
state = {
currentText: ""
}
// ...
} 18
49. Few notes:Few notes:
Component state managed byComponent state managed by
MobX or not?MobX or not?
class App extends React.Component {
@observable
currentText = ""
// ...
}
export default observer(App)
19
50. Few notes:Few notes:
Component state managed byComponent state managed by
MobX or not?MobX or not?
Better optimized than setState
class App extends React.Component {
@observable
currentText = ""
// ...
}
export default observer(App)
19
51. Few notes:Few notes:
Component state managed byComponent state managed by
MobX or not?MobX or not?
Better optimized than setState
Simpler API than setState
class App extends React.Component {
@observable
currentText = ""
// ...
}
export default observer(App)
19
52. Few notes:Few notes:
Component state managed byComponent state managed by
MobX or not?MobX or not?
Better optimized than setState
Simpler API than setState
Easier to refactor to a separate store
class App extends React.Component {
@observable
currentText = ""
// ...
}
export default observer(App)
19
53. Few notes:Few notes:
Decorators are optionalDecorators are optional
class Store {
@observable
todos = []
}
class Store {
todos = []
}
decorate(Store, {
todos: observable
});
20
54. Few notes:Few notes:
Classes are optionalClasses are optional
class Store {
@observable
todos = []
}
const store = new Store()
const store = observable({
todos: []
})
21
67. Don't mix Business Logic & UIDon't mix Business Logic & UI
What if we want a React Native app later?
25
68. Don't mix Business Logic & UIDon't mix Business Logic & UI
What if we want a React Native app later?
What if UI framework changes?
25
69. Don't mix Business Logic & UIDon't mix Business Logic & UI
What if we want a React Native app later?
What if UI framework changes?
What if we need SSR?
25
70. Don't mix Business Logic & UIDon't mix Business Logic & UI
What if we want a React Native app later?
What if UI framework changes?
What if we need SSR?
25
71. Don't mix Business Logic & UIDon't mix Business Logic & UI
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;
}
}
Extracting the StoreExtracting the Store
26
72. 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);
Don't mix Business Logic & UIDon't mix Business Logic & UI
Wiring the Store as propWiring the Store as prop
27
73. 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);
Don't mix Business Logic & UIDon't mix Business Logic & UI
Use Provider & InjectUse Provider & Inject
28
74. 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);
Don't mix Business Logic & UIDon't mix Business Logic & UI
Use Provider & InjectUse Provider & Inject
No need to pass down manually your stores
28
75. 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);
Don't mix Business Logic & UIDon't mix Business Logic & UI
Use Provider & InjectUse Provider & Inject
No need to pass down manually your stores
Allows to change store implementation in your tests
28
76. 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);
Don't mix Business Logic & UIDon't mix Business Logic & UI
Use Provider & InjectUse Provider & Inject
No need to pass down manually your stores
Allows to change store implementation in your tests
Only one point of store injections into the views
28
77. Real-World ArchitectureReal-World Architecture
Current State RecapCurrent State Recap
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
29
78. Real-World ArchitectureReal-World Architecture
Current State RecapCurrent State Recap
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
W
ay too m
uch
W
ay too m
uch
things!
things!
29
81. Domain StateDomain State
WTF is dat?WTF is dat?
Is the domain of your app
in our example, of Todo
Describes the application entities and relations
30
82. Domain StateDomain State
WTF is dat?WTF is dat?
Is the domain of your app
in our example, of Todo
Describes the application entities and relations
Usually it is persistent and stored somewhere
30
83. Domain StateDomain State
WTF is dat?WTF is dat?
Is the domain of your app
in our example, of Todo
Describes the application entities and relations
Usually it is persistent and stored somewhere
Not tight with the UI
30
84. Domain StateDomain State
WTF is dat?WTF is dat?
Is the domain of your app
in our example, of Todo
Describes the application entities and relations
Usually it is persistent and stored somewhere
Not tight with the UI
Highly reusable
30
85. Domain StateDomain State
first implementationfirst implementation
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 = "";
};
}
31
86. Domain StateDomain State
domain actionsdomain actions
// 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!
32
87. Domain StateDomain State
let's be more real!let's be more real!
// domain todo model
class Todo {
name = "";
done = false;
user_id = 0; // UHM... is this ok?
// ...
}
// domain user model
class User {
id = 0
name = ""
}
33
88. Domain StateDomain State
tree or graph?tree or graph?
// 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 = ""
}
34
89. Domain StateDomain State
tree or graph?tree or graph?
// 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 = ""
}
Normalized
34
90. Domain StateDomain State
tree or graph?tree or graph?
// 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 = ""
}
Normalized
Object tree
34
91. Domain StateDomain State
tree or graph?tree or graph?
// 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 = ""
}
Normalized
Object tree
Easily serialize
34
92. Domain StateDomain State
tree or graph?tree or graph?
// 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 = ""
}
Normalized
Object tree
Easily serialize
Difficult access 34
93. Domain StateDomain State
tree or graph?tree or graph?
// 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 = ""
}
Normalized
Object tree
Easily serialize
Difficult access
Denormalized
34
94. Domain StateDomain State
tree or graph?tree or graph?
// 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 = ""
}
Normalized
Object tree
Easily serialize
Difficult access
Denormalized
Object graph
34
95. Domain StateDomain State
tree or graph?tree or graph?
// 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 = ""
}
Normalized
Object tree
Easily serialize
Difficult access
Denormalized
Object graph
Harder serialization
34
96. Domain StateDomain State
tree or graph?tree or graph?
// 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 = ""
}
Normalized
Object tree
Easily serialize
Difficult access
Denormalized
Object graph
Harder serialization
Easy access 34
98. Domain StateDomain State
lets do both!lets do both!
// 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
}) 36
99. Domain StateDomain State
lets do both!lets do both!
// 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
}) 36
102. Multiple Store CommunicationMultiple Store Communication
available pattersavailable patters
Singleton instance & require/import
Dependency injection framework
37
103. Multiple Store CommunicationMultiple Store Communication
available pattersavailable patters
Singleton instance & require/import
Dependency injection framework
Root store pattern
37
104. Multiple Store CommunicationMultiple Store Communication
root store patternroot store pattern
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 }
} 38
107. Multiple Store CommunicationMultiple Store Communication
root store patternroot store pattern
Central point for each store to
communicate
Strongly typed
39
108. Multiple Store CommunicationMultiple Store Communication
root store patternroot store pattern
Central point for each store to
communicate
Strongly typed
Works as dependency root
Can host environment specific variables
39
109. Multiple Store CommunicationMultiple Store Communication
root store patternroot store pattern
Central point for each store to
communicate
Strongly typed
Works as dependency root
Can host environment specific variables
Very easily testable
39
110. Multiple Store CommunicationMultiple Store Communication
back to our problemback to our problem
// 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
}
}
40
111. Multiple Store CommunicationMultiple Store Communication
back to our problemback to our problem
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);
41
112. Multiple Store CommunicationMultiple Store Communication
back to our problemback to our problem
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")
})
42
113. Real-World ArchitectureReal-World Architecture
Current State RecapCurrent State Recap
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
43
114. Real-World ArchitectureReal-World Architecture
Current State RecapCurrent State Recap
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
43
116. Domain Model SerializationDomain Model Serialization
turning JSON into observablesturning JSON into observables
Our state is an object tree
44
117. Domain Model SerializationDomain Model Serialization
turning JSON into observablesturning JSON into observables
Our state is an object tree
Unfortunately it's not a plain JS object
44
118. Domain Model SerializationDomain Model Serialization
turning JSON into observablesturning JSON into observables
Our state is an object tree
Unfortunately it's not a plain JS object
Most APIs provide JSON as intermediate language
44
119. Domain Model SerializationDomain Model Serialization
turning JSON into observablesturning JSON into observables
Our state is an object tree
Unfortunately it's not a plain JS object
Most APIs provide JSON as intermediate language
We need to provide conversions to serialize or
deserialize our state
44
120. Domain Model SerializationDomain Model Serialization
deserializationdeserialization
class Todo {
// ...
constructor(store, data){
this.store = store
if(data) Object.assign(this, data)
}
}
45
121. Domain Model SerializationDomain Model Serialization
serializationserialization
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 itderive
46
122. Domain Model SerializationDomain Model Serialization
packagespackages
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
47
123. Domain Model SerializationDomain Model Serialization
the deserialization problemthe deserialization problem
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!
48
124. MobX hidden featureMobX hidden feature
_interceptReads_interceptReads
import {_interceptReads} from "mobx"
const todos = observable.map()
_interceptReads(todos, value => value + "! LOL")
todos.set(1, "Mattia")
console.log(todos.get("1")) // => Mattia! LOL
undocumented (yet there since 2017) mobx feature
allows to transform mobx values while reading
available for objects, arrays, maps and boxed values
49
125. MobX hidden featureMobX hidden feature
_interceptReads to the rescue!_interceptReads to the rescue!
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]
}
}
50
128. MobX PerformanceMobX Performance
better performant domain storesbetter performant domain stores
Use ES6 Maps and lookup by ID when possible
Store JSON in an observable map
51
129. MobX PerformanceMobX Performance
better performant domain storesbetter performant domain stores
Use ES6 Maps and lookup by ID when possible
Store JSON in an observable map
Use _interceptReads to perform lazy deserialization
51
130. MobX PerformanceMobX Performance
better performant domain storesbetter performant domain stores
Use ES6 Maps and lookup by ID when possible
Store JSON in an observable map
Use _interceptReads to perform lazy deserialization
Implement ID -> name without deserialization
51
131. Real-World ArchitectureReal-World Architecture
Current State RecapCurrent State Recap
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
52
132. Real-World ArchitectureReal-World Architecture
Current State RecapCurrent State Recap
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
52
139. MobX Async TipsMobX Async Tips
reactive finite state machinesreactive finite state machines
<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>
57
140. Real-World ArchitectureReal-World Architecture
Current State RecapCurrent State Recap
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
SERVICE
DOMAIN MODEL
PRESENTER
Aggregates data for the view
Call actions on the services
58
144. Today's RecapToday's Recap
1. Use Redux if you're paid by the hour
2. Find your minimal state
3. Derive whatever is possible
59
145. Today's RecapToday's Recap
1. Use Redux if you're paid by the hour
2. Find your minimal state
3. Derive whatever is possible
4. Think as you're building for any UI environment
59
146. Today's RecapToday's Recap
1. Use Redux if you're paid by the hour
2. Find your minimal state
3. Derive whatever is possible
4. Think as you're building for any UI environment
5. Always experiment and have fun!
59
147. Want something opinionated?Want something opinionated?
1. Define store shape
2. Automatic serialization/deserialization
3. Implements both a immutable & mutable store
4. Defines a stricter architecture
5. Time travelling
discover mobx-state-treediscover mobx-state-tree
60
148. Not into mutable things?Not into mutable things?
stay here for the next talk!stay here for the next talk!
61
149. Thanks for your time!Thanks for your time!
Questions?Questions?
@MattiaManzati - slides.com/mattiamanzati
62