โ† Back to DevBytes

State Management in MobX: Patterns and Libraries

State Management in MobX: Patterns and Libraries

MobX is a battle-tested state management library that brings the power of transparent functional reactive programming to JavaScript applications. Unlike Redux, which enforces a single source of truth through immutable state and explicit actions, MobX embraces mutable state and automatic dependency tracking. This tutorial walks through the core concepts, practical patterns, and complementary libraries that make MobX a compelling choice for React and beyond.

What Is MobX?

MobX is built on three foundational ideas: observable state, derived values, and reactions. State is the source of truth; derived values (called computeds) are values that depend on observable state; and reactions (like React components or autoruns) automatically respond when state changes. The library tracks which observables each reaction reads, and only re-runs the reaction when those specific observables change.

This fine-grained tracking means you don't need to manually wire selectors or memoize values. MobX figures out the dependency graph for you at runtime.

Why MobX Matters

Core Building Blocks

Observables

The observable function (or makeObservable/makeAutoObservable for classes) marks a value as reactive. When you read it inside a tracked context and later mutate it, every observer is notified.

import { makeAutoObservable } from "mobx";

class CounterStore {
  count = 0;

  constructor() {
    makeAutoObservable(this);
  }

  increment() {
    this.count++;
  }

  reset() {
    this.count = 0;
  }
}

const counter = new CounterStore();
counter.increment();
console.log(counter.count); // 1

makeAutoObservable automatically infers which properties are observable, which are computed, and which are actions based on their type. For more control, use makeObservable with an explicit map.

Computed Values

Computed values are derived from observable state and are cached until their dependencies change. They are declared with getters in a class.

import { makeAutoObservable } from "mobx";

class TodoStore {
  todos = [];

  constructor() {
    makeAutoObservable(this);
  }

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

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

  get completedCount() {
    return this.todos.length - this.remainingCount;
  }
}

The remainingCount getter is only recomputed when todos or any done flag changes. Calling it repeatedly returns the cached value.

Actions

Actions are functions that mutate state. MobX batches state changes inside actions so that observers are notified once at the end, not on every mutation. With makeAutoObservable, all methods are treated as actions by default.

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

class CartStore {
  items = [];

  constructor() {
    makeObservable(this, {
      items: observable,
      addItem: action,
      removeItem: action,
      clear: action,
      total: computed,
    });
  }

  addItem(product) {
    this.items.push(product);
  }

  removeItem(id) {
    this.items = this.items.filter((i) => i.id !== id);
  }

  clear() {
    this.items = [];
  }

  get total() {
    return this.items.reduce((sum, i) => sum + i.price, 0);
  }
}

For asynchronous mutations, wrap the mutating portion in runInAction or use flow for generator-based async actions.

Integrating with React

The observer HOC

The mobx-react-lite package provides the observer function, which turns a React component into a reactive one. The component re-renders only when the observables it reads change.

import { observer } from "mobx-react-lite";
import { counter } from "./stores/counter";

const CounterView = observer(() => {
  return (
    <div>
      <p>Count: {counter.count}</p>
      <button onClick={() => counter.increment()}>+</button>
      <button onClick={() => counter.reset()}>Reset</button>
    </div>
  );
});

export default CounterView;

Passing Stores via Context

A common pattern is to instantiate stores at the app root and provide them through React Context. This keeps components decoupled from singleton imports.

import { createContext, useContext } from "react";
import { CounterStore } from "./CounterStore";
import { TodoStore } from "./TodoStore";

export const stores = {
  counter: new CounterStore(),
  todos: new TodoStore(),
};

const StoreContext = createContext(stores);

export const useStores = () => useContext(StoreContext);

export const StoreProvider = ({ children }) => (
  <StoreContext.Provider value={stores}>{children}</StoreContext.Provider>
);

Then consume them in any component:

import { observer } from "mobx-react-lite";
import { useStores } from "./StoreProvider";

const TodoList = observer(() => {
  const { todos } = useStores();
  return (
    <ul>
      {todos.todos.map((t, idx) => (
        <li key={idx}>{t.text}</li>
      ))}
    </ul>
  );
});

Common Patterns

Root Store Composition

For larger apps, group related stores under a single root store. This makes it easy to inject cross-store references and to serialize the entire application state.

import { CounterStore } from "./CounterStore";
import { TodoStore } from "./TodoStore";
import { UserStore } from "./UserStore";

export class RootStore {
  counter;
  todos;
  user;

  constructor() {
    this.counter = new CounterStore();
    this.todos = new TodoStore();
    this.user = new UserStore(this); // pass root for cross-store access
  }
}

Async Actions with Flow

The flow helper uses generators to handle async operations cleanly, automatically wrapping yielded promises in actions.

import { makeAutoObservable, flow } from "mobx";

class UserStore {
  user = null;
  loading = false;
  error = null;

  constructor() {
    makeAutoObservable(this, { fetchUser: flow }, { autoBind: true });
  }

  fetchUser = flow(function* (id) {
    this.loading = true;
    this.error = null;
    try {
      const response = yield fetch(`/api/users/${id}`);
      this.user = yield response.json();
    } catch (err) {
      this.error = err.message;
    } finally {
      this.loading = false;
    }
  });
}

Local Observable State in Components

For component-local state that needs to be observable, use the useLocalObservable hook.

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

const SearchBox = observer(() => {
  const state = useLocalObservable(() => ({
    query: "",
    setQuery(value) {
      this.query = value;
    },
    get isLong() {
      return this.query.length > 20;
    },
  }));

  return (
    <div>
      <input
        value={state.query}
        onChange={(e) => state.setQuery(e.target.value)}
      />
      {state.isLong && <span>Query is quite long</span>}
    </div>
  );
});

Complementary Libraries

mobx-state-tree (MST)

MST is a structured state container built on top of MobX. It introduces typed models, snapshots, patches, and time-travel debugging. MST is ideal when you need serialization, validation, or a more opinionated architecture.

import { types, flow } from "mobx-state-tree";

const Todo = types.model("Todo", {
  id: types.identifier,
  text: types.string,
  done: types.boolean,
});

const TodoStore = types
  .model("TodoStore", {
    todos: types.array(Todo),
  })
  .actions((self) => ({
    add(text) {
      self.todos.push({ id: String(Date.now()), text, done: false });
    },
    toggle(id) {
      const todo = self.todos.find((t) => t.id === id);
      if (todo) todo.done = !todo.done;
    },
  }))
  .views((self) => ({
    get remaining() {
      return self.todos.filter((t) => !t.done).length;
    },
  }));

const store = TodoStore.create({ todos: [] });
store.add("Learn MST");
console.log(store.remaining); // 1

mobx-react

For class components or older React versions, the mobx-react package provides observer, Provider, and inject. Modern apps generally prefer mobx-react-lite, but mobx-react still works for both function and class components.

mobx-utils

mobx-utils adds handy utilities such as fromPromise, observableResource, now, and keepAlive. For example, fromPromise wraps a promise in an observable with state, value, and error fields.

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

const UserProfile = observer(({ userId }) => {
  const request = fromPromise(fetch(`/api/users/${userId}`).then((r) => r.json()));

  if (request.state === "pending") return <p>Loading...</p>;
  if (request.state === "rejected") return <p>Error: {request.reason.message}</p>;

  return <div>{request.value.name}</div>;
});

mst-react or mobx-react-form

For form-heavy applications, mobx-react-form provides field-level observables, validation, and bindings. It integrates well with both MobX and MST stores.

Best Practices

When to Choose MobX

MobX shines in applications with rich, interdependent domain models โ€” dashboards, editors, real-time tools, and complex forms. Its automatic dependency tracking eliminates the selector boilerplate of Redux while keeping re-renders surgical. For simpler apps, React's built-in state may suffice, but as state grows in complexity, MobX's class-based stores and fine-grained reactivity scale gracefully.

Conclusion

MobX offers a pragmatic, low-ceremony approach to state management that scales from a single component's local state to a multi-store domain architecture. By combining observable state, computed derivations, and batched actions with libraries like mobx-react-lite, mobx-state-tree, and mobx-utils, you can build reactive applications that stay maintainable as they grow. The key is to model your state as plain classes, keep mutations inside actions, and let MobX handle the dependency graph โ€” so you can focus on your domain logic rather than wiring up re-renders by hand.

๐Ÿ›  Tools from DevBytes

Inventory Tracker Pro โ€” Excel inventory system, low-stock alerts ยท $19
AI Dev Kit for Mac โ€” local AI dev environment templates ยท $9.99
KeyMapper for Mac โ€” custom keyboard shortcut toolkit ยท $7.99

โ† Back to all articles