Files
Portfolio/blog-posts/36-zustand-vs-jotai.md
damjan_savicandClaude Opus 4.5 43484c5023 Add blog posts, cleanup unused files, update components
- 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>
2026-01-25 11:42:11 +01:00

524 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)