Zum Inhalt springen

State Management 2026: Was du wann einsetzen solltest

Praktischer Leitfaden zur Wahl des State-Managements in modernen React-Apps – von useState über Zustand bis React Query – und die Gründe dahinter.

3 Min. Lesezeit
Entscheidungsbaum von „neuer Status?“ zu vier Zuhause: lokale UI-Hooks, ein TanStack Query-Servercache, ein Zustand-Client-Store und URL-Parameter.

Das State-Management-Paradoxon

Noch nie gab es so viele Möglichkeiten für State Management wie heute – und trotzdem kommt die Mehrzahl der Anwendungen mit einer Handvoll davon aus. Der häufigste Fehler, den ich sehe: zu einer globalen State-Library zu greifen, bevor einfachere Lösungen überhaupt ausprobiert wurden.

Das folgende Entscheidungsschema habe ich über die Arbeit an Dutzenden React-Anwendungen hinweg verfeinert.

Die Hierarchie des State

Bevor du ein Tool auswählst, ordne zunächst ein, was du überhaupt verwaltest:

KategorieBeispieleBeste Wahl
Lokaler UI-StateModal geöffnet, Formulareingabe, HoveruseState / useReducer
Geteilter UI-Stateausgewählter Tab, Sidebar geöffnetContext oder Zustand
Server-CacheNutzerdaten, ProduktlisteTanStack Query / SWR
URL-StateFilter, Paginierung, SucheURL-Parameter / nuqs
Persistente EinstellungenTheme, SprachelocalStorage + context

Die meisten „Wir brauchen Redux“-Entscheidungen sind in Wirklichkeit „Wir brauchen TanStack Query“-Entscheidungen.

Ebene 1: lokaler State — hier fängst du an

Starte immer mit lokalem State. Der überwiegende Teil des UI-State gehört zu genau einer Komponente.

tstypescript
// ✅ This doesn't need to be global
function Modal({ onClose }: { onClose: () => void }) {
  const [step, setStep] = useState<"details" | "confirm" | "done">("details");
  const [formData, setFormData] = useState({ name: "", email: "" });
 
  return (
    <dialog>
      {step === "details" && (
        <DetailsForm data={formData} onChange={setFormData} onNext={() => setStep("confirm")} />
      )}
      {step === "confirm" && (
        <ConfirmStep data={formData} onBack={() => setStep("details")} onSubmit={handleSubmit} />
      )}
    </dialog>
  );
}

Bei komplexem lokalem State mit mehreren Teilwerten führt useReducer zu saubererem Code als mehrere einzelne useState-Aufrufe.

Ebene 2: Server-State — TanStack Query

Wenn du API-Daten in einem globalen Store ablegst, arbeitest du gegen die Natur der Sache. Server-State hat eigene Besonderheiten – er ist asynchron, kann veralten und durch andere Operationen invalidiert werden. Genau dafür ist TanStack Query gebaut.

tstypescript
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
 
function UserProfile({ userId }: { userId: string }) {
  const queryClient = useQueryClient();
 
  // Automatic loading, error, and staleness handling
  const { data: user, isLoading } = useQuery({
    queryKey: ["users", userId],
    queryFn: () => fetchUser(userId),
    staleTime: 5 * 60 * 1000, // Fresh for 5 minutes
  });
 
  const updateUser = useMutation({
    mutationFn: (data: Partial<User>) => patchUser(userId, data),
    onSuccess: () => {
      // Invalidate and refetch after mutation
      queryClient.invalidateQueries({ queryKey: ["users", userId] });
    },
  });
 
  if (isLoading) return <Skeleton />;
 
  return (
    <form onSubmit={(e) => {
      e.preventDefault();
      updateUser.mutate({ name: e.target.name.value });
    }}>
      <input name="name" defaultValue={user.name} />
      <button type="submit" disabled={updateUser.isPending}>
        {updateUser.isPending ? "Saving..." : "Save"}
      </button>
    </form>
  );
}

Das ersetzt manuell verwalteten Ladezustand, Fehlerzustand, Caching-Logik, Refetching im Hintergrund und optimistische Updates – alles, was sonst in deinem globalen Store landen würde.

Ebene 3: URL-State — unterschätzt und selten genutzt

Filter, Paginierung, Sortierung und Suchanfragen gehören in die URL. Das ist kostenloser State, der:

  • Seiten-Neuladen übersteht
  • sich per Link teilen lässt
  • mit Vor- und Zurück-Buttons des Browsers funktioniert
  • von Suchmaschinen indexiert werden kann
tstypescript
import { useQueryStates } from "nuqs";
 
function ProductList() {
  const [params, setParams] = useQueryStates({
    page: parseAsInteger.withDefault(1),
    sort: parseAsString.withDefault("newest"),
    category: parseAsString.withDefault(""),
    minPrice: parseAsInteger.withDefault(0),
  });
 
  const { data } = useQuery({
    queryKey: ["products", params],
    queryFn: () => fetchProducts(params),
  });
 
  return (
    <div>
      <FilterBar filters={params} onChange={setParams} />
      <ProductGrid products={data?.items} />
      <Pagination
        page={params.page}
        total={data?.total}
        onChange={(page) => setParams({ page })}
      />
    </div>
  );
}

Ebene 4: Client-State — Zustand, wenn du ihn brauchst

Wenn du State hast, der:

  • über viele, weit voneinander entfernte Komponenten hinweg geteilt wird
  • keine Serverdaten enthält
  • sich nicht ohne Weiteres in der Nähe seiner Verwendung unterbringen lässt

Zustand ist die sauberste Lösung – minimales Boilerplate, keine Provider nötig, exzellenter TypeScript-Support.

tstypescript
import { create } from "zustand";
import { persist } from "zustand/middleware";
 
interface CartStore {
  items: CartItem[];
  addItem: (product: Product, quantity: number) => void;
  removeItem: (productId: string) => void;
  clearCart: () => void;
  total: () => number;
}
 
export const useCartStore = create<CartStore>()(
  persist(
    (set, get) => ({
      items: [],
 
      addItem: (product, quantity) =>
        set((state) => {
          const existing = state.items.find((i) => i.productId === product.id);
          if (existing) {
            return {
              items: state.items.map((i) =>
                i.productId === product.id
                  ? { ...i, quantity: i.quantity + quantity }
                  : i,
              ),
            };
          }
          return {
            items: [
              ...state.items,
              { productId: product.id, product, quantity },
            ],
          };
        }),
 
      removeItem: (productId) =>
        set((state) => ({
          items: state.items.filter((i) => i.productId !== productId),
        })),
 
      clearCart: () => set({ items: [] }),
 
      total: () =>
        get().items.reduce(
          (sum, item) => sum + item.product.price * item.quantity,
          0,
        ),
    }),
    { name: "cart-storage" }, // Auto-persisted to localStorage
  ),
);

Der Entscheidungsbaum

New state to manage?
│
├─ Is it UI state for one component?
│  └─ YES → useState / useReducer
│
├─ Is it data from a server?
│  └─ YES → TanStack Query
│
├─ Should it be in the URL?
│  └─ YES → URL params (nuqs)
│
├─ Is it shared across many components?
│  └─ YES → Zustand
│
└─ Is it a user preference that should persist?
   └─ YES → localStorage + small Zustand slice

Das beste State-Management-System ist eines, das du kaum bemerkst. Wenn du ständig mit deiner State-Library kämpfst, übernimmt sie wahrscheinlich zu viele Aufgaben, die andere Ebenen besser erledigen könnten.

Wilfredo Rujel

Wilfredo Rujel

Full-Stack-Softwareentwickler

Diesen Beitrag teilenX