Using mobx-state-tree with TypeScript & React

I've been eyeballing mobx-state-tree for a while now. While I mostly work with Redux for state management, I'd read through the README last year and it seemed to be in a lot of ways the best of both worlds.

I couldn't find any simple examples of how to use it with TypeScript, so I made this one up from reading some different GitHub issues.

import * as React from 'react';

import * as ReactDOM from 'react-dom';

import { computed, observable } from 'mobx';
import { observer } from 'mobx-react';

import { types } from 'mobx-state-tree';

const Todo = types.model({
  text: types.string
})

const Store = types.model({
  todos: types.array(Todo),
}).actions((self) => {
  return {
    // The typeof operator belo is the important one: this is how you interact with types introduced
    // by mobx-state-tree
    add (todo: typeof Todo.Type) {
      self.todos.push(todo);
    },

    remove(todo: typeof Todo.Type) {
      // self.todos.remove(todo);

      // This is necessary to interact with Store.Type.todos, which is an IObservable array, not
      // a normal array, which is what filter returns.
      self.todos.replace(self.todos.filter((v, i) => {
        return v.text !== todo.text;
      }));
    },
  }
});

@observer
class App extends React.Component<{store: typeof Store.Type}> {
  todoInput: HTMLInputElement;

  addTodo(e: React.SyntheticEvent<HTMLButtonElement>) {
    e.preventDefault();
    if(this.todoInput.value !== '') {
      this.props.store.add({ text: this.todoInput.value });
      // Notice that this is type-checked properly. For example, something like this:
      // this.props.store.add({ tyxt: true });
      // Will result in a compile-time error
      this.todoInput.value = '';
    }
  }

  removeTodo(e: React.SyntheticEvent<HTMLButtonElement>, todo: typeof Todo.Type) {
    e.preventDefault();
    this.props.store.remove(todo);
  }

  render() {
    return <div>
      <form>
        <input ref={(r) => { if(r) this.todoInput = r; }} type="text" placeholder="Add entry" />
        <button type="submit" onClick={(e) => this.addTodo(e)}>Add todo</button>
      </form>

      <ul>
        {this.props.store.todos.map((item, i) => {
          return <li key={i}>
            {item.text} &nbsp; &nbsp;
            <button onClick={(e) => this.removeTodo(e, item)}>Remove</button>
          </li>
        })}
      </ul>

    </div>
  }
}

document.addEventListener('DOMContentLoaded', () => {
  const store = Store.create({ todos: [] });
  ReactDOM.render(<App store={store} />, document.getElementById('root'));
});

Specifically, the secret sauce here is in the line:

class App extends React.Component<{store: typeof Store.Type}>

The typeof operator is what you will use in the interfaces of properties as well as the declarations of e.g. model actions to retrieve the types of models and ensure that everything is properly typed.

As far as I can tell, this is necessary because mobx-state-tree relies on having a runtime representation of types. The code to map this concept back to TypeScript is quite a doozy!

Thankfully, in practice, it seems to work just fine.

To easily download and run the above code, check out the repository on GitHub.