MobX is a battle tested library that makes state management simple and scalable by transparently applying functional reactive programming (TFRP).
Anything that can be derived from the application state, should be derived.
Automatically.
- MobX
In het kort zijn Decorators gewoon "fancy" mixins / Higher Order Functions (HOC). De volgende code doet hetzelfde.
class User {
name = 'Sander';
age = 27;
}
withSomeHoc(User);
@withSomeHoc
class User {
name = 'Sander';
age = 27;
}
// an observable class
class DJ {
currentSong = { artist: 'Frans Duijts', title: 'Samen op het strand' };
subscribers = [];
subscribe = (observer) => {
this.subscribers.push(observer);
}
onSongChange = () => {
this.subscribers.forEach(observer => {
observer.update(this.currentSong);
});
}
}
const dj = new DJ();
// an observer class
class Partygoer {
constructor() {
// subscribe to DJ
dj.subscribe(this);
}
update = (song) => {
console.log(song);
}
}
Observable values kunnen JS primitives, references, plain objects, class instances, arrays en maps zijn. MobX zal de decorated value controleren op veranderingen. Met React betekent dat er een re-render zal plaatsvinden wanneer deze value verandert.
class Counter extends Component {
@observable count = 0;
}
De @observer decorator wordt gebruikt om een React component te veranderen in een Reactive Component. Data dat gebruikt wordt voor het renderen van dit component zal dit component forceren te re-renderen wanneer deze data verandert.
@observer
class Counter extends Component {
@observable count = 0;
}
Alles dat de state aanpast is een Action. Bij MobX gebruiken we @action om duidelijk te maken wat een action is en waar ze staan. Dit helpt om je code goed te structureren. Actions zullen mutaties samenvoegen en alleen computed values en reactions op de hoogte brengen wanneer alle andere actions zijn afgerond. Dit zorgt er voor dat values die nog bezig zijn met updaten niet zichtbaar zijn voor de rest van de applicatie tot de action is voldaan.
@observer
class Counter extends Component {
@observable count = 0;
@action
countUp = () => {
this.count += 1;
}
}
Computed values zijn values die afkomstig zijn van de bestaande state of een andere berekende (computed) value. Computed values moeten niet worden onderschat volgens MobX. Het deel van jouw state dat mag veranderen worden hiermee zo klein mogelijk gehouden. Computed values zijn geoptimaliseerd, dus je moet ze zo vaak mogelijk gebruiken.
@observer
class Counter extends Component {
@observable count = 0;
@action
countUp = () => {
this.count += 1;
}
@computed
isCountDone = () => {
return this.count === 10;
}
}
De @inject decorator gebruik je om data van MobX stores toe te voegen aan een React component. Dit is vergelijkbaar met connect van Redux.
@inject('countStore')
@observer
class Counter extends Component {
render() {
return (
<p>count: {this.props.countStore.count}</p>
);
}
}
Met MobX hoef je dus ook geen setState meer te gebruiken. Observables vervangen de state van React en geven een performance boost!
@observer
class Counter extends Component {
@observable count = 0;
@action
countUp = () => {
this.count += 1;
}
render() {
return (
<div>
<p>count: {this.count}</p>
<button onClick={this.countUp}>
count up!
</button>
</div>
);
}
}
// playlist
const initialState = {
name = 'Funky songs';
};
const reducer1 = (state = initialState, action) => {
switch (action) {
default: return state;
}
}
// user
const initialState = {
name = 'Sander';
}
const reducer2 = (state = initialState, action) => {
switch (action) {
default: return state;
}
}
const reducers = combineReducers({ reducer1, reducer2 });
const store = createStore(reducers);
class PlaylistStore {
name = 'Funky songs';
}
class UserStore {
name = 'Sander';
}
const stores = {
userStore: new UserStore(),
playlistStore: new PlaylistStore(),
};
Actions are payloads of information that send data from your application to your store. They are the only source of information for the store.
const toggleTodo = (done) => ({
type: TOGGLE_TODO,
done,
})
Events invoke actions. Actions are the only thing that modify state and may have other side effects.
@action
onClick = (done) => {
this.done = done
}
const toggleTodo = (done) => ({
type: TOGGLE_TODO,
done,
})
const reducer = (state, action) => {
switch (action.type) {
case TOGGLE_TODO:
return {
...state,
done: action.done,
};
}
}
<button
onClick={() => this.props.todoStore.done = true}
/>
{
messages: {
byId: {
message1: {
id: 'message1',
author: 'user1',
body: '...',
},
/* ... more messages */
},
allIds: ['message1', /* ... */]
},
users: {
byId: {
user1: {
id: 'user1',
name: 'user1',
},
/* ... more users */
},
allIds: ['user1', /* ... */]
}
}
@observable messages = [
{
id: 'message1',
author: 'user1',
body: '...'.
},
/* ... more messages */
];
@observable users = [
{
id: 'user1',
author: 'user1',
},
/* ... more users */
];
import thunk from 'redux-thunk';
import todo from 'ducks/todo';
const middleware = applyMiddleware(thunk);
const reducers = combineReducers({ todo });
const store = createStore(reducers, middleware);
<Provider store={store}>
<App />
</Provider>
import todoStore from 'stores/TodoStore';
const stores = { todoStore };
<Provider {...stores}>
<App />
</Provider>
const TOGGLE_TODO = 'todo/TOGGLE';
const initialState = {
done: false,
};
export const reducer = (state = initialState, action) => {
switch(action.type) {
case TOGGLE_TODO:
return {
...state,
done: action.done,
}
default:
return state;
}
}
export const toggleTodoAction = (done) => ({
type: TOGGLE_TODO,
done
});
class TodoStore {
@observable done = false;
@action
toggleTodo = (done) => {
this.done = done;
}
}
const FETCH_ALL_TODOS_LOADING = 'todo/FETCH_ALL_LOADING';
const FETCH_ALL_TODOS_SUCCESS = 'todo/FETCH_ALL_SUCCESS';
const FETCH_ALL_TODOS_FAILED = 'todo/FETCH_ALL_FAILED';
const initialState = {
data: [],
loading: false,
error: false,
};
export const reducer = (state = initialState, action) => {
switch(action.type) {
case FETCH_ALL_TODOS_LOADING:
return {
...state,
loading: true,
error: false,
};
case FETCH_ALL_TODOS_SUCCESS:
return {
...state,
loading: false,
error: false,
data: action.payload,
};
case FETCH_ALL_TODOS_FAILED:
return {
...state,
loading: false,
error: true,
};
default:
return state;
}
}
export const fetchAllTodos = () => async (dispatch) => {
dispatch({ type: FETCH_ALL_TODOS_LOADING });
try {
const response = await fetch('someapi.com/todos');
const data = await response.json();
dispatch({ type: FETCH_ALL_TODOS_SUCCESS, payload: data });
} catch (e) {
dispatch({ type: FETCH_ALL_TODOS_FAILED });
}
};
class TodoStore {
@observable data = [];
@observable loading = false;
@observable error = false;
@action
fetchAllTodos = async () => {
this.loading = true;
try {
const response = await fetch('someapi.com/todos');
const data = await response.json();
this.data = data;
this.loading = false;
} catch (e) {
this.loading = false;
this.error = true;
}
}
}
@asyncStore
class TodoStore {
@observable data = [];
@action
fetchAllTodos = () => {
this.data = this.api.get('someapi.com/todos');
}
}
function asyncStore(store) {
return class extends store {
@observable loading = false;
@observable error = false;
api = {
get: (path) => {
this.loading = true;
try {
const response = await fetch(path);
const data = await response.json();
this.loading = false;
this.error = false;
return data;
} catch (e) {
this.loading = false;
this.error = true;
}
}
}
}
}
import { connect } from 'react-redux';
import { toggleTodoAction } from 'ducks/todo';
const TodoItem = (props) => (
<button onClick={() => props.toggleTodoAction(true)}>
Done!
</button>
);
export default connect(state => ({
todo: state.todo
}), { toggleTodoAction })(TodoItem);
import { observer, inject } from 'mobx-react';
@inject('todoStore')
@observer
class TodoItem extends React.Component {
render() {
return (
<button onClick={() => this.props.toggleTodo(true)}>
Done!
</button>
);
}
}
import { observer, inject } from 'mobx-react';
const TodoItem = inject('todoStore')(observer((props) => (
<button onClick={() => props.toggleTodo(true)}>
Done!
</button>
));
"Nieuwe" FP technieken
Geen "magie"
Bekende OOP technieken
Meer "magie"
Simpelere, kleinere syntax
Gebouwd met abstracties
Meer "magie"
Meer expliciet, meer syntax
Heeft meer "speciale" tools nodig (Thunk)
Geen "magie"
Redux dev tools
Time Travel door immutability
Geen vervanger voor Redux dev tools
Geen Time Travel
Expliciet
One-way
Geen "magie"
Impliciet
Many-ways
Meer "magie"
Expliciet
One-way
Pure
Impliciet
Many-ways
Impure
Globaal gedeelde state
Encapsulation is niet strict
Duidelijke verdeling dmv meerdere stores
Toegang tot OOP patterns
Data/logica is encapsulated (omhult)
Pure
Stricte orde van handelingen
Mutaties zijn gecentraliseerd
Impure
Geen stricte orde van handelingen
Mutaties kunnen overal gebeuren