NexusCS

MobX

JavaScript libraries
MobX is a simple, scalable, signal-based state management library. It makes state management simple by transparently applying functional reactive programming.
mobx
state-management
react
observable
reactive

Getting started

Installation

npm install mobx mobx-react-lite

For class component support:

npm install mobx mobx-react

makeAutoObservable (Recommended)

import { makeAutoObservable } from "mobx";

class Timer {
  secondsPassed = 0;

  constructor() {
    makeAutoObservable(this);
  }

  increaseTimer() {
    this.secondsPassed += 1;
  }
}

Automatically infers observable, computed, and action annotations.

makeObservable (Explicit)

import { makeObservable, observable, action } from "mobx";

class Todo {
  id = Math.random();
  title = "";
  finished = false;

  constructor(title) {
    makeObservable(this, {
      title: observable,
      finished: observable,
      toggle: action,
    });
    this.title = title;
  }

  toggle() {
    this.finished = !this.finished;
  }
}

Requires explicit annotations for each field.

TypeScript Configuration

{
  "compilerOptions": {
    "useDefineForClassFields": true
  }
}

Required for MobX 6+ with TypeScript.

Observable State

Annotations Reference

Annotation Description
observable Track state changes
observable.ref Only track reassignment
observable.shallow Track collection membership only
observable.struct Structural comparison
computed Derived value (getter)
computed.struct Structural equality
action Modifies state
action.bound Auto-bind this
flow Async generator

Factory Function Pattern

import { makeAutoObservable } from "mobx";

function createDoubler(value) {
  return makeAutoObservable({
    value,
    get double() {
      return this.value * 2;
    },
    increment() {
      this.value++;
    },
  });
}

const doubler = createDoubler(5);
doubler.increment(); // value = 6, double = 12

Plain objects can be made observable.

Observable Collections

import { observable } from "mobx";

const todos = observable.array([{ title: "Learn MobX", done: false }]);

const counts = observable.map({
  apples: 5,
  bananas: 3,
});

const tags = observable.set(["mobx", "react"]);

Collections have additional methods:

todos.clear();
todos.replace([...newItems]);
todos.remove(item);

observable() Factory

import { observable } from "mobx";

const state = observable({
  count: 0,
  get double() {
    return this.count * 2;
  },
  increment() {
    this.count++;
  },
});

Creates observable proxy for plain objects.

Actions

Basic Actions

import { makeAutoObservable } from "mobx";

class Store {
  count = 0;

  constructor() {
    makeAutoObservable(this);
  }

  increment() {
    this.count++;
  }

  decrement() {
    this.count--;
  }
}

Actions modify observable state and batch reactions.

action.bound

import { makeObservable, observable, action } from "mobx";

class Store {
  count = 0;

  constructor() {
    makeObservable(this, {
      count: observable,
      increment: action.bound,
    });
  }

  increment() {
    this.count++;
  }
}

const store = new Store();
const handler = store.increment;
handler(); // this is bound correctly

Auto-binds this to the instance.

runInAction (Inline Actions)

import { observable, runInAction } from "mobx";

const state = observable({ count: 0 });

runInAction(() => {
  state.count++;
  state.count++;
});

For immediate one-off state modifications.

Async with runInAction

class Store {
  state = "pending";
  projects = [];

  async fetchProjects() {
    this.state = "pending";
    try {
      const projects = await fetch("/api/projects");
      runInAction(() => {
        this.projects = projects;
        this.state = "done";
      });
    } catch (e) {
      runInAction(() => {
        this.state = "error";
      });
    }
  }
}

Wrap state updates after await.

Async with flow (Recommended)

import { flow, makeAutoObservable } from "mobx";

class Store {
  projects = [];
  state = "pending";

  constructor() {
    makeAutoObservable(this, {
      fetchProjects: flow,
    });
  }

  *fetchProjects() {
    this.state = "pending";
    try {
      const projects = yield fetch("/api/projects");
      this.state = "done";
      this.projects = projects;
    } catch (error) {
      this.state = "error";
    }
  }
}

Generator function, use yield instead of await.

Computed Values

Basic Computed

import { makeAutoObservable } from "mobx";

class TodoList {
  todos = [];

  constructor() {
    makeAutoObservable(this);
  }

  get unfinishedCount() {
    return this.todos.filter((t) => !t.finished).length;
  }
}

Computed values are cached and lazy.

Computed with Setter

class Person {
  firstName = "";
  lastName = "";

  constructor() {
    makeAutoObservable(this);
  }

  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  }

  set fullName(value) {
    const [firstName, lastName] = value.split(" ");
    this.firstName = firstName;
    this.lastName = lastName;
  }
}

Computed can have setters for two-way binding.

Computed Options

import { computed, makeObservable } from "mobx";

class Store {
  constructor() {
    makeObservable(this, {
      total: computed.struct,
    });
  }

  get total() {
    return { sum: this.items.reduce((a, b) => a + b, 0) };
  }
}

computed.struct uses structural equality.

Reactions

autorun

import { autorun, makeAutoObservable } from "mobx";

class Store {
  count = 0;
  constructor() {
    makeAutoObservable(this);
  }
}

const store = new Store();

const dispose = autorun(() => {
  console.log(`Count: ${store.count}`);
});

// Later: cleanup
dispose();

Runs immediately, re-runs when observables change.

reaction

import { reaction } from "mobx";

const dispose = reaction(
  () => store.count,
  (count, previousCount) => {
    console.log(`Changed from ${previousCount} to ${count}`);
  },
);

Fine-grained control over what triggers the effect.

when

import { when } from "mobx";

// With callback
const dispose = when(
  () => store.count > 10,
  () => console.log("Count exceeded 10!"),
);

// As Promise
await when(() => store.count > 10);
console.log("Count exceeded 10!");

Runs once when predicate becomes true.

Reaction Options

import { autorun } from "mobx";

autorun(
  () => {
    console.log(store.count);
  },
  {
    delay: 300,
    name: "CountLogger",
    onError: (error) => console.error(error),
  },
);

Common options: delay, name, onError.

React Integration

observer HOC

import { observer } from "mobx-react-lite";

const TimerView = observer(({ timer }) => (
  <span>Seconds passed: {timer.secondsPassed}</span>
));

Makes component reactive, auto-applies React.memo.

useLocalObservable

import { observer } from "mobx-react-lite";
import { useLocalObservable } from "mobx-react-lite";

const Counter = observer(() => {
  const counter = useLocalObservable(() => ({
    count: 0,
    increment() {
      this.count++;
    },
  }));

  return <button onClick={counter.increment}>Count: {counter.count}</button>;
});

Local observable state in functional components.

Observer Component (Inline)

import { Observer } from "mobx-react-lite";

function App() {
  return (
    <div onClick={() => store.count++}>
      <Observer>{() => <span>{store.count}</span>}</Observer>
    </div>
  );
}

For observing in callbacks or limited scope.

Context Pattern

import React from "react";
import { observer } from "mobx-react-lite";

const StoreContext = React.createContext(null);

export const StoreProvider = ({ children }) => {
  const store = React.useMemo(() => new RootStore(), []);
  return (
    <StoreContext.Provider value={store}>{children}</StoreContext.Provider>
  );
};

export const useStore = () => React.useContext(StoreContext);

const Component = observer(() => {
  const store = useStore();
  return <div>{store.count}</div>;
});

Recommended pattern for distributing stores.

SSR Configuration

import { enableStaticRendering } from "mobx-react-lite";

enableStaticRendering(typeof window === "undefined");

Call once in server-side entry point.

Patterns

RootStore Pattern

class RootStore {
  constructor() {
    this.userStore = new UserStore(this);
    this.todoStore = new TodoStore(this);
  }
}

class TodoStore {
  constructor(rootStore) {
    this.rootStore = rootStore;
    makeAutoObservable(this);
  }

  get currentUser() {
    return this.rootStore.userStore.currentUser;
  }
}

Central store with cross-store references.

Configuration

import { configure } from "mobx";

configure({
  enforceActions: "always",
  computedRequiresReaction: true,
  reactionRequiresObservable: true,
  observableRequiresReaction: true,
  disableErrorBoundaries: false,
});

Strict mode for development.

Best Practices

  • Use makeAutoObservable for new code
  • Wrap async updates with runInAction or flow
  • Dispose reactions to prevent memory leaks
  • Use observer on all components reading observables
  • Enable strict mode during development
  • Prefer flow over async/await for async actions

Domain Store Example

class TodoStore {
  todos = [];

  constructor() {
    makeAutoObservable(this, {
      fetchTodos: flow,
    });
  }

  get completedCount() {
    return this.todos.filter((t) => t.done).length;
  }

  addTodo(title) {
    this.todos.push({ title, done: false });
  }

  *fetchTodos() {
    this.todos = yield fetchTodosFromAPI();
  }
}

Observable state + computed + actions.

Debugging

import { trace, spy } from "mobx";

// In computed or reaction
computed(() => {
  trace();
  return store.total;
});

// Global spy
spy((event) => {
  console.log(event);
});

Use trace() to debug why computed/reaction runs.

Also see