Skip to main content

MobX vs Zustand in 2026: OOP vs Functional State Management

·PkgPulse Team

TL;DR

Zustand for most React projects; MobX for complex domain logic or OOP teams. Zustand (~10M weekly downloads) is simpler, more idiomatic for functional React, and has a gentler learning curve. MobX (~3.5M downloads) uses observable reactivity that auto-tracks dependencies — powerful for complex state but adds conceptual overhead. Teams migrating from Angular or with OOP backgrounds often prefer MobX.

Key Takeaways

  • Zustand: ~10M weekly downloads — MobX: ~3.5M (npm, March 2026)
  • MobX uses observables — mutations are tracked automatically via Proxy
  • Zustand is explicit — you call set() to update state
  • MobX-React-Lite is tiny — ~1.5KB for the React bindings
  • MobX scales to complex domains — class-based stores map to domain models

Core Philosophy

// Zustand — functional, explicit updates
import { create } from 'zustand';

const useCounterStore = create((set, get) => ({
  count: 0,
  increment: () => set(state => ({ count: state.count + 1 })),
  decrement: () => set(state => ({ count: state.count - 1 })),
  reset: () => set({ count: 0 }),
  // Must explicitly call set() — no magic
}));
// MobX — observable, automatic tracking
import { makeAutoObservable } from 'mobx';
import { observer } from 'mobx-react-lite';

class CounterStore {
  count = 0;

  constructor() {
    makeAutoObservable(this); // Makes all properties observable
  }

  increment() { this.count++; }   // Direct mutation — MobX intercepts via Proxy
  decrement() { this.count--; }
  reset() { this.count = 0; }

  get doubled() { return this.count * 2; } // Computed value — auto-cached
}

const counter = new CounterStore();

// Components must be wrapped in observer() to react to changes
const Counter = observer(() => (
  <div onClick={() => counter.increment()}>
    Count: {counter.count} (doubled: {counter.doubled})
  </div>
));

Complex Domain Logic

// MobX — class stores map naturally to domain models
class TodoStore {
  todos = [];
  filter = 'all';

  constructor() {
    makeAutoObservable(this);
  }

  addTodo(title) {
    this.todos.push({ id: Date.now(), title, completed: false });
  }

  toggleTodo(id) {
    const todo = this.todos.find(t => t.id === id);
    if (todo) todo.completed = !todo.completed;
  }

  // Computed values — cached and only recompute when deps change
  get filteredTodos() {
    switch (this.filter) {
      case 'active': return this.todos.filter(t => !t.completed);
      case 'completed': return this.todos.filter(t => t.completed);
      default: return this.todos;
    }
  }

  get stats() {
    return {
      total: this.todos.length,
      active: this.todos.filter(t => !t.completed).length,
      completedPercent: Math.round(
        this.todos.filter(t => t.completed).length / this.todos.length * 100
      ),
    };
  }
}
// Zustand equivalent — more verbose for complex computed state
const useTodoStore = create((set, get) => ({
  todos: [],
  filter: 'all',
  addTodo: (title) => set(state => ({
    todos: [...state.todos, { id: Date.now(), title, completed: false }],
  })),
  toggleTodo: (id) => set(state => ({
    todos: state.todos.map(t => t.id === id ? { ...t, completed: !t.completed } : t),
  })),
  // No built-in computed values — use selectors or useMemo
  getFilteredTodos: () => {
    const { todos, filter } = get();
    if (filter === 'active') return todos.filter(t => !t.completed);
    if (filter === 'completed') return todos.filter(t => t.completed);
    return todos;
  },
}));

Reactions and Side Effects

// MobX — reactions auto-run when dependencies change
import { reaction, autorun, when } from 'mobx';

// autorun — runs immediately and whenever deps change
const disposer = autorun(() => {
  console.log('Count changed to:', counter.count);
});

// reaction — runs when selector changes
reaction(
  () => userStore.isLoggedIn,
  (isLoggedIn) => {
    if (isLoggedIn) router.push('/dashboard');
    else router.push('/login');
  }
);

// when — runs once when condition becomes true
when(
  () => cartStore.total > 100,
  () => console.log('You qualify for free shipping!')
);
// Zustand — subscribe to state changes
const unsubscribe = useCartStore.subscribe(
  state => state.total,
  (total) => {
    if (total > 100) console.log('Free shipping!');
  }
);

// Or use useEffect in components
useEffect(() => {
  if (isLoggedIn) router.push('/dashboard');
}, [isLoggedIn]);

React Integration

// MobX — requires observer() wrapper
// Forget this and components won't react to store changes
const UserProfile = observer(({ userId }) => {
  const user = userStore.getUserById(userId);
  return <div>{user.name}</div>;
});

// With hooks (mobx-react-lite)
const UserList = observer(() => {
  const { users, loading } = userStore;
  // Component auto-subscribes to exactly the observables accessed
  return loading ? <Spinner /> : users.map(u => <User key={u.id} user={u} />);
});
// Zustand — no wrapper needed, uses standard hooks
const UserProfile = ({ userId }) => {
  const user = useUserStore(state => state.users[userId]);
  return <div>{user?.name}</div>;
};

// Selector determines what the component subscribes to
const UserList = () => {
  const { users, loading } = useUserStore(state => ({
    users: state.users,
    loading: state.loading,
  }));
  return loading ? <Spinner /> : users.map(u => <User key={u.id} user={u} />);
};

When to Choose

Choose Zustand when:

  • New React project with functional components and hooks
  • Team is comfortable with functional programming patterns
  • You want simple, explicit state updates
  • Less mental overhead is a priority
  • Most typical SaaS app state (auth, UI, cart)

Choose MobX when:

  • Team comes from Angular, Java, or C# background (OOP comfort)
  • Complex domain logic maps naturally to class-based stores
  • Fine-grained reactivity matters (MobX is surgical in what it re-renders)
  • You prefer automatic dependency tracking over manual selectors
  • Large state trees with many computed values

Compare MobX and Zustand package health on PkgPulse.

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.