
2026年フロントエンド状態管理:Zustand vs Jotai vs Redux vs Signals
2026年フロントエンド状態管理:Zustand vs Jotai vs Redux vs Signals
2026年のフロントエンド状態管理ソリューションを徹底比較。Zustand、Jotai、Redux Toolkit、Signals(Angular/Preact)のバンドルサイズ、パフォーマンス、DX、コード例を詳しく解説。
状態管理の新時代
2026年、フロントエンド開発における状態管理の選択肢は、かつてないほど多様化しています。Redux一択の時代は終わり、Zustand、Jotai、Valtio、そしてSignalsといった新世代のソリューションが台頭しています。
それぞれが異なる設計思想を持ち、異なるユースケースに適しています。本記事では、2026年時点で最も注目される4つの状態管理ソリューションを、実際のコード例と共に徹底比較します。
概要比較
| 項目 | Zustand | Jotai | Redux Toolkit | Signals |
|---|---|---|---|---|
| 設計パターン | フラックス(単一ストア) | アトミック | フラックス(単一ストア) | リアクティブ |
| バンドルサイズ | ~1.1KB (gzip) | ~3.4KB (gzip) | ~11KB (gzip) | ~2KB (gzip) |
| 学習曲線 | 非常に低い | 低い | 中程度 | 低い |
| TypeScript対応 | 優秀 | 優秀 | 優秀 | 優秀 |
| DevTools | あり (Redux DevTools互換) | あり | 優秀 | フレームワーク依存 |
| ミドルウェア | あり | 限定的 | 豊富 | なし |
| React 19対応 | 完全 | 完全 | 完全 | Preact/Angular |
| 初回リリース | 2019年 | 2020年 | 2019年 | 2022年 |
Zustand:シンプルさの極み
概要
Zustandは、Poimandresグループが開発した超軽量な状態管理ライブラリです。「クマ」を意味するドイツ語のこの名前が示す通り、力強くもシンプルな設計が特徴です。
基本的な使い方
import { create } from 'zustand';
// ストアの定義
interface TodoStore {
todos: Todo[];
filter: 'all' | 'active' | 'completed';
addTodo: (text: string) => void;
toggleTodo: (id: string) => void;
removeTodo: (id: string) => void;
setFilter: (filter: 'all' | 'active' | 'completed') => void;
}
interface Todo {
id: string;
text: string;
completed: boolean;
createdAt: Date;
}
const useTodoStore = create<TodoStore>((set) => ({
todos: [],
filter: 'all',
addTodo: (text) =>
set((state) => ({
todos: [
...state.todos,
{
id: crypto.randomUUID(),
text,
completed: false,
createdAt: new Date(),
},
],
})),
toggleTodo: (id) =>
set((state) => ({
todos: state.todos.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
),
})),
removeTodo: (id) =>
set((state) => ({
todos: state.todos.filter((todo) => todo.id !== id),
})),
setFilter: (filter) => set({ filter }),
}));
コンポーネントでの使用
function TodoList() {
// 必要な部分だけを選択(セレクタパターン)
const todos = useTodoStore((state) => state.todos);
const filter = useTodoStore((state) => state.filter);
const toggleTodo = useTodoStore((state) => state.toggleTodo);
const filteredTodos = todos.filter((todo) => {
if (filter === 'active') return !todo.completed;
if (filter === 'completed') return todo.completed;
return true;
});
return (
<ul>
{filteredTodos.map((todo) => (
<li key={todo.id} onClick={() => toggleTodo(todo.id)}>
{todo.completed ? '✓' : '○'} {todo.text}
</li>
))}
</ul>
);
}
Zustandの高度な機能
import { create } from 'zustand';
import { devtools, persist, subscribeWithSelector } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
// ミドルウェアを組み合わせた高度なストア
const useStore = create<AppStore>()(
devtools(
persist(
immer(
subscribeWithSelector((set, get) => ({
user: null,
theme: 'light',
notifications: [],
setUser: (user) =>
set((state) => {
state.user = user;
}),
toggleTheme: () =>
set((state) => {
state.theme = state.theme === 'light' ? 'dark' : 'light';
}),
addNotification: (notification) =>
set((state) => {
state.notifications.push(notification);
}),
// 非同期アクション
fetchUser: async (id: string) => {
const response = await fetch(`/api/users/${id}`);
const user = await response.json();
set((state) => {
state.user = user;
});
},
}))
),
{
name: 'app-storage',
partialize: (state) => ({
theme: state.theme,
}),
}
),
{ name: 'AppStore' }
)
);
Zustandの長所と短所
| 長所 | 短所 |
|---|---|
| 超軽量(~1.1KB) | 大規模アプリでの構造化が自由すぎる |
| APIがシンプル | 公式のコード分割パターンが少ない |
| ボイラープレートが最少 | ミドルウェアの組み合わせが複雑になることも |
| React外でも使用可能 | アトミックな更新には不向き |
| DevToolsサポート | — |

Jotai:アトミックな状態管理
概要
Jotaiは、Poimandresグループが開発したアトミック(原子的)な状態管理ライブラリです。Recoilにインスパイアされていますが、よりシンプルで軽量な設計です。「状態」を意味する日本語の「状態(じょうたい)」が名前の由来です。
基本的な使い方
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai';
// アトムの定義
const todosAtom = atom<Todo[]>([]);
const filterAtom = atom<'all' | 'active' | 'completed'>('all');
// 派生アトム(computed)
const filteredTodosAtom = atom((get) => {
const todos = get(todosAtom);
const filter = get(filterAtom);
switch (filter) {
case 'active':
return todos.filter((t) => !t.completed);
case 'completed':
return todos.filter((t) => t.completed);
default:
return todos;
}
});
// 統計情報の派生アトム
const todoStatsAtom = atom((get) => {
const todos = get(todosAtom);
return {
total: todos.length,
completed: todos.filter((t) => t.completed).length,
active: todos.filter((t) => !t.completed).length,
completionRate:
todos.length > 0
? Math.round(
(todos.filter((t) => t.completed).length / todos.length) * 100
)
: 0,
};
});
// 書き込みアトム(アクション)
const addTodoAtom = atom(null, (get, set, text: string) => {
const todos = get(todosAtom);
set(todosAtom, [
...todos,
{
id: crypto.randomUUID(),
text,
completed: false,
createdAt: new Date(),
},
]);
});
const toggleTodoAtom = atom(null, (get, set, id: string) => {
const todos = get(todosAtom);
set(
todosAtom,
todos.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
});
コンポーネントでの使用
function TodoList() {
const filteredTodos = useAtomValue(filteredTodosAtom);
const toggleTodo = useSetAtom(toggleTodoAtom);
return (
<ul>
{filteredTodos.map((todo) => (
<li key={todo.id} onClick={() => toggleTodo(todo.id)}>
{todo.completed ? '✓' : '○'} {todo.text}
</li>
))}
</ul>
);
}
function TodoStats() {
const stats = useAtomValue(todoStatsAtom);
return (
<div>
<p>合計: {stats.total}</p>
<p>完了: {stats.completed}</p>
<p>未完了: {stats.active}</p>
<p>完了率: {stats.completionRate}%</p>
</div>
);
}
function AddTodo() {
const [text, setText] = useState('');
const addTodo = useSetAtom(addTodoAtom);
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
if (text.trim()) {
addTodo(text.trim());
setText('');
}
};
return (
<form onSubmit={handleSubmit}>
<input value={text} onChange={(e) => setText(e.target.value)} />
<button type="submit">追加</button>
</form>
);
}
Jotaiの非同期アトム
import { atom } from 'jotai';
import { atomWithQuery } from 'jotai-tanstack-query';
// 非同期の派生アトム
const userIdAtom = atom(1);
const userAtom = atom(async (get) => {
const id = get(userIdAtom);
const response = await fetch(`/api/users/${id}`);
return response.json();
});
// TanStack Queryとの統合
const usersQueryAtom = atomWithQuery(() => ({
queryKey: ['users'],
queryFn: async () => {
const response = await fetch('/api/users');
return response.json();
},
}));
// ローカルストレージとの同期
import { atomWithStorage } from 'jotai/utils';
const themeAtom = atomWithStorage('theme', 'light');
const languageAtom = atomWithStorage('language', 'ja');
Jotaiの長所と短所
| 長所 | 短所 |
|---|---|
| 細粒度のリアクティビティ | アトムが増えると管理が複雑に |
| React Suspenseとの統合 | 大規模アプリでの規約が少ない |
| コードスプリットが自然 | DevToolsがZustandほど成熟していない |
| ボトムアップ設計 | ミドルウェアの概念がない |
| TypeScriptの推論が優秀 | — |
Redux Toolkit:王者の進化
概要
Redux Toolkitは、Reduxの公式推奨ツールキットです。かつてのReduxの冗長さを大幅に削減し、2026年でも大規模アプリケーションのスタンダードとして健在です。
基本的な使い方
import { createSlice, configureStore, PayloadAction } from '@reduxjs/toolkit';
// Sliceの定義
const todoSlice = createSlice({
name: 'todos',
initialState: {
items: [] as Todo[],
filter: 'all' as 'all' | 'active' | 'completed',
isLoading: false,
error: null as string | null,
},
reducers: {
addTodo: (state, action: PayloadAction<string>) => {
state.items.push({
id: crypto.randomUUID(),
text: action.payload,
completed: false,
createdAt: new Date().toISOString(),
});
},
toggleTodo: (state, action: PayloadAction<string>) => {
const todo = state.items.find((t) => t.id === action.payload);
if (todo) {
todo.completed = !todo.completed;
}
},
removeTodo: (state, action: PayloadAction<string>) => {
state.items = state.items.filter((t) => t.id !== action.payload);
},
setFilter: (
state,
action: PayloadAction<'all' | 'active' | 'completed'>
) => {
state.filter = action.payload;
},
},
});
// ストアの設定
const store = configureStore({
reducer: {
todos: todoSlice.reducer,
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
export const { addTodo, toggleTodo, removeTodo, setFilter } =
todoSlice.actions;
RTK Queryによるデータフェッチ
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
// APIの定義
const apiSlice = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
tagTypes: ['Todos', 'Users'],
endpoints: (builder) => ({
getTodos: builder.query<Todo[], void>({
query: () => '/todos',
providesTags: ['Todos'],
}),
addTodo: builder.mutation<Todo, string>({
query: (text) => ({
url: '/todos',
method: 'POST',
body: { text },
}),
invalidatesTags: ['Todos'],
}),
toggleTodo: builder.mutation<Todo, string>({
query: (id) => ({
url: `/todos/${id}/toggle`,
method: 'PATCH',
}),
invalidatesTags: ['Todos'],
}),
}),
});
export const { useGetTodosQuery, useAddTodoMutation, useToggleTodoMutation } =
apiSlice;
コンポーネントでの使用
import { useSelector, useDispatch } from 'react-redux';
function TodoList() {
// RTK Queryを使った場合
const { data: todos, isLoading, error } = useGetTodosQuery();
const [toggleTodo] = useToggleTodoMutation();
if (isLoading) return <div>読み込み中...</div>;
if (error) return <div>エラーが発生しました</div>;
return (
<ul>
{todos?.map((todo) => (
<li key={todo.id} onClick={() => toggleTodo(todo.id)}>
{todo.completed ? '✓' : '○'} {todo.text}
</li>
))}
</ul>
);
}
Redux Toolkitの長所と短所
| 長所 | 短所 |
|---|---|
| 最も成熟したエコシステム | バンドルサイズが大きい(~11KB) |
| RTK Query が強力 | 他と比べてボイラープレートが多い |
| DevTools が最高品質 | 小規模アプリにはオーバーキル |
| ミドルウェアが豊富 | 学習コストが比較的高い |
| 大規模アプリでの実績 | Immerの挙動を理解する必要あり |

Signals:リアクティビティの未来
概要
Signalsは、細粒度のリアクティビティを実現する新しいプリミティブです。Angular、Preact、Solid.js、Vue.jsなど多くのフレームワークで採用されており、2026年にはフロントエンド状態管理のパラダイムシフトを起こしています。
Preact Signalsの例
import { signal, computed, effect, batch } from '@preact/signals-react';
// シグナルの定義
const todos = signal<Todo[]>([]);
const filter = signal<'all' | 'active' | 'completed'>('all');
// 計算シグナル(自動的に依存関係を追跡)
const filteredTodos = computed(() => {
const f = filter.value;
const t = todos.value;
switch (f) {
case 'active':
return t.filter((todo) => !todo.completed);
case 'completed':
return t.filter((todo) => todo.completed);
default:
return t;
}
});
const todoStats = computed(() => ({
total: todos.value.length,
completed: todos.value.filter((t) => t.completed).length,
active: todos.value.filter((t) => !t.completed).length,
}));
// アクション
function addTodo(text: string) {
todos.value = [
...todos.value,
{
id: crypto.randomUUID(),
text,
completed: false,
createdAt: new Date(),
},
];
}
function toggleTodo(id: string) {
todos.value = todos.value.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
);
}
// エフェクト(副作用)
effect(() => {
console.log(`TODOが${todos.value.length}件あります`);
localStorage.setItem('todos', JSON.stringify(todos.value));
});
Angular Signalsの例
import { Component, signal, computed, effect } from '@angular/core';
@Component({
selector: 'app-todo-list',
template: `
<div>
<h2>TODO リスト ({{ stats().total }}件)</h2>
<form (submit)="addTodo($event)">
<input #input placeholder="新しいTODO" />
<button type="submit">追加</button>
</form>
<div>
<button (click)="filter.set('all')">すべて</button>
<button (click)="filter.set('active')">未完了</button>
<button (click)="filter.set('completed')">完了</button>
</div>
<ul>
@for (todo of filteredTodos(); track todo.id) {
<li (click)="toggleTodo(todo.id)">
{{ todo.completed ? '✓' : '○' }} {{ todo.text }}
</li>
}
</ul>
</div>
`,
})
export class TodoListComponent {
todos = signal<Todo[]>([]);
filter = signal<'all' | 'active' | 'completed'>('all');
filteredTodos = computed(() => {
const f = this.filter();
return this.todos().filter((todo) => {
if (f === 'active') return !todo.completed;
if (f === 'completed') return todo.completed;
return true;
});
});
stats = computed(() => ({
total: this.todos().length,
completed: this.todos().filter((t) => t.completed).length,
}));
constructor() {
effect(() => {
console.log('TODOが変更されました:', this.todos());
});
}
addTodo(event: Event) {
event.preventDefault();
const input = (event.target as HTMLFormElement).querySelector('input')!;
if (input.value.trim()) {
this.todos.update((todos) => [
...todos,
{
id: crypto.randomUUID(),
text: input.value.trim(),
completed: false,
createdAt: new Date(),
},
]);
input.value = '';
}
}
toggleTodo(id: string) {
this.todos.update((todos) =>
todos.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
}
}
Signalsの長所と短所
| 長所 | 短所 |
|---|---|
| 超高速な更新(O(1)) | React標準ではない(Preact版が必要) |
| 自動的な依存関係追跡 | エコシステムが発展途上 |
| 最小限の再レンダリング | DevToolsが限定的 |
| バンドルサイズが小さい | 大規模アプリでの実績が少ない |
| フレームワーク非依存の概念 | ミドルウェアの概念がない |
パフォーマンス比較
ベンチマーク結果(2026年3月時点)
1,000件のアイテムリストで、1件を更新した場合の再レンダリングパフォーマンスを比較しました。
| 指標 | Zustand | Jotai | Redux Toolkit | Signals |
|---|---|---|---|---|
| 初回レンダリング | 12ms | 14ms | 16ms | 8ms |
| 単一アイテム更新 | 3ms | 2ms | 4ms | <1ms |
| 再レンダリング範囲 | セレクタに依存 | アトム単位 | 接続コンポーネント | シグナル単位 |
| メモリ使用量 | 低い | 低い | 中程度 | 非常に低い |
| GCプレッシャー | 低い | 中程度 | 中程度 | 非常に低い |
バンドルサイズ詳細
Zustand: 1.1KB (gzip) ← 最軽量
Jotai: 3.4KB (gzip)
Signals: ~2KB (gzip)
Redux Toolkit: 11KB (gzip) ← 最大(RTK Query含まず)
RTK + Query: ~25KB (gzip)
バンドルサイズは、特にモバイルユーザーの初回ロード時間に影響します。軽量なライブラリを選ぶことは、パフォーマンスの観点から重要な判断です。
移行ガイド
ReduxからZustandへの移行
// Before: Redux Toolkit
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: (state) => { state.value += 1; },
decrement: (state) => { state.value -= 1; },
incrementByAmount: (state, action: PayloadAction<number>) => {
state.value += action.payload;
},
},
});
// After: Zustand
const useCounterStore = create<CounterStore>((set) => ({
value: 0,
increment: () => set((state) => ({ value: state.value + 1 })),
decrement: () => set((state) => ({ value: state.value - 1 })),
incrementByAmount: (amount: number) =>
set((state) => ({ value: state.value + amount })),
}));
ReduxからJotaiへの移行
// Before: Redux selector
const selectTodos = (state: RootState) => state.todos.items;
const selectFilter = (state: RootState) => state.todos.filter;
const selectFilteredTodos = createSelector(
[selectTodos, selectFilter],
(todos, filter) => {
// フィルタリングロジック
}
);
// After: Jotai atoms
const todosAtom = atom<Todo[]>([]);
const filterAtom = atom<string>('all');
const filteredTodosAtom = atom((get) => {
const todos = get(todosAtom);
const filter = get(filterAtom);
// フィルタリングロジック(自動的にメモ化される)
});
選択基準のまとめ
プロジェクト規模別の推奨
| プロジェクト規模 | 推奨 | 理由 |
|---|---|---|
| 小規模(個人開発) | Zustand | シンプル、軽量、学習コスト最小 |
| 中規模(チーム5-10人) | Zustand or Jotai | 柔軟性とシンプルさのバランス |
| 大規模(チーム10人+) | Redux Toolkit | 規約の強制、DevTools、実績 |
| 新規フレームワーク採用 | Signals | Angular/Preact/Solid使用時 |
| パフォーマンス最重視 | Signals or Jotai | 細粒度の更新 |
ユースケース別の推奨
| ユースケース | 推奨 | 理由 |
|---|---|---|
| フォーム状態 | Jotai | フィールド単位の更新が効率的 |
| サーバー状態 | RTK Query | キャッシュ、再検証が強力 |
| UIテーマ/設定 | Zustand + persist | シンプルで永続化が容易 |
| リアルタイムデータ | Signals | 最高の更新パフォーマンス |
| 複雑なビジネスロジック | Redux Toolkit | ミドルウェア、構造化された設計 |
2026年のトレンド
- Zustandの支配: npm ダウンロード数でReduxを超え、最も人気のある選択肢に
- Signalsの成長: TC39での標準化提案が進行中、将来的にブラウザネイティブに
- サーバー状態の分離: TanStack QueryやSWRとの併用が標準パターンに
- React Server Componentsとの共存: サーバー側ではデータフェッチ、クライアント側では状態管理という分業
実践的なアドバイス
1. サーバー状態とクライアント状態を分離する
// サーバー状態: TanStack Query
const { data: users } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
});
// クライアント状態: Zustand
const useUIStore = create((set) => ({
sidebarOpen: false,
theme: 'dark',
toggleSidebar: () =>
set((state) => ({ sidebarOpen: !state.sidebarOpen })),
}));
2. グローバル状態を最小限に保つ
すべての状態をグローバルストアに入れる必要はありません。コンポーネントローカルのuseStateで十分な場合は、それを使いましょう。
3. セレクタを活用する
// 悪い例:ストア全体をサブスクライブ
const store = useStore();
// 良い例:必要な部分だけを選択
const count = useStore((state) => state.count);
const increment = useStore((state) => state.increment);
まとめ
2026年のフロントエンド状態管理は、かつてのRedux一択から、多様なソリューションが共存する成熟した環境へと進化しました。
- Zustand: 迷ったらこれ。最もシンプルで軽量、多くのプロジェクトに適合
- Jotai: 細粒度の更新が必要な場合に最適。React Suspenseとの親和性が高い
- Redux Toolkit: 大規模チーム、複雑なビジネスロジック、エンタープライズ向け
- Signals: 次世代のリアクティビティ。Angular/Preact/Solidのプロジェクトに最適
重要なのは、ツールに振り回されるのではなく、プロジェクトの要件に基づいて適切な選択をすることです。どのライブラリも2026年時点で十分に成熟しており、間違った選択は存在しません。
フロントエンド開発の効率化には、無料のCSSグラデーションジェネレーターやカラーピッカー、HTMLエンコーダーもぜひご活用ください。UIの微調整やテンプレート作成に役立ちます。