Menu
Back to Blog
2 min read
Web Development

Zustand vs. Jotai: Moderne State Management für React 2026

Vergleich der führenden React State Management Libraries. Zustand vs. Jotai - Architektur, Performance, Use Cases und Entscheidungshilfe.

ZustandJotaiReact State ManagementRedux AlternativeAtomic StateGlobal State
Zustand vs. Jotai: Moderne State Management für React 2026

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

// 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

// 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

// 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

// 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

// 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

// 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

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

// 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

// 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

KriteriumZustandJotai
**Lernkurve**Sehr einfachEinfach
**Bundle Size**1.2KB2.2KB
**Globaler State**ExzellentGut
**Komponenten-State**GutExzellent
**Derived State**SelectorsAtoms (eleganter)
**Async**MiddlewareNativ
**DevTools**JaJa
**Redux Migration**EinfachSchwieriger

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