1. Zustand vs Redux — der ehrliche Vergleich
Redux war jahrelang Standard, aber der Boilerplate-Overhead ist real: Actions, Reducers, Selectors, Middleware, Provider-Wrapper — für jeden neuen State-Slice sind dutzende Zeilen nötig. Zustand löst dasselbe Problem mit einem Bruchteil des Codes.
VergleichZustand vs Redux — Funktions-Matrix 2026
| Kriterium |
Zustand |
Redux Toolkit |
Jotai |
| Setup-Code |
~5 Zeilen |
~40 Zeilen |
~3 Zeilen |
| Bundle-Size |
~1.1 KB |
~47 KB |
~3.3 KB |
| Provider nötig? |
Nein |
Ja (Provider) |
Optional |
| Redux DevTools |
Via Middleware |
Nativ |
Via Plugin |
| TypeScript |
Exzellent |
Exzellent |
Exzellent |
| Persistence |
persist() Middleware |
redux-persist (extra) |
atomWithStorage |
| Immer-Integration |
immer() Middleware |
Nativ (createSlice) |
Manuell |
| Learning Curve |
Sehr flach |
Steil (viele Konzepte) |
Flach |
| Server-State |
Manuell |
RTK Query (extra) |
Manuell |
| Empfehlung 2026 |
Client-State |
Legacy / große Teams |
Atomic State |
Wann Zustand, wann TanStack Query? Zustand = Client-State (UI-Zustand, User-Preferences, Auth-State). TanStack Query = Server-State (API-Daten, Caching, Refetching). Die meisten Apps brauchen beide. Claude Code kombiniert sie automatisch sinnvoll.
# Claude Code Prompt: "Zeig mir den Unterschied zwischen Redux und Zustand anhand eines Warenkorbs"
// ❌ Redux Toolkit — der klassische Boilerplate
// cartSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
interface CartState { items: CartItem[]; total: number }
const cartSlice = createSlice({
name: 'cart',
initialState: { items: [], total: 0 } as CartState,
reducers: {
addItem: (state, action: PayloadAction<CartItem>) => {
state.items.push(action.payload)
state.total += action.payload.price
},
removeItem: (state, action: PayloadAction<string>) => {
state.items = state.items.filter(i => i.id !== action.payload)
state.total = state.items.reduce((sum, i) => sum + i.price, 0)
}
}
})
// + store.ts, Provider in App, useAppSelector, useAppDispatch...
// ✅ Zustand — gleiche Funktionalität, 80% weniger Code
import { create } from 'zustand'
interface CartStore {
items: CartItem[]
total: number
addItem: (item: CartItem) => void
removeItem: (id: string) => void
}
const useCartStore = create<CartStore>()((set, get) => ({
items: [],
total: 0,
addItem: (item) => set(state => ({
items: [...state.items, item],
total: state.total + item.price
})),
removeItem: (id) => {
const items = get().items.filter(i => i.id !== id)
set({ items, total: items.reduce((sum, i) => sum + i.price, 0) })
}
}))
// Verwendung — kein Provider, direkt nutzen:
const { items, total, addItem } = useCartStore()
2. Store-Design: create(), TypeScript und Actions
Der Schlüssel zu wartbaren Zustand-Stores liegt im Design: State und Actions im selben Objekt, klare TypeScript-Interfaces, und Actions die genug Logik kapseln um direkt verwendbar zu sein.
StoreVollständiger Store mit TypeScript-Typisierung
# Prompt: "Zustand Store für User-Auth mit TypeScript, Login/Logout, Token-Refresh"
// stores/authStore.ts
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
interface User {
id: string
email: string
name: string
role: 'admin' | 'user' | 'guest'
}
interface AuthState {
// State
user: User | null
token: string | null
isLoading: boolean
error: string | null
isLoggedIn: boolean // Derived — direkt im Store
// Actions
login: (email: string, password: string) => Promise<void>
logout: () => void
setUser: (user: User) => void
clearError: () => void
}
export const useAuthStore = create<AuthState>()(
devtools(
(set, get) => ({
// Initial State
user: null,
token: null,
isLoading: false,
error: null,
isLoggedIn: false,
// Actions mit Business-Logik
login: async (email, password) => {
set({ isLoading: true, error: null }, false, 'auth/loginStart')
try {
const { user, token } = await loginApi(email, password)
set({ user, token, isLoggedIn: true, isLoading: false }, false, 'auth/loginSuccess')
} catch (err) {
set({ error: (err as Error).message, isLoading: false }, false, 'auth/loginError')
}
},
logout: () => {
set({ user: null, token: null, isLoggedIn: false }, false, 'auth/logout')
},
setUser: (user) => set({ user }, false, 'auth/setUser'),
clearError: () => set({ error: null }, false, 'auth/clearError'),
}),
{ name: 'AuthStore' } // DevTools-Label
)
)
// Verwendung in Komponenten
function LoginForm() {
const { login, isLoading, error } = useAuthStore()
const isLoggedIn = useAuthStore(state => state.isLoggedIn) // Single-Selector
return (
<form onSubmit={(e) => { e.preventDefault(); login(email, password) }}>
{error && <ErrorMessage>{error}</ErrorMessage>}
<button disabled={isLoading}>{isLoading ? 'Laden...' : 'Login'}</button>
</form>
)
}
Action-Namen in devtools: Das dritte Argument von set() ist der Action-Name im Redux DevTools. Konvention: 'storeName/actionName'. Claude Code fügt diese automatisch ein wenn devtools aktiviert ist.
Patternget() — Auf aktuellen State in Actions zugreifen
# Prompt: "Zustand Store mit get() für komplexe Actions die auf anderen State-Werten aufbauen"
interface ShoppingStore {
items: CartItem[]
discount: number
total: number
addItem: (item: CartItem) => void
applyDiscount: (code: string) => Promise<boolean>
recalcTotal: () => void
}
const useShoppingStore = create<ShoppingStore>()((set, get) => ({
items: [],
discount: 0,
total: 0,
recalcTotal: () => {
// get() gibt IMMER aktuellen State zurück
const { items, discount } = get()
const subtotal = items.reduce((sum, i) => sum + i.price * i.qty, 0)
set({ total: subtotal * (1 - discount / 100) })
},
addItem: (item) => {
const existing = get().items.find(i => i.id === item.id)
if (existing) {
set(state => ({
items: state.items.map(i => i.id === item.id ? {...i, qty: i.qty + 1} : i)
}))
} else {
set(state => ({ items: [...state.items, {...item, qty: 1}] }))
}
get().recalcTotal() // Andere Action aufrufen via get()
},
applyDiscount: async (code) => {
const discount = await validateDiscountCode(code)
if (discount) {
set({ discount })
get().recalcTotal() // Nach Discount Neuberechnung
return true
}
return false
}
}))
3. Slices-Pattern: Großen Store modular aufteilen
Wenn eine App wächst, wird ein einzelner monolithischer Store unübersichtlich. Das Slices-Pattern teilt ihn in thematische Module auf — die dennoch einen gemeinsamen Store teilen und aufeinander zugreifen können.
SlicescombineStores — Modularer Store mit TypeScript
# Prompt: "Zustand Store in Slices aufteilen: Auth, UI, Cart — alle im selben Store kombiniert"
// stores/slices/authSlice.ts
import type { StateCreator } from 'zustand'
export interface AuthSlice {
user: User | null
isLoggedIn: boolean
login: (email: string, pw: string) => Promise<void>
logout: () => void
}
// StateCreator erhält alle Store-Types — Zugriff auf andere Slices möglich!
export const createAuthSlice: StateCreator<
AuthSlice & UISlice & CartSlice, // Kompletter Store-Typ
[],
[],
AuthSlice
> = (set, get) => ({
user: null,
isLoggedIn: false,
login: async (email, pw) => {
const user = await loginApi(email, pw)
set({ user, isLoggedIn: true })
// Zugriff auf anderen Slice nach Login
get().loadCart(user.id) // CartSlice-Action!
},
logout: () => {
set({ user: null, isLoggedIn: false })
get().clearCart() // CartSlice beim Logout leeren
}
})
// stores/slices/uiSlice.ts
export interface UISlice {
theme: 'light' | 'dark'
sidebarOpen: boolean
activeModal: string | null
toggleTheme: () => void
toggleSidebar: () => void
openModal: (name: string) => void
closeModal: () => void
}
export const createUISlice: StateCreator<AuthSlice & UISlice & CartSlice, [], [], UISlice> =
(set) => ({
theme: 'light',
sidebarOpen: false,
activeModal: null,
toggleTheme: () => set(s => ({ theme: s.theme === 'light' ? 'dark' : 'light' })),
toggleSidebar: () => set(s => ({ sidebarOpen: !s.sidebarOpen })),
openModal: (name) => set({ activeModal: name }),
closeModal: () => set({ activeModal: null }),
})
// stores/useStore.ts — Alles zusammenführen
import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'
type AppStore = AuthSlice & UISlice & CartSlice
export const useStore = create<AppStore>()(
devtools(
persist(
(...a) => ({
...createAuthSlice(...a),
...createUISlice(...a),
...createCartSlice(...a),
}),
{
name: 'app-store',
// Nur bestimmte State-Teile persistieren
partialize: (state) => ({ theme: state.theme, user: state.user })
}
)
)
)
// Typisierte Slice-Selektoren für einzelne Bereiche
export const useAuth = () => useStore(({ user, isLoggedIn, login, logout }) =>
({ user, isLoggedIn, login, logout })
)
export const useUI = () => useStore(({ theme, sidebarOpen, toggleTheme, toggleSidebar }) =>
({ theme, sidebarOpen, toggleTheme, toggleSidebar })
)
Slice-Zugriff auf andere Slices: Durch den kombinierten Typ AuthSlice & UISlice & CartSlice in StateCreator kann jeder Slice via get() auf Actions anderer Slices zugreifen — ohne Coupling auf Implementierungsebene.
4. Middleware: persist, devtools und immer
Middlewares sind das mächtigste Feature von Zustand — sie wrappen den Store und erweitern ihn um Persistence, Debugging und komfortable Mutation-Patterns, ohne den Store-Code zu verändern.
persistlocalStorage-Persistence mit partialize
# Prompt: "Zustand Store mit persist Middleware, nur ausgewählte Felder speichern, Custom Storage"
import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware'
interface UserPreferencesStore {
theme: 'light' | 'dark' | 'system'
language: string
notifications: boolean
fontSize: number
recentSearches: string[] // Nicht persistieren (zu groß)
sessionData: object // Nicht persistieren (sensibel)
setTheme: (theme: 'light' | 'dark' | 'system') => void
setLanguage: (lang: string) => void
toggleNotify: () => void
addSearch: (term: string) => void
}
export const usePreferences = create<UserPreferencesStore>()(
persist(
(set) => ({
theme: 'system',
language: 'de',
notifications: true,
fontSize: 16,
recentSearches: [],
sessionData: {},
setTheme: (theme) => set({ theme }),
setLanguage: (language) => set({ language }),
toggleNotify: () => set(s => ({ notifications: !s.notifications })),
addSearch: (term) => set(s => ({
recentSearches: [term, ...s.recentSearches.filter(t => t !== term)].slice(0, 10)
})),
}),
{
name: 'user-preferences', // localStorage Key
storage: createJSONStorage(() => localStorage),
// Nur diese Felder persistieren!
partialize: (state) => ({
theme: state.theme,
language: state.language,
notifications: state.notifications,
fontSize: state.fontSize,
// recentSearches und sessionData werden NICHT gespeichert
}),
// Migration bei Store-Änderungen
version: 2,
migrate: (persisted: any, version: number) => {
if (version === 0) {
// Version 0 → 1: 'darkMode' boolean zu 'theme' string
return { ...persisted, theme: persisted.darkMode ? 'dark' : 'light' }
}
if (version === 1) {
// Version 1 → 2: 'system' als neuer Theme-Wert
return persisted // Rückwärtskompatibel
}
return persisted
}
}
)
)
immerImmer.js-Integration für komfortable Mutations
# Prompt: "Zustand mit immer Middleware für tief verschachtelte State-Updates"
import { create } from 'zustand'
import { immer } from 'zustand/middleware/immer'
interface TreeState {
nodes: Record<string, { id: string; name: string; children: string[]; expanded: boolean }>
toggleNode: (id: string) => void
renameNode: (id: string, name: string) => void
addChild: (parentId: string, child: { id: string; name: string }) => void
}
const useTreeStore = create<TreeState>()(
immer((set) => ({
nodes: {
root: { id: 'root', name: 'Root', children: ['a', 'b'], expanded: true },
a: { id: 'a', name: 'Node A', children: [], expanded: false },
},
// Mit immer: direkte Mutations statt Spread-Orgie!
toggleNode: (id) => set(state => {
state.nodes[id].expanded = !state.nodes[id].expanded // ✅ Direkte Mutation!
}),
renameNode: (id, name) => set(state => {
state.nodes[id].name = name // ✅ Kein Spread nötig
}),
addChild: (parentId, child) => set(state => {
state.nodes[parentId].children.push(child.id) // ✅ Array push direkt
state.nodes[child.id] = { ...child, children: [], expanded: false }
}),
// Ohne immer wäre toggleNode so:
// set(state => ({ nodes: { ...state.nodes, [id]: { ...state.nodes[id], expanded: !state.nodes[id].expanded } } }))
// → Mit 3 Ebenen Verschachtelung wird das schnell unleserlich
}))
)
# Middleware-Stack — Reihenfolge ist wichtig!
# devtools( persist( immer( ... ) ) ) → devtools als äußerste Schicht
const useComplexStore = create<ComplexState>()(
devtools(
persist(
immer(
(set, get) => ({ /* Store-Definition */ })
),
{ name: 'complex-store' }
),
{ name: 'ComplexStore' }
)
)
Immer-Middleware vs Immer-Paket direkt: Nutze immer aus zustand/middleware/immer — nicht das immer-Paket direkt im set()-Callback. Die Zustand-Middleware integriert sich korrekt mit dem Proxy-System und anderen Middlewares.
5. Async Actions und Loading States
Zustand braucht keine extra Bibliothek für Async — Actions sind einfach async-Funktionen. Das Pattern für Loading States, Error Handling und Optimistic Updates ist dabei konsistent und einfach verständlich.
AsyncLoading States ohne extra Library
# Prompt: "Zustand Store für API-Calls mit Loading/Error States und Optimistic Updates"
interface PostsStore {
posts: Post[]
loading: boolean
error: string | null
savingId: string | null // Welcher Post wird gerade gespeichert
fetchPosts: () => Promise<void>
createPost: (data: CreatePostData) => Promise<Post>
updatePost: (id: string, data: Partial<Post>) => Promise<void>
deletePost: (id: string) => Promise<void>
}
export const usePostsStore = create<PostsStore>()((set, get) => ({
posts: [],
loading: false,
error: null,
savingId: null,
fetchPosts: async () => {
set({ loading: true, error: null })
try {
const posts = await fetchPostsApi()
set({ posts, loading: false })
} catch (e) {
set({ error: (e as Error).message, loading: false })
}
},
// Optimistic Update: UI sofort aktualisieren, bei Fehler zurückrollen
updatePost: async (id, data) => {
const prev = get().posts // Snapshot für Rollback
// Sofort UI updaten
set(state => ({
savingId: id,
posts: state.posts.map(p => p.id === id ? { ...p, ...data } : p)
}))
try {
await updatePostApi(id, data)
set({ savingId: null })
} catch (e) {
// Rollback bei API-Fehler
set({ posts: prev, savingId: null, error: (e as Error).message })
}
},
deletePost: async (id) => {
const prev = get().posts
set(state => ({ posts: state.posts.filter(p => p.id !== id) })) // Optimistic
try {
await deletePostApi(id)
} catch (e) {
set({ posts: prev, error: (e as Error).message }) // Rollback
}
},
createPost: async (data) => {
const post = await createPostApi(data)
set(state => ({ posts: [post, ...state.posts] }))
return post
}
}))
// Verwendung in Komponenten
function PostList() {
const { posts, loading, error, fetchPosts, savingId } = usePostsStore()
useEffect(() => { fetchPosts() }, [])
if (loading) return <Spinner />
if (error) return <ErrorMessage>{error}</ErrorMessage>
return posts.map(post => (
<PostCard key={post.id} post={post} isSaving={savingId === post.id} />
))
}
PatternMulti-Request Tracking ohne Race-Conditions
# Prompt: "Zustand Store der mehrere parallele Requests trackt und Race-Conditions verhindert"
interface DataStore {
data: Record<string, unknown>
loading: Set<string> // Set statt boolean für Multiple Requests
errors: Record<string, string>
fetchItem: (key: string) => Promise<void>
isLoading: (key: string) => boolean
}
const useDataStore = create<DataStore>()((set, get) => ({
data: {},
loading: new Set(),
errors: {},
isLoading: (key) => get().loading.has(key),
fetchItem: async (key) => {
if (get().isLoading(key)) return // Deduplication
if (get().data[key]) return // Bereits gecacht
set(state => ({ loading: new Set([...state.loading, key]) }))
try {
const result = await fetchItemApi(key)
set(state => {
const loading = new Set(state.loading)
loading.delete(key)
return { loading, data: { ...state.data, [key]: result } }
})
} catch (e) {
set(state => {
const loading = new Set(state.loading)
loading.delete(key)
return { loading, errors: { ...state.errors, [key]: (e as Error).message } }
})
}
}
}))
6. Selektoren und Performance-Optimierung
Jede Zustand-Store-Subscription re-rendert die Komponente sobald irgendetwas im Store sich ändert. Performance-Optimierung bedeutet hier: nur auf relevante State-Teile subscriben und Equality-Checks nutzen.
PerformanceuseShallow für Objekt-Selektoren
# Prompt: "Zustand Performance-Optimierung: useShallow, Fine-grained Subscriptions, Equality"
import { useShallow } from 'zustand/react/shallow'
// ❌ Schlechte Performance: ganzen Store subscriben
function BadComponent() {
const store = useStore() // Re-render bei JEDER Store-Änderung!
return <div>{store.user?.name}</div>
}
// ✅ Gut: einzelner primitiver Wert
function GoodComponent() {
// Re-render NUR wenn user.name sich ändert
const userName = useStore(state => state.user?.name)
return <div>{userName}</div>
}
// ✅ Gut: useShallow für mehrere Werte
function MultiValueComponent() {
// useShallow verhindert Re-render wenn Objekte referenz-gleich aber inhaltlich gleich sind
const { theme, language, fontSize } = useStore(
useShallow(state => ({
theme: state.theme,
language: state.language,
fontSize: state.fontSize
}))
)
return <Settings theme={theme} language={language} fontSize={fontSize} />
}
// ✅ Gut: Array mit useShallow
function ProductList() {
const [items, total] = useCartStore(
useShallow(state => [state.items, state.total])
)
return <CartDisplay items={items} total={total} />
}
// ✅ Memoized Selector für berechnete Werte
import { useMemo } from 'react'
function CartSummary() {
const items = useCartStore(state => state.items)
// Berechnung nur wenn items sich ändert
const summary = useMemo(() => ({
count: items.reduce((n, i) => n + i.qty, 0),
total: items.reduce((s, i) => s + i.price * i.qty, 0),
brands: [...new Set(items.map(i => i.brand))]
}), [items])
return <div>{summary.count} Artikel — €{summary.total.toFixed(2)}</div>
}
AdvancedsubscribeWithSelector — Außerhalb von React reagieren
# Prompt: "Zustand subscribeWithSelector für nicht-React Code: Sync zu localStorage, WebSocket"
import { create } from 'zustand'
import { subscribeWithSelector } from 'zustand/middleware'
const useAppStore = create<AppState>()(
subscribeWithSelector((set) => ({
user: null,
theme: 'light',
wsState: 'disconnected' as 'connected' | 'disconnected',
setWsState: (s: 'connected' | 'disconnected') => set({ wsState: s })
}))
)
// Außerhalb von React subscriben — z.B. in einem Service
// subscribeWithSelector ermöglicht granulare Subscriptions
// 1. Theme-Änderungen → document.body class updaten
useAppStore.subscribe(
(state) => state.theme, // Selector
(theme) => { // Callback (nur wenn theme sich ändert)
document.body.classList.toggle('dark', theme === 'dark')
},
{ fireImmediately: true } // Sofort beim Start ausführen
)
// 2. User-Login → WebSocket verbinden/trennen
let ws: WebSocket | null = null
useAppStore.subscribe(
(state) => state.user,
(user, prevUser) => {
if (user && !prevUser) {
ws = new WebSocket(`wss://api.example.com/ws?token=${user.token}`)
ws.onopen = () => useAppStore.getState().setWsState('connected')
ws.onclose = () => useAppStore.getState().setWsState('disconnected')
} else if (!user && prevUser) {
ws?.close()
ws = null
}
}
)
// 3. Store direkt außerhalb von React lesen
// useAppStore.getState() — kein Hook nötig!
function apiMiddleware(request: Request) {
const { user } = useAppStore.getState()
if (user?.token) {
request.headers.set('Authorization', `Bearer ${user.token}`)
}
return request
}
// 4. Store außerhalb von React updaten (z.B. in einem Service/Worker)
useAppStore.setState({ wsState: 'connected' }) // Direkt ohne Hook
getState() und setState() ohne React: Zustand ist nicht auf React angewiesen. store.getState() und store.setState() funktionieren in Services, WebWorker, Node.js und jedem anderen Kontext. Claude Code nutzt das für Architektur-Patterns wie Repository Pattern oder Service Layer.
TestingZustand Stores in Tests isolieren
# Prompt: "Zustand Stores für Unit-Tests zurücksetzen, Mock-State injizieren"
import { act, renderHook } from '@testing-library/react'
import { useCartStore } from './cartStore'
// Store vor jedem Test zurücksetzen
beforeEach(() => {
act(() => useCartStore.setState({ items: [], total: 0 }))
})
test('addItem erhöht total korrekt', () => {
const { result } = renderHook(() => useCartStore())
act(() => {
result.current.addItem({ id: '1', name: 'Test', price: 9.99, qty: 1 })
})
expect(result.current.items).toHaveLength(1)
expect(result.current.total).toBe(9.99)
})
// Alternativer Ansatz: createStore statt create für besseres Testing
import { createStore } from 'zustand/vanilla'
const createCartStore = () => createStore<CartState>()((set) => ({
items: [],
total: 0,
addItem: (item) => set(s => ({ items: [...s.items, item], total: s.total + item.price }))
}))
test('isolierter Store pro Test', () => {
const store = createCartStore() // Neuer Store-Instance je Test
store.getState().addItem({ id: '1', name: 'X', price: 5, qty: 1 })
expect(store.getState().total).toBe(5)
})
persist-Middleware in Tests: Die persist-Middleware schreibt in localStorage. In Tests kann das zu Interference zwischen Tests führen. Lösung: localStorage.clear() in beforeEach oder createJSONStorage(() => new MemoryStorage()) verwenden.
Claude Code und Zustand: Workflow in der Praxis
Was Claude Code bei Zustand besonders stark macht ist das systemische Denken: Nicht nur einzelne Stores generieren, sondern die gesamte State-Architektur planen — welcher State gehört in Zustand, welcher in TanStack Query, welcher in React local state?
WorkflowClaude Code State-Architektur-Entscheidungen
# Prompt: "Analysiere diese Komponente und entscheide: Zustand vs TanStack Query vs useState"
// Claude Code's State-Decision-Framework:
// useState → Lokaler UI-State (Formular, Toggle, lokale Animation)
const [isOpen, setIsOpen] = useState(false)
const [inputValue, setInputValue] = useState('')
// Zustand → Shared Client-State (Auth, UI-Preferences, App-State, Cart)
const { user, theme } = useStore()
// TanStack Query → Server-State (API-Daten mit Caching + Sync)
const { data: posts } = useQuery({ queryKey: ['posts'], queryFn: fetchPosts })
// Die meisten Apps brauchen alle drei:
function App() {
// useState: Drawer offen/zu
const [drawerOpen, setDrawerOpen] = useState(false)
// Zustand: Eingeloggter User + Theme
const { user, theme } = useStore(useShallow(s => ({ user: s.user, theme: s.theme })))
// TanStack Query: Userdaten vom Server
const { data: profile } = useQuery({
queryKey: ['profile', user?.id],
queryFn: () => fetchProfile(user!.id),
enabled: !!user // Nur wenn User eingeloggt
})
}
Zustand-Modul im Claude Code Kurs
Im Claude Code Mastery Kurs lernst du Zustand von Grund auf: Store-Design, Slices-Pattern, alle Middlewares (persist, devtools, immer), Async Actions, Performance-Optimierung und Integration mit TanStack Query — alles mit TypeScript und echten Projekten.
14 Tage kostenlos testen →