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>
This commit is contained in:
@@ -0,0 +1,523 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user