- Add 100 blog posts covering AI, development, and tech topics - Add .env.example for environment configuration - Add accessibility and lighthouse audit scripts - Remove obsolete SEO reports and temporary files - Remove dev-dist build artifacts and backup files - Remove unused portrait images (moved/consolidated elsewhere) - Update contact form and component improvements Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
524 lines
13 KiB
Markdown
524 lines
13 KiB
Markdown
# Zustand vs. Jotai: Moderne State Management für React 2026
|
||
|
||
**Meta-Description:** Vergleich der führenden React State Management Libraries. Zustand vs. Jotai - Architektur, Performance, Use Cases und Entscheidungshilfe.
|
||
|
||
**Keywords:** Zustand, Jotai, React State Management, Redux Alternative, Atomic State, Global State, React Context
|
||
|
||
---
|
||
|
||
## Einführung
|
||
|
||
Redux-Fatigue ist real. 2026 dominieren **Zustand** und **Jotai** als leichtgewichtige, TypeScript-first Alternativen. Beide kommen von Poimandres (formerly pmndrs) – aber lösen verschiedene Probleme.
|
||
|
||
---
|
||
|
||
## Quick Comparison
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ ZUSTAND vs. JOTAI │
|
||
├─────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ ZUSTAND JOTAI │
|
||
│ ──────────────────── ──────────────────── │
|
||
│ Store-basiert Atom-basiert │
|
||
│ Top-Down Bottom-Up │
|
||
│ Zentraler State Dezentraler State │
|
||
│ Simpler für globalen State Flexibler für UI State │
|
||
│ │
|
||
│ Best for: Best for: │
|
||
│ • App-weiter State • Component-lokaler State │
|
||
│ • Einfache Stores • Derived State │
|
||
│ • Server State (mit Persist) • Async State │
|
||
│ • Redux-Migration • Code-Splitting │
|
||
│ │
|
||
│ Bundle: ~1.2KB Bundle: ~2.2KB │
|
||
│ │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## Zustand
|
||
|
||
### Basic Store
|
||
|
||
```typescript
|
||
// stores/useStore.ts
|
||
import { create } from 'zustand';
|
||
|
||
interface CounterState {
|
||
count: number;
|
||
increment: () => void;
|
||
decrement: () => void;
|
||
reset: () => void;
|
||
}
|
||
|
||
export const useCounterStore = create<CounterState>((set) => ({
|
||
count: 0,
|
||
increment: () => set((state) => ({ count: state.count + 1 })),
|
||
decrement: () => set((state) => ({ count: state.count - 1 })),
|
||
reset: () => set({ count: 0 })
|
||
}));
|
||
|
||
// Verwendung in Component
|
||
function Counter() {
|
||
const { count, increment, decrement } = useCounterStore();
|
||
|
||
return (
|
||
<div>
|
||
<span>{count}</span>
|
||
<button onClick={increment}>+</button>
|
||
<button onClick={decrement}>-</button>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// Selective Subscription (Performance!)
|
||
function CountDisplay() {
|
||
const count = useCounterStore((state) => state.count);
|
||
return <span>{count}</span>;
|
||
}
|
||
```
|
||
|
||
### Komplexer Store mit Slices
|
||
|
||
```typescript
|
||
// stores/userStore.ts
|
||
import { create } from 'zustand';
|
||
import { persist, devtools } from 'zustand/middleware';
|
||
|
||
interface User {
|
||
id: string;
|
||
name: string;
|
||
email: string;
|
||
}
|
||
|
||
interface UserState {
|
||
user: User | null;
|
||
isAuthenticated: boolean;
|
||
isLoading: boolean;
|
||
error: string | null;
|
||
|
||
login: (email: string, password: string) => Promise<void>;
|
||
logout: () => void;
|
||
updateProfile: (data: Partial<User>) => Promise<void>;
|
||
}
|
||
|
||
export const useUserStore = create<UserState>()(
|
||
devtools(
|
||
persist(
|
||
(set, get) => ({
|
||
user: null,
|
||
isAuthenticated: false,
|
||
isLoading: false,
|
||
error: null,
|
||
|
||
login: async (email, password) => {
|
||
set({ isLoading: true, error: null });
|
||
try {
|
||
const response = await fetch('/api/auth/login', {
|
||
method: 'POST',
|
||
body: JSON.stringify({ email, password })
|
||
});
|
||
|
||
if (!response.ok) throw new Error('Login failed');
|
||
|
||
const user = await response.json();
|
||
set({
|
||
user,
|
||
isAuthenticated: true,
|
||
isLoading: false
|
||
});
|
||
} catch (error) {
|
||
set({
|
||
error: error.message,
|
||
isLoading: false
|
||
});
|
||
}
|
||
},
|
||
|
||
logout: () => {
|
||
set({
|
||
user: null,
|
||
isAuthenticated: false
|
||
});
|
||
},
|
||
|
||
updateProfile: async (data) => {
|
||
const { user } = get();
|
||
if (!user) return;
|
||
|
||
set({ isLoading: true });
|
||
try {
|
||
const response = await fetch('/api/user/profile', {
|
||
method: 'PATCH',
|
||
body: JSON.stringify(data)
|
||
});
|
||
|
||
const updatedUser = await response.json();
|
||
set({
|
||
user: updatedUser,
|
||
isLoading: false
|
||
});
|
||
} catch (error) {
|
||
set({
|
||
error: error.message,
|
||
isLoading: false
|
||
});
|
||
}
|
||
}
|
||
}),
|
||
{
|
||
name: 'user-storage', // localStorage key
|
||
partialize: (state) => ({ user: state.user }) // Nur user persistieren
|
||
}
|
||
),
|
||
{ name: 'UserStore' } // DevTools name
|
||
)
|
||
);
|
||
```
|
||
|
||
### Store Slices Pattern
|
||
|
||
```typescript
|
||
// stores/slices/cartSlice.ts
|
||
import { StateCreator } from 'zustand';
|
||
|
||
export interface CartSlice {
|
||
items: CartItem[];
|
||
addItem: (item: CartItem) => void;
|
||
removeItem: (id: string) => void;
|
||
clearCart: () => void;
|
||
totalPrice: () => number;
|
||
}
|
||
|
||
export const createCartSlice: StateCreator<CartSlice> = (set, get) => ({
|
||
items: [],
|
||
|
||
addItem: (item) => set((state) => ({
|
||
items: [...state.items, item]
|
||
})),
|
||
|
||
removeItem: (id) => set((state) => ({
|
||
items: state.items.filter((i) => i.id !== id)
|
||
})),
|
||
|
||
clearCart: () => set({ items: [] }),
|
||
|
||
totalPrice: () => get().items.reduce((sum, item) => sum + item.price, 0)
|
||
});
|
||
|
||
// stores/index.ts
|
||
import { create } from 'zustand';
|
||
import { CartSlice, createCartSlice } from './slices/cartSlice';
|
||
import { UserSlice, createUserSlice } from './slices/userSlice';
|
||
|
||
type StoreState = CartSlice & UserSlice;
|
||
|
||
export const useStore = create<StoreState>()((...a) => ({
|
||
...createCartSlice(...a),
|
||
...createUserSlice(...a)
|
||
}));
|
||
```
|
||
|
||
---
|
||
|
||
## Jotai
|
||
|
||
### Basic Atoms
|
||
|
||
```typescript
|
||
// atoms/counter.ts
|
||
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai';
|
||
|
||
// Primitive Atom
|
||
export const countAtom = atom(0);
|
||
|
||
// Derived Atom (Read-only)
|
||
export const doubleCountAtom = atom((get) => get(countAtom) * 2);
|
||
|
||
// Derived Atom (Read-Write)
|
||
export const countWithDoubleAtom = atom(
|
||
(get) => get(countAtom),
|
||
(get, set, newValue: number) => {
|
||
set(countAtom, newValue * 2);
|
||
}
|
||
);
|
||
|
||
// Verwendung
|
||
function Counter() {
|
||
const [count, setCount] = useAtom(countAtom);
|
||
const doubleCount = useAtomValue(doubleCountAtom);
|
||
|
||
return (
|
||
<div>
|
||
<span>{count} (double: {doubleCount})</span>
|
||
<button onClick={() => setCount(count + 1)}>+</button>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// Nur Setter (keine Re-renders bei count-Änderung)
|
||
function IncrementButton() {
|
||
const setCount = useSetAtom(countAtom);
|
||
return <button onClick={() => setCount(c => c + 1)}>+</button>;
|
||
}
|
||
```
|
||
|
||
### Async Atoms
|
||
|
||
```typescript
|
||
// atoms/user.ts
|
||
import { atom } from 'jotai';
|
||
import { atomWithQuery } from 'jotai-tanstack-query';
|
||
|
||
// Async Read Atom
|
||
export const userAtom = atom(async () => {
|
||
const response = await fetch('/api/user');
|
||
return response.json();
|
||
});
|
||
|
||
// Mit React Query Integration
|
||
export const userQueryAtom = atomWithQuery(() => ({
|
||
queryKey: ['user'],
|
||
queryFn: async () => {
|
||
const response = await fetch('/api/user');
|
||
return response.json();
|
||
}
|
||
}));
|
||
|
||
// Verwendung mit Suspense
|
||
function UserProfile() {
|
||
const [user] = useAtom(userAtom);
|
||
// user ist bereits resolved!
|
||
return <div>{user.name}</div>;
|
||
}
|
||
|
||
// In parent
|
||
<Suspense fallback={<Loading />}>
|
||
<UserProfile />
|
||
</Suspense>
|
||
```
|
||
|
||
### Atom Families
|
||
|
||
```typescript
|
||
// atoms/todos.ts
|
||
import { atom } from 'jotai';
|
||
import { atomFamily } from 'jotai/utils';
|
||
|
||
interface Todo {
|
||
id: string;
|
||
text: string;
|
||
done: boolean;
|
||
}
|
||
|
||
// Atom für jeden Todo (by ID)
|
||
export const todoAtomFamily = atomFamily((id: string) =>
|
||
atom<Todo | null>(null)
|
||
);
|
||
|
||
// Liste der IDs
|
||
export const todoIdsAtom = atom<string[]>([]);
|
||
|
||
// Derived: Alle Todos
|
||
export const todosAtom = atom((get) => {
|
||
const ids = get(todoIdsAtom);
|
||
return ids.map(id => get(todoAtomFamily(id))).filter(Boolean);
|
||
});
|
||
|
||
// Verwendung
|
||
function TodoItem({ id }: { id: string }) {
|
||
const [todo, setTodo] = useAtom(todoAtomFamily(id));
|
||
|
||
if (!todo) return null;
|
||
|
||
return (
|
||
<div>
|
||
<input
|
||
type="checkbox"
|
||
checked={todo.done}
|
||
onChange={() => setTodo({ ...todo, done: !todo.done })}
|
||
/>
|
||
{todo.text}
|
||
</div>
|
||
);
|
||
}
|
||
```
|
||
|
||
### Persistence mit atomWithStorage
|
||
|
||
```typescript
|
||
import { atomWithStorage } from 'jotai/utils';
|
||
|
||
// Automatisch in localStorage persistiert
|
||
export const themeAtom = atomWithStorage<'light' | 'dark'>('theme', 'light');
|
||
|
||
export const settingsAtom = atomWithStorage('settings', {
|
||
notifications: true,
|
||
language: 'de'
|
||
});
|
||
|
||
// Session Storage
|
||
export const sessionDataAtom = atomWithStorage(
|
||
'session',
|
||
null,
|
||
undefined, // default serializer
|
||
{ getOnInit: true }
|
||
);
|
||
```
|
||
|
||
---
|
||
|
||
## Vergleich: Gleiche Funktionalität
|
||
|
||
### Shopping Cart mit Zustand
|
||
|
||
```typescript
|
||
// stores/cartStore.ts
|
||
import { create } from 'zustand';
|
||
|
||
interface CartStore {
|
||
items: CartItem[];
|
||
addItem: (item: CartItem) => void;
|
||
removeItem: (id: string) => void;
|
||
updateQuantity: (id: string, quantity: number) => void;
|
||
total: number;
|
||
}
|
||
|
||
export const useCartStore = create<CartStore>((set, get) => ({
|
||
items: [],
|
||
|
||
addItem: (item) => set((state) => {
|
||
const existing = state.items.find(i => i.id === item.id);
|
||
if (existing) {
|
||
return {
|
||
items: state.items.map(i =>
|
||
i.id === item.id
|
||
? { ...i, quantity: i.quantity + 1 }
|
||
: i
|
||
)
|
||
};
|
||
}
|
||
return { items: [...state.items, { ...item, quantity: 1 }] };
|
||
}),
|
||
|
||
removeItem: (id) => set((state) => ({
|
||
items: state.items.filter(i => i.id !== id)
|
||
})),
|
||
|
||
updateQuantity: (id, quantity) => set((state) => ({
|
||
items: state.items.map(i =>
|
||
i.id === id ? { ...i, quantity } : i
|
||
)
|
||
})),
|
||
|
||
get total() {
|
||
return get().items.reduce(
|
||
(sum, item) => sum + item.price * item.quantity,
|
||
0
|
||
);
|
||
}
|
||
}));
|
||
```
|
||
|
||
### Shopping Cart mit Jotai
|
||
|
||
```typescript
|
||
// atoms/cart.ts
|
||
import { atom } from 'jotai';
|
||
|
||
export const cartItemsAtom = atom<CartItem[]>([]);
|
||
|
||
export const addItemAtom = atom(
|
||
null,
|
||
(get, set, item: CartItem) => {
|
||
const items = get(cartItemsAtom);
|
||
const existing = items.find(i => i.id === item.id);
|
||
|
||
if (existing) {
|
||
set(cartItemsAtom, items.map(i =>
|
||
i.id === item.id
|
||
? { ...i, quantity: i.quantity + 1 }
|
||
: i
|
||
));
|
||
} else {
|
||
set(cartItemsAtom, [...items, { ...item, quantity: 1 }]);
|
||
}
|
||
}
|
||
);
|
||
|
||
export const removeItemAtom = atom(
|
||
null,
|
||
(get, set, id: string) => {
|
||
set(cartItemsAtom, get(cartItemsAtom).filter(i => i.id !== id));
|
||
}
|
||
);
|
||
|
||
export const cartTotalAtom = atom((get) => {
|
||
const items = get(cartItemsAtom);
|
||
return items.reduce(
|
||
(sum, item) => sum + item.price * item.quantity,
|
||
0
|
||
);
|
||
});
|
||
```
|
||
|
||
---
|
||
|
||
## Entscheidungshilfe
|
||
|
||
| Kriterium | Zustand | Jotai |
|
||
|-----------|---------|-------|
|
||
| **Lernkurve** | Sehr einfach | Einfach |
|
||
| **Bundle Size** | 1.2KB | 2.2KB |
|
||
| **Globaler State** | Exzellent | Gut |
|
||
| **Komponenten-State** | Gut | Exzellent |
|
||
| **Derived State** | Selectors | Atoms (eleganter) |
|
||
| **Async** | Middleware | Nativ |
|
||
| **DevTools** | Ja | Ja |
|
||
| **Redux Migration** | Einfach | Schwieriger |
|
||
|
||
### Wähle Zustand wenn:
|
||
|
||
- Du einen zentralen, Redux-ähnlichen Store willst
|
||
- Dein State hauptsächlich global ist
|
||
- Du von Redux migrierst
|
||
- Du Actions und State zusammen halten willst
|
||
|
||
### Wähle Jotai wenn:
|
||
|
||
- Du viel derived/computed State hast
|
||
- Du feingranulare Re-Renders brauchst
|
||
- Du React Suspense für Async State nutzt
|
||
- Du atomare Updates bevorzugst
|
||
|
||
---
|
||
|
||
## Fazit
|
||
|
||
Beide Libraries sind exzellent. Die Wahl hängt vom Denkmodell ab:
|
||
|
||
- **Zustand**: "Ein Store, viele Slices" (Top-Down)
|
||
- **Jotai**: "Viele Atoms, komponiert" (Bottom-Up)
|
||
|
||
Für die meisten Apps: **Zustand** für Einfachheit, **Jotai** für Flexibilität.
|
||
|
||
---
|
||
|
||
## Bildprompts
|
||
|
||
1. "Two state management approaches - central store vs distributed atoms, architectural diagram"
|
||
2. "React components with state flowing down, tree structure visualization"
|
||
3. "Lightweight boxes vs heavy Redux container, bundle size comparison concept"
|
||
|
||
---
|
||
|
||
## Quellen
|
||
|
||
- [Zustand GitHub](https://github.com/pmndrs/zustand)
|
||
- [Jotai Documentation](https://jotai.org/)
|
||
- [Zustand vs Jotai Comparison](https://docs.pmnd.rs/zustand/getting-started/comparison)
|
||
- [Jotai Tutorial](https://jotai.org/docs/basics/primitives)
|