MobX の使い方 — シンプルでスケーラブルな状態管理ライブラリ
一言でいうと
MobXは、シグナルベースのリアクティブプログラミングにより、アプリケーションの状態管理をシンプルかつスケーラブルに実現するライブラリです。状態の変更を自動的に追跡し、依存するUIやデータを最小限のコストで更新します。
どんな時に使う?
- Reactアプリケーションのグローバル状態管理 — Reduxのようなボイラープレートなしに、直感的なコードで複雑な状態を管理したい場合
- ドメインモデルの構築 — ECサイトのカート、フォームの入力状態、ダッシュボードのフィルタ条件など、ビジネスロジックを持つオブジェクトをリアクティブに扱いたい場合
- UIフレームワーク非依存の状態管理 — React以外(Vue、バニラJSなど)でも使える、テスタブルな状態管理層を構築したい場合
インストール
# npm
npm install mobx
# yarn
yarn add mobx
# pnpm
pnpm add mobx
Reactと組み合わせる場合は、バインディングライブラリも必要です:
npm install mobx-react-lite
# クラスコンポーネントも使う場合は mobx-react
注意: この記事はMobX v6.15.0時点の情報に基づいています。
MobX の基本的な使い方
MobXの核心は Observable(状態)→ Action(変更)→ Reaction(反応) という3つの概念です。
import { makeAutoObservable } from "mobx";
class TodoStore {
todos: { id: number; title: string; done: boolean }[] = [];
constructor() {
// すべてのプロパティをobservable、メソッドをactionとして自動設定
makeAutoObservable(this);
}
addTodo(title: string) {
this.todos.push({ id: Date.now(), title, done: false });
}
toggleTodo(id: number) {
const todo = this.todos.find((t) => t.id === id);
if (todo) {
todo.done = !todo.done;
}
}
// computedとして自動認識される(getterはcomputedになる)
get unfinishedCount() {
return this.todos.filter((t) => !t.done).length;
}
}
const todoStore = new TodoStore();
export default todoStore;
Reactコンポーネントでの利用:
import React from "react";
import { observer } from "mobx-react-lite";
import todoStore from "./todoStore";
const TodoList: React.FC = observer(() => {
return (
<div>
<h2>残りタスク: {todoStore.unfinishedCount}</h2>
<ul>
{todoStore.todos.map((todo) => (
<li
key={todo.id}
onClick={() => todoStore.toggleTodo(todo.id)}
style={{ textDecoration: todo.done ? "line-through" : "none" }}
>
{todo.title}
</li>
))}
</ul>
<button onClick={() => todoStore.addTodo("新しいタスク")}>追加</button>
</div>
);
});
export default TodoList;
よく使うAPI
1. makeAutoObservable — 自動でリアクティブ化
最も頻繁に使うAPIです。クラスのプロパティを自動的にobservable、getterをcomputed、メソッドをactionとして設定します。
import { makeAutoObservable } from "mobx";
class Counter {
count = 0;
constructor() {
// 第2引数で特定プロパティの挙動を上書きできる
makeAutoObservable(this, {
// resetをactionではなくflowとして扱う、等のカスタマイズが可能
});
}
increment() {
this.count++;
}
get doubled() {
return this.count * 2;
}
}
2. makeObservable — 明示的なアノテーション指定
大規模プロジェクトで各プロパティの役割を明示したい場合に使います。
import { makeObservable, observable, computed, action } from "mobx";
class UserStore {
name = "";
age = 0;
constructor() {
makeObservable(this, {
name: observable,
age: observable,
isAdult: computed,
setName: action,
});
}
get isAdult() {
return this.age >= 18;
}
setName(name: string) {
this.name = name;
}
}
3. autorun — 状態変更への自動反応
observableな値が変更されるたびに、自動的にコールバックが再実行されます。ログ出力や副作用の実行に便利です。
import { makeAutoObservable, autorun } from "mobx";
class PriceStore {
price = 100;
taxRate = 0.1;
constructor() {
makeAutoObservable(this);
}
get priceWithTax() {
return this.price * (1 + this.taxRate);
}
}
const store = new PriceStore();
// priceまたはtaxRateが変わるたびに自動実行される
const dispose = autorun(() => {
console.log(`税込価格: ${store.priceWithTax}`);
});
store.price = 200; // => "税込価格: 220" が出力される
// 不要になったら破棄(メモリリーク防止)
dispose();
4. reaction — 特定の値の変化だけを監視
autorunと異なり、「何を監視するか」と「変化時に何をするか」を分離できます。
import { makeAutoObservable, reaction } from "mobx";
class AuthStore {
token: string | null = null;
user: { name: string } | null = null;
constructor() {
makeAutoObservable(this);
}
login(token: string, name: string) {
this.token = token;
this.user = { name };
}
logout() {
this.token = null;
this.user = null;
}
}
const authStore = new AuthStore();
// tokenの変化だけを監視し、変化時にのみ副作用を実行
const dispose = reaction(
() => authStore.token, // 監視対象(data関数)
(token, previousToken) => {
// 変化時のみ実行(初回は実行されない)
if (token) {
console.log("ログインしました");
} else {
console.log("ログアウトしました");
}
}
);
authStore.login("abc123", "田中"); // => "ログインしました"
authStore.logout(); // => "ログアウトしました"
dispose();
5. runInAction — 非同期処理後の状態更新
非同期処理のawait後にobservableを更新する場合、runInActionで囲む必要があります。
import { makeAutoObservable, runInAction } from "mobx";
class ArticleStore {
articles: { id: number; title: string }[] = [];
loading = false;
error: string | null = null;
constructor() {
makeAutoObservable(this);
}
async fetchArticles() {
this.loading = true;
this.error = null;
try {
const response = await fetch("/api/articles");
const data = await response.json();
// awaitの後のobservable更新はrunInActionで囲む
runInAction(() => {
this.articles = data;
this.loading = false;
});
} catch (e) {
runInAction(() => {
this.error = e instanceof Error ? e.message : "Unknown error";
this.loading = false;
});
}
}
}
類似パッケージとの比較
| 特徴 | MobX | Redux (+ Toolkit) | Zustand | Jotai |
|---|---|---|---|---|
| 設計思想 | リアクティブ(自動追跡) | Flux(単方向データフロー) | Flux簡易版 | アトミック |
| ボイラープレート | 少ない | 中程度(Toolkitで軽減) | 少ない | 少ない |
| 学習コスト | 中(リアクティブの概念理解が必要) | 中〜高 | 低 | 低 |
| TypeScript対応 | ◎ | ◎ | ◎ | ◎ |
| DevTools | MobX Developer Tools | Redux DevTools(充実) | Redux DevTools互換 | 独自 |
| 再レンダリング最適化 | 自動(observerで囲むだけ) | 手動(selector/memoize) | 手動(selector) | 自動(atom単位) |
| ミュータブル更新 | ✅ 直接代入OK | ❌(Immer経由なら可) | ✅ | ❌ |
| バンドルサイズ | ~16KB | ~1KB | ~3KB |
選定の目安:
- オブジェクト指向でドメインモデルを組みたい → MobX
- チーム開発で厳格なフローが欲しい → Redux Toolkit
- 軽量でシンプルに済ませたい → Zustand / Jotai
注意点・Tips
observerで囲み忘れに注意
MobXの最も多いハマりポイントです。Reactコンポーネントをobserverで囲まないと、observableの変更が反映されません。
// ❌ observerで囲んでいない → 再レンダリングされない
const BadComponent: React.FC = () => {
return <div>{store.count}</div>;
};
// ✅ observerで囲む → 自動的に再レンダリングされる
const GoodComponent: React.FC = observer(() => {
return <div>{store.count}</div>;
});
observableの分割代入は追跡が切れる
// ❌ 分割代入するとリアクティビティが失われる
const BadComponent: React.FC = observer(() => {
const { count } = store; // この時点でプリミティブ値がコピーされる
return <div>{count}</div>; // 更新されない可能性がある
});
// ✅ ドット記法でアクセスする
const GoodComponent: React.FC = observer(() => {
return <div>{store.count}</div>;
});
configureでstrict modeを有効にする
action外からのobservable変更を禁止することで、意図しない状態変更を防げます。
import { configure } from "mobx";
configure({
enforceActions: "always", // action外からの変更を禁止
computedRequiresReaction: true, // reaction外でのcomputed使用を警告
reactionRequiresObservable: true, // observableを使わないreactionを警告
});
flowで非同期処理をスマートに書く
runInActionを何度も書くのが面倒な場合、ジェネレータベースのflowが便利です。
import { makeAutoObservable, flow } from "mobx";
class UserStore {
user: { name: string } | null = null;
loading = false;
constructor() {
makeAutoObservable(this, {
fetchUser: flow, // flowとして明示
});
}
// ジェネレータ関数として定義(awaitの代わりにyield)
*fetchUser(id: number): Generator<Promise<Response>, void, Response> {
this.loading = true;
try {
const response = yield fetch(`/api/users/${id}`);
const data: { name: string } = yield (response as any).json();
this.user = data;
} finally {
this.loading = false;
}
}
}
React Context経由でStoreを渡すパターン
グローバル変数ではなく、React Contextを使ってStoreを注入するのがテスタビリティの面で推奨されます。
import React, { createContext, useContext } from "react";
const StoreContext = createContext<TodoStore | null>(null);
export const StoreProvider: React.FC<{ store: TodoStore; children: React.ReactNode }> = ({
store,
children,
}) => <StoreContext.Provider value={store}>{children}</StoreContext.Provider>;
export const useStore = (): TodoStore => {
const store = useContext(StoreContext);
if (!store) throw new Error("useStore must be used within StoreProvider");
return store;
};
まとめ
MobXは、observableな状態を定義するだけで変更の追跡と伝播を自動的に行ってくれる、直感的な状態管理ライブラリです。ボイラープレートが少なく、オブジェクト指向的なドメインモデル設計と相性が良いため、ビジネスロジックが複雑なアプリケーションで特に力を発揮します。observerの囲み忘れと分割代入によるリアクティビティ喪失にさえ気をつければ、非常に快適な開発体験が得られるでしょう。