
Frontend State Management 2026: Zustand vs Jotai vs Redux vs Signals
Frontend State Management 2026: Zustand vs Jotai vs Redux vs Signals
Ein umfassender Vergleich der führenden State-Management-Lösungen 2026: Zustand, Jotai, Redux Toolkit und Signals. Mit Codebeispielen und Empfehlungen.
Die Evolution des State Managements
State Management ist seit jeher eines der meistdiskutierten Themen in der Frontend-Entwicklung. 2026 hat sich die Landschaft deutlich verändert: Redux ist nicht mehr der unbestrittene Standard, und neue Ansätze wie Signals gewinnen an Bedeutung.
In diesem Artikel vergleichen wir die vier relevantesten State-Management-Lösungen: Zustand, Jotai, Redux Toolkit und Signals. Wir analysieren sie anhand von Code-Beispielen, Performance-Benchmarks und praktischen Anwendungsszenarien.
Überblick der Kandidaten
Zustand
Zustand (deutsch: "Zustand") ist eine minimalistische State-Management-Bibliothek, die auf einfache API und geringe Komplexität setzt.
import { create } from "zustand";
interface CounterStore {
count: number;
increment: () => void;
decrement: () => void;
reset: () => void;
}
const useCounterStore = create<CounterStore>((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
}));
// Verwendung in einer Komponente
function Counter() {
const { count, increment, decrement } = useCounterStore();
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
);
}
Kennzahlen:
- Paketgröße: ~1.1 KB (gzipped)
- GitHub-Stars: 50k+
- API-Oberfläche: Minimal (~5 Kernkonzepte)
Jotai
Jotai verfolgt einen Bottom-up-Ansatz mit atomarem State-Management, inspiriert von Recoil.
import { atom, useAtom } from "jotai";
// Atoms definieren
const countAtom = atom(0);
const doubleCountAtom = atom((get) => get(countAtom) * 2);
// Abgeleitete Atoms mit Write-Logik
const incrementAtom = atom(
null,
(get, set) => {
set(countAtom, get(countAtom) + 1);
}
);
// Verwendung
function Counter() {
const [count, setCount] = useAtom(countAtom);
const [doubleCount] = useAtom(doubleCountAtom);
const [, increment] = useAtom(incrementAtom);
return (
<div>
<p>Count: {count}</p>
<p>Double: {doubleCount}</p>
<button onClick={increment}>+</button>
<button onClick={() => setCount((c) => c - 1)}>-</button>
</div>
);
}
Kennzahlen:
- Paketgröße: ~3.5 KB (gzipped)
- GitHub-Stars: 20k+
- API-Oberfläche: Mittel (~10 Kernkonzepte)
Redux Toolkit
Redux Toolkit ist die offizielle, empfohlene Methode, Redux zu verwenden. Es vereinfacht die ursprüngliche Redux-Komplexität erheblich.
import { configureStore, createSlice, PayloadAction } from "@reduxjs/toolkit";
import { useSelector, useDispatch } from "react-redux";
// Slice erstellen
const counterSlice = createSlice({
name: "counter",
initialState: { count: 0 },
reducers: {
increment: (state) => {
state.count += 1;
},
decrement: (state) => {
state.count -= 1;
},
incrementByAmount: (state, action: PayloadAction<number>) => {
state.count += action.payload;
},
reset: (state) => {
state.count = 0;
},
},
});
// Store konfigurieren
const store = configureStore({
reducer: {
counter: counterSlice.reducer,
},
});
type RootState = ReturnType<typeof store.getState>;
type AppDispatch = typeof store.dispatch;
// Custom Hooks
const useAppSelector = useSelector.withTypes<RootState>();
const useAppDispatch = useDispatch.withTypes<AppDispatch>();
// Verwendung
function Counter() {
const count = useAppSelector((state) => state.counter.count);
const dispatch = useAppDispatch();
return (
<div>
<p>Count: {count}</p>
<button onClick={() => dispatch(counterSlice.actions.increment())}>
+
</button>
<button onClick={() => dispatch(counterSlice.actions.decrement())}>
-
</button>
</div>
);
}
Kennzahlen:
- Paketgröße: ~11 KB (gzipped, mit react-redux)
- GitHub-Stars: 60k+ (Redux gesamt)
- API-Oberfläche: Groß (~20+ Kernkonzepte)
Signals
Signals sind ein reaktives Primitiv, das in verschiedenen Frameworks implementiert ist. In React werden sie hauptsächlich über @preact/signals-react oder das TC39 Signals Proposal genutzt.
import { signal, computed, effect } from "@preact/signals-react";
// Signals definieren
const count = signal(0);
const doubleCount = computed(() => count.value * 2);
// Side Effects
effect(() => {
console.log(`Count ist jetzt: ${count.value}`);
});
// Verwendung - Kein Hook nötig!
function Counter() {
return (
<div>
<p>Count: {count}</p>
<p>Double: {doubleCount}</p>
<button onClick={() => count.value++}>+</button>
<button onClick={() => count.value--}>-</button>
</div>
);
}
Kennzahlen:
- Paketgröße: ~2 KB (gzipped)
- Konzept: TC39 Stage 1 Proposal
- API-Oberfläche: Minimal (~3 Kernkonzepte)
Detaillierter Vergleich
Komplexität und Lernkurve
| Aspekt | Zustand | Jotai | Redux Toolkit | Signals |
|---|---|---|---|---|
| Einarbeitung | 30 Min | 1-2 Std | 1-2 Tage | 1 Std |
| Boilerplate | Minimal | Minimal | Mittel | Minimal |
| Konzepte | Store, set | Atom, get, set | Slice, Action, Reducer, Dispatch | Signal, Computed, Effect |
| TypeScript | Exzellent | Gut | Gut | Gut |
| DevTools | Ja (Extension) | Ja (Extension) | Exzellent | Begrenzt |
Performance
// Performance-Test: 10.000 State-Updates
// Zustand - Batched by default
const useStore = create((set) => ({
items: [],
addItems: (newItems) =>
set((state) => ({ items: [...state.items, ...newItems] })),
}));
// Jotai - Granulare Updates
const itemsAtom = atom([]);
const addItemsAtom = atom(null, (get, set, newItems) => {
set(itemsAtom, [...get(itemsAtom), ...newItems]);
});
// Redux Toolkit - Immer-powered
const itemsSlice = createSlice({
name: "items",
initialState: { items: [] },
reducers: {
addItems: (state, action) => {
state.items.push(...action.payload);
},
},
});
// Signals - Fine-grained reactivity
const items = signal([]);
const addItems = (newItems) => {
items.value = [...items.value, ...newItems];
};
| Benchmark | Zustand | Jotai | Redux Toolkit | Signals |
|---|---|---|---|---|
| Initiale Render-Zeit | 12ms | 14ms | 18ms | 8ms |
| 1.000 Updates/s | 45ms | 42ms | 65ms | 22ms |
| Selektives Re-Render | Gut | Exzellent | Gut (mit Selektoren) | Exzellent |
| Memory-Overhead | Gering | Gering | Mittel | Gering |
| Bundle-Impact | 1.1 KB | 3.5 KB | 11 KB | 2 KB |
Signals gewinnen bei der rohen Performance, da sie auf fine-grained Reactivity setzen und das Virtual DOM umgehen können.
Praxisbeispiel: Todo-App
Schauen wir uns an, wie jede Lösung eine realistische Todo-App implementiert:
Zustand
import { create } from "zustand";
import { persist, devtools } from "zustand/middleware";
interface Todo {
id: string;
text: string;
completed: boolean;
createdAt: Date;
}
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;
filteredTodos: () => Todo[];
}
const useTodoStore = create<TodoStore>()(
devtools(
persist(
(set, get) => ({
todos: [],
filter: "all",
addTodo: (text) =>
set(
(state) => ({
todos: [
...state.todos,
{
id: crypto.randomUUID(),
text,
completed: false,
createdAt: new Date(),
},
],
}),
false,
"addTodo"
),
toggleTodo: (id) =>
set(
(state) => ({
todos: state.todos.map((t) =>
t.id === id ? { ...t, completed: !t.completed } : t
),
}),
false,
"toggleTodo"
),
removeTodo: (id) =>
set(
(state) => ({
todos: state.todos.filter((t) => t.id !== id),
}),
false,
"removeTodo"
),
setFilter: (filter) => set({ filter }),
filteredTodos: () => {
const { todos, filter } = get();
switch (filter) {
case "active":
return todos.filter((t) => !t.completed);
case "completed":
return todos.filter((t) => t.completed);
default:
return todos;
}
},
}),
{ name: "todo-storage" }
)
)
);
Jotai
import { atom } from "jotai";
import { atomWithStorage } from "jotai/utils";
interface Todo {
id: string;
text: string;
completed: boolean;
}
// Basis-Atoms
const todosAtom = atomWithStorage<Todo[]>("todos", []);
const filterAtom = atom<"all" | "active" | "completed">("all");
// Abgeleitetes Atom
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;
}
});
// Action-Atoms
const addTodoAtom = atom(null, (get, set, text: string) => {
const todos = get(todosAtom);
set(todosAtom, [
...todos,
{ id: crypto.randomUUID(), text, completed: false },
]);
});
const toggleTodoAtom = atom(null, (get, set, id: string) => {
const todos = get(todosAtom);
set(
todosAtom,
todos.map((t) =>
t.id === id ? { ...t, completed: !t.completed } : t
)
);
});
Middleware und Erweiterungen
| Feature | Zustand | Jotai | Redux Toolkit | Signals |
|---|---|---|---|---|
| Persistenz | persist | atomWithStorage | redux-persist | Manuell |
| DevTools | devtools | jotai-devtools | Redux DevTools | Begrenzt |
| Async State | Nativ | atomWithQuery | RTK Query | Manuell |
| Immer | immer middleware | atomWithImmer | Eingebaut | N/A |
| Undo/Redo | Manuell | atomWithUndo | redux-undo | Manuell |
Server-seitiger State (SSR/RSC)
Mit dem Aufstieg von React Server Components ist SSR-Kompatibilität wichtig:
// Zustand - SSR mit initialem State
const useStore = create((set) => ({
// ...
}));
// In Server Component
export default async function Page() {
const data = await fetchData();
return <ClientComponent initialData={data} />;
}
// In Client Component
function ClientComponent({ initialData }) {
useEffect(() => {
useStore.setState({ data: initialData });
}, [initialData]);
}
// Jotai - Provider-basiert für SSR
function App({ initialState }) {
return (
<Provider>
<HydrateAtoms initialValues={[[dataAtom, initialState]]}>
<MyApp />
</HydrateAtoms>
</Provider>
);
}

Wann welche Lösung wählen?
Zustand wählen wenn:
- Sie eine einfache, pragmatische Lösung bevorzugen
- Ihr Team neu im State Management ist
- Sie von Redux migrieren wollen (ähnliche Konzepte, weniger Boilerplate)
- Sie globalen State mit minimaler API brauchen
- Performance wichtig ist, aber nicht kritisch
Ideale Anwendungsfälle:
- SaaS-Dashboards
- E-Commerce-Apps
- Content-Management-Systeme
Jotai wählen wenn:
- Sie atomaren, granularen State brauchen
- Ihre App viele unabhängige State-Stücke hat
- Sie React Suspense nutzen wollen
- Performance durch selektives Re-Rendering kritisch ist
Ideale Anwendungsfälle:
- Komplexe Formulare
- Design-Tools (wie ein Color Picker)
- Konfigurationsintensive Apps
Redux Toolkit wählen wenn:
- Sie ein großes Team mit bestehender Redux-Erfahrung haben
- Sie umfangreiche DevTools und Debugging brauchen
- Ihre App einen komplexen, zentralisierten State hat
- Sie RTK Query für Server-State nutzen wollen
Ideale Anwendungsfälle:
- Enterprise-Anwendungen
- Apps mit komplexen State-Transitionen
- Projekte mit strikten Audit-Anforderungen
Signals wählen wenn:
- Maximale Performance Ihr oberstes Ziel ist
- Sie mit der experimentellen Natur leben können
- Ihr Team reaktive Programmierung kennt
- Sie ein Framework wie Preact oder Solid verwenden
Ideale Anwendungsfälle:
- Echtzeit-Dashboards
- Animations-intensive Apps
- Gaming-UIs
- Performance-kritische Anwendungen
Kombination mit Server State
In der Praxis brauchen die meisten Apps sowohl Client- als auch Server-State:
// TanStack Query für Server State + Zustand für Client State
import { useQuery } from "@tanstack/react-query";
import { create } from "zustand";
// Server State mit TanStack Query
function useTodos() {
return useQuery({
queryKey: ["todos"],
queryFn: () => fetch("/api/todos").then((r) => r.json()),
});
}
// Client State mit Zustand
const useUIStore = create((set) => ({
selectedTodoId: null,
isFilterOpen: false,
setSelectedTodo: (id) => set({ selectedTodoId: id }),
toggleFilter: () =>
set((state) => ({ isFilterOpen: !state.isFilterOpen })),
}));
// Komponente die beides nutzt
function TodoList() {
const { data: todos, isLoading } = useTodos();
const { selectedTodoId, setSelectedTodo } = useUIStore();
if (isLoading) return <Spinner />;
return (
<ul>
{todos.map((todo) => (
<li
key={todo.id}
className={todo.id === selectedTodoId ? "selected" : ""}
onClick={() => setSelectedTodo(todo.id)}
>
{todo.text}
</li>
))}
</ul>
);
}

Migration zwischen State-Management-Lösungen
Von Redux zu Zustand
// Redux (vorher)
const todosSlice = createSlice({
name: "todos",
initialState: [],
reducers: {
add: (state, action) => { state.push(action.payload); },
remove: (state, action) => state.filter(t => t.id !== action.payload),
},
});
// Zustand (nachher)
const useTodoStore = create((set) => ({
todos: [],
add: (todo) => set((s) => ({ todos: [...s.todos, todo] })),
remove: (id) => set((s) => ({ todos: s.todos.filter(t => t.id !== id) })),
}));
Von Zustand zu Jotai
// Zustand (vorher)
const useStore = create((set) => ({
count: 0,
name: "",
increment: () => set((s) => ({ count: s.count + 1 })),
setName: (name) => set({ name }),
}));
// Jotai (nachher)
const countAtom = atom(0);
const nameAtom = atom("");
const incrementAtom = atom(null, (get, set) => {
set(countAtom, get(countAtom) + 1);
});
Best Practices 2026
1. Trennen Sie Client- und Server-State
Verwenden Sie TanStack Query oder SWR für Server-State und Zustand/Jotai/Redux nur für Client-State.
2. Halten Sie den State so lokal wie möglich
Nicht jeder State gehört in einen globalen Store. React-eigener State (useState, useReducer) ist oft ausreichend.
3. Verwenden Sie Selektoren
// Zustand: Selektoren vermeiden unnötige Re-Renders
const count = useStore((state) => state.count);
// NICHT: const { count } = useStore();
4. TypeScript ist Pflicht
Alle vier Lösungen unterstützen TypeScript hervorragend. Nutzen Sie es.
5. Testen Sie Ihren State
// Zustand Store testen
import { renderHook, act } from "@testing-library/react-hooks";
test("increment increases count", () => {
const { result } = renderHook(() => useCounterStore());
expect(result.current.count).toBe(0);
act(() => result.current.increment());
expect(result.current.count).toBe(1);
});
Fazit
2026 gibt es keine universell "beste" State-Management-Lösung. Die Wahl hängt von Ihrem Projekt, Team und Ihren Anforderungen ab:
- Zustand ist der beste Allrounder und die sicherste Wahl für die meisten Projekte
- Jotai glänzt bei granularem, atomarem State und React-Suspense-Integration
- Redux Toolkit bleibt die Wahl für große Enterprise-Teams mit bestehender Redux-Infrastruktur
- Signals ist der Performance-Champion, aber noch nicht vollständig standardisiert
Unser Tipp: Starten Sie mit Zustand. Es ist einfach zu lernen, performant und deckt 90% aller Anwendungsfälle ab. Wechseln Sie nur, wenn Sie eine spezifische Anforderung haben, die eine andere Lösung besser erfüllt.
Nutzen Sie unsere Developer Tools beim Entwickeln, etwa den JSON Formatter zum Debuggen von State-Objekten oder den Text Diff zum Vergleichen von State-Snapshots.