1/29/19
2019 edition
MobX State Tree and React Hooks
simple scalable state-management
https://github.com/mobxjs/mobx
// Actions
let nextTodoId = 0;
export const addTodo = text => ({
type: "ADD_TODO",
id: nextTodoId++,
text
});
export const setVisibilityFilter = filter => ({
type: "SET_VISIBILITY_FILTER",
filter
});
export const toggleTodo = id => ({
type: "TOGGLE_TODO",
id
});
// Reducer
export const VisibilityFilters = {
SHOW_ALL: "SHOW_ALL",
SHOW_COMPLETED: "SHOW_COMPLETED",
SHOW_ACTIVE: "SHOW_ACTIVE"
};
const todos = (state = [], action) => {
switch (action.type) {
case "ADD_TODO":
return [
...state,
{
id: action.id,
text: action.text,
completed: false
}
];
case "TOGGLE_TODO":
return state.map(todo =>
todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
);
default:
return state;
}
};
export default todos;
Redux
import { observable, action } from "mobx";
class TodoStore {
@observable todos = [];
@action
addTodo(todo) {
this.todos.push(todo);
}
@action
removeTodo() {
this.todos.remove(todo);
}
}
What did we gain?
Freedom
Less Boilerplate
Observable Data Flow (with actions)
What did we lose?
Opinions
Opinionated, transactional, MobX powered state container combining the best features of the immutable and mutable world for an optimal DX
https://github.com/mobxjs/mobx
import { types } from "mobx-state-tree";
const Todo = types
.model("Todo", {
title: types.string,
done: false
})
.actions(self => ({
toggle() {
self.done = !self.done;
}
}));
const Store = types.model("Store", {
todos: types.array(Todo)
});
// create an instance from a snapshot
const store = Store.create({
todos: [
{
title: "Get coffee"
}
]
});
What did we lose?
More boilerplate
Less Freedom
Larger bundle (mobx, mobx-react, mobx-state-tree)
What did we gain?
Strong types (checked at runtime)
Single State Tree (similar to Redux)
Tools and Helpers
import { types, getSnapshot, applySnapshot } from "mobx-state-tree";
// Todo Model
const Todo = types
.model("Todo", {
title: types.string,
done: types.boolean
})
.actions(self => ({
toggle() {
self.done = !self.done;
}
}));
// Editor
class TodoEditor {
editTodo(todo) {
this.initialState = getSnapshot(todo);
this.todo = todo;
}
cancel() {
applySnapshot(this.initialState, this.todo);
}
}
getSnapshot(globalStore);
NodeJS
Browser
applySnapshot(globalStore, snapshot);
import { getEnv } from 'mobx-state-tree';
const UserStore = types
.model("UserStore", {
user: User
})
.actions(self => {
login: async user => {
// Get the Axios library from HTTP
const result = await getEnv().axios.post("/login", user);
self.user = result;
};
});
// Create axios singleton
const axiosInstance = axios.create({
baseURL: 'https://some-domain.com/api/',
timeout: 1000,
headers: {'X-Custom-Header': 'foobar'}
});
// Supply entire state tree with axios
const Store = types.model("Store", {
userStore: UserStore,
todosStore: TodosStore
}, {
axios: axiosInstance
});
import { observable, action } from "mobx";
class TodoStore {
@observable todos = [];
@action
addTodoForUser(user, todo) {
this.todos.push(todo);
}
@action
removeTodo() {
this.todos.remove(todo);
}
}
class UserStore {
@observable user;
@action
login(user) {
this.user = user;
}
}
// Some UI Control
class TodoUI {
constructor() {
this.authStore = new UserStore();
this.todoStore = new TodoStore();
}
createTodoForUser() {
this.todoStore.addTodo(user, new Todo());
}
}
import { getRoot } from "mobx-state-tree";
const TodosStore = types
.model("TodosStore", {
todos: types.array(Todo)
})
.actions(self => ({
addTodoForUser(todo) {
const user = getRoot(self).userStore.user;
todos.push(todo);
}
}));
const UserStore = types
.model("UserStore", {
user: User
})
.actions(self => ({
login(user) {
self.user = user;
}
}));
const Store = types.model("Store", {
userStore: UserStore,
todosStore: TodosStore
});
// Some UI Control
class TodoUI {
constructor() {
this.store = new Store();
}
createTodoForUser() {
this.store.todos.addTodoForUser(new Todo());
}
}
vs.
class TodosStore {
@observable
todos = [];
@action
removeTodo(todo) {
const idx = this.todos.indexOf(todo);
this.todos.splice(idx, 1);
}
}
class TodosStore {
@observable
todos = [];
@action
removeTodo(todo) {
this.todos = this.todos.filter(t => t.id === todo.id);
}
}
import { destroy } from "mobx-state-tree";
const TodosStore = types
.model("TodosStore", {
todos: types.array(Todo)
})
.actions(self => ({
removeTodo(todo) {
destroy(todo);
}
}));
https://reactjs.org/docs/hooks-intro.html
Hooks are an upcoming feature that lets you use state and other React features without writing a class
import React from "react";
import MousePosition from "react-mouse-position";
export default React.createClass({
mixins: [MousePosition],
render() {
return (
<div>
<span>X: {this.state.x}</span>
<span>Y: {this.state.y}</span>
</div>
);
}
});
e.g. class implements A, B, C, D
import React from "react";
import MousePosition from "react-mouse-position";
class TraceMouse extends React.Component {
render() {
const { x, y } = this.props;
return (
<div>
<span>X: {x}</span>
<span>Y: {y}</span>
</div>
);
}
}
// Where do we get MousePosition?
export default MousePosition(TraceMouse);
e.g. (Component: T) => React.Component<T>
import React from "react";
import MousePosition from "react-mouse-position";
export default class TraceMouse extends React.Component {
render() {
const { x, y } = this.props;
return (
<div>
<MousePosition>
{(x, y) => (
<>
<span>X: {x}</span>
<span>Y: {y}</span>
</>
)}
</MousePosition>
</div>
);
}
}
import React from "react";
import { useMousePosition } from "react-mouse-position";
export function TraceMouse() {
const { x, y } = useMousePosition();
return (
<div>
<span>X: {x}</span>
<span>Y: {y}</span>
</div>
);
}
// @flow
import React, { Fragment, useState, useContext } from "react";
import { observer } from "mobx-react-lite";
import { StoreContext } from "lib/stores";
export const CommitTable = (props: Props) => {
const { authStore } = useContext(StoreContext);
const [confirming, setConfirming] = useState(null);
const [error, setError] = useState(null);
const onPromoteRequested = (jiraCommit: JiraCommit) => {
setConfirming(jiraCommit);
setError(null);
};
const onPromoteConfirmed = async (jiraCommit: JiraCommit) => {
try {
await props.repoStore.promote(jiraCommit, authStore.token);
} catch (e) {
setError(e);
} finally {
setConfirming(null);
}
};
// omitted for brevity
return <div />;
};
export default observer(CommitTable);
// @flow
import React, { Fragment, Component } from "react";
import { inject, observer } from "mobx-react";
export class CommitTable extends Component<Props, State> {
state = {
confirming: null,
error: null
};
onPromoteRequested = (jiraCommit: JiraCommit) => {
this.setState({ confirming: jiraCommit, error: null });
};
onPromoteConfirmed = async (jiraCommit: JiraCommit) => {
try {
await this.props.repoStore.promote(jiraCommit, this.props.authStore.token);
} catch (e) {
this.setState({ error: e });
} finally {
this.setState({ confirming: null });
}
};
onPromoteCancelled = () => {
this.setState({ confirming: null });
};
onDismissErrorMessage = () => {
this.setState({ error: null });
};
render() {
// omitted for brevity
return <div />;
}
}
export default inject("authStore", "repoStore")(observer(CommitTable));
Current
Hooks
// @flow
import React, { Fragment, useContext } from "react";
import { observer, useObservable } from "mobx-react-lite";
import { StoreContext } from "lib/stores";
export const CommitTable = (props: Props) => {
const { authStore } = useContext(StoreContext);
const confirming = useObservable(null)
const error = useObservable(null);
const onPromoteRequested = (jiraCommit: JiraCommit) => {
confirming = jiraCommit;
error = null;
};
const onPromoteConfirmed = async (jiraCommit: JiraCommit) => {
try {
await props.repoStore.promote(jiraCommit, authStore.token);
} catch (e) {
setError(e);
error = e;
} finally {
confirming = null;
}
};
// omitted for brevity
return <div />;
};
export default observer(CommitTable);
Hooks + mobx-react-lite
Bundle size results
🔥
😨
Text
We are here
Phi's Law