A few of my favorite things
1/29/19
2019 edition
MobX State Tree and React Hooks
MobX State Tree
What is MobX?
simple scalable state-management
https://github.com/mobxjs/mobx
Simple compared to what?
// 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
What is MobX State Tree?
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
Snapshots
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);
}
}
Snapshots and SSR
getSnapshot(globalStore);
NodeJS
Browser
applySnapshot(globalStore, snapshot);
Dependency Injection
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
});
Tree helpers
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.
Destroy
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);
}
}));
Hooks with Mobx
What are Hooks?
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
Hooks are a better primitive to express intent and share behavior
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>
);
}
});
Mixins
e.g. class implements A, B, C, D
- Namespace collisions
- Hard to debug, understand
- Implicit dependencies
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);
Higher Order Components
e.g. (Component: T) => React.Component<T>
- Namespace issues on injected props
- Hard to write types for
- Creates Intermediate components
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>
);
}
}
Render Props
- Creates Intermediate components
- False hierarchy in the code
- Messy Syntax
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>
);
}
Hooks
- No Intermediate components
- No False hierarchy in the code
- Better syntax*
- Eliminates (this) problem in many cases
- Minifies better
Combining with Mobx
// @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
206KB
---->
157KB
🔥
😨
Text
We are here
Phi's Law
Questions / Comments?
Favorite things
By Charles King
Favorite things
2019
- 637