Back

Zustand vs Jotai: Die richtige State-Management-Lösung für Ihre React-App wählen

Zustand vs Jotai: Die richtige State-Management-Lösung für Ihre React-App wählen

Neu bei Zustand oder Jotai? Schauen Sie sich zuerst unsere ausführlichen Leitfäden an:

Das State Management in React hat sich deutlich über die Komplexität von Redux hinaus entwickelt. Für kleine bis mittlere Projekte haben sich leichtgewichtige Alternativen wie Zustand und Jotai großer Beliebtheit erfreut. Aber welche sollten Sie wählen? Dieser Artikel vergleicht diese beiden Bibliotheken, die vom selben Entwickler (Daishi Kato) erstellt wurden, um Ihnen bei einer fundierten Entscheidung basierend auf Ihren Projektanforderungen zu helfen.

Wichtige Erkenntnisse

  • Zustand verwendet einen zentralisierten, Top-Down-Ansatz, der ideal für miteinander verbundenen State und Teamzusammenarbeit ist
  • Jotai verwendet einen atomaren, Bottom-Up-Ansatz, der perfekt für feinkörnige Reaktivität und sich schnell ändernde Daten ist
  • Beide Bibliotheken sind leichtgewichtig, performant und TypeScript-freundlich
  • Zustand ist oft besser für größere Anwendungen mit komplexen State-Beziehungen geeignet
  • Jotai glänzt in Szenarien, die unabhängige State-Teile mit minimalen Re-Renders erfordern

Die Ursprünge und Philosophie von Zustand und Jotai

Beide Bibliotheken wurden entwickelt, um spezifische Probleme im React-Ökosystem zu lösen, jedoch mit unterschiedlichen Ansätzen:

Zustand: Der Top-Down-Ansatz

Zustand (Deutsch für “state”) wurde 2019 als einfachere Alternative zu Redux veröffentlicht. Es folgt einem zentralisierten, Top-Down-Ansatz für State Management.

import { create } from 'zustand'

const useStore = create((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
}))

Jotai: Der Bottom-Up-Ansatz

Jotai (Japanisch für “state”) wurde 2020 veröffentlicht und lässt sich von Recoil inspirieren. Es verwendet einen atomaren, Bottom-Up-Ansatz, bei dem der State in kleine, unabhängige Atome aufgeteilt wird.

import { atom, useAtom } from 'jotai'

const countAtom = atom(0)
const doubleCountAtom = atom((get) => get(countAtom) * 2)

Mentale Modelle: Wie sie State unterschiedlich angehen

Das Verständnis des mentalen Modells hinter jeder Bibliothek ist entscheidend für die Wahl der richtigen für Ihr Projekt.

Zustand’s Store-basiertes Modell

Zustand verwendet einen einzigen Store, der Ihren gesamten State und alle Aktionen enthält. Dieses Modell ist Entwicklern vertraut, die Redux verwendet haben:

// Erstellen eines Stores
const useUserStore = create((set) => ({
  user: null,
  isLoading: false,
  error: null,
  fetchUser: async (id) => {
    set({ isLoading: true });
    try {
      const response = await fetch(`/api/users/${id}`);
      const user = await response.json();
      set({ user, isLoading: false });
    } catch (error) {
      set({ error, isLoading: false });
    }
  }
}));

// Verwendung des Stores in einer Komponente
function Profile({ userId }) {
  const { user, fetchUser } = useUserStore(
    state => ({ user: state.user, fetchUser: state.fetchUser })
  );
  
  useEffect(() => {
    fetchUser(userId);
  }, [userId, fetchUser]);
  
  return user ? <div>{user.name}</div> : <div>Loading...</div>;
}

Jotai’s atomares Modell

Jotai teilt den State in Atome auf, die zusammengesetzt werden können. Dies ähnelt React’s eigenem useState, aber mit der Möglichkeit, State zwischen Komponenten zu teilen:

// Erstellen von Atomen
const userAtom = atom(null);
const isLoadingAtom = atom(false);
const errorAtom = atom(null);

// Erstellen eines abgeleiteten Atoms
const userStatusAtom = atom(
  (get) => ({
    user: get(userAtom),
    isLoading: get(isLoadingAtom),
    error: get(errorAtom)
  })
);

// Erstellen eines Action-Atoms
const fetchUserAtom = atom(
  null,
  async (get, set, userId) => {
    set(isLoadingAtom, true);
    try {
      const response = await fetch(`/api/users/${userId}`);
      const user = await response.json();
      set(userAtom, user);
      set(isLoadingAtom, false);
    } catch (error) {
      set(errorAtom, error);
      set(isLoadingAtom, false);
    }
  }
);

// Verwendung von Atomen in einer Komponente
function Profile({ userId }) {
  const [{ user, isLoading }] = useAtom(userStatusAtom);
  const [, fetchUser] = useAtom(fetchUserAtom);
  
  useEffect(() => {
    fetchUser(userId);
  }, [userId, fetchUser]);
  
  return user ? <div>{user.name}</div> : <div>Loading...</div>;
}

Performance-Überlegungen: Zustand vs Jotai

Beide Bibliotheken sind auf Performance ausgelegt, aber sie optimieren für verschiedene Szenarien.

Zustand’s Performance-Profil

  • Selektive Subscriptions: Komponenten rendern nur neu, wenn sich ihr ausgewählter State ändert
  • Bundle-Größe: ~2,8kB minifiziert und gzipped
  • Middleware-Unterstützung: Eingebaute Middleware für Performance-Optimierung
  • Batch-Updates: Automatisches Batching von State-Updates

Jotai’s Performance-Profil

  • Granulare Updates: Nur Komponenten, die spezifische Atome verwenden, rendern neu
  • Bundle-Größe: ~3,5kB minifiziert und gzipped (Core-Paket)
  • Atom-Level-Optimierung: Feinkörnige Kontrolle darüber, welche State-Änderungen Re-Renders auslösen
  • Abgeleiteter State: Effiziente Behandlung berechneter Werte

Für sich schnell ändernde Daten, die nur bestimmte Teile Ihrer UI betreffen, führt Jotai’s atomarer Ansatz oft zu weniger Re-Renders. Für miteinander verbundenen State, der sich weniger häufig ändert, kann Zustand’s Ansatz effizienter sein.

TypeScript-Integration

Beide Bibliotheken bieten exzellente TypeScript-Unterstützung, aber mit unterschiedlichen Ansätzen.

Zustand mit TypeScript

interface BearState {
  bears: number;
  increase: (by: number) => void;
}

const useBearStore = create<BearState>((set) => ({
  bears: 0,
  increase: (by) => set((state) => ({ bears: state.bears + by })),
}));

Jotai mit TypeScript

interface User {
  id: string;
  name: string;
}

const userAtom = atom<User | null>(null);
const nameAtom = atom(
  (get) => get(userAtom)?.name || '',
  (get, set, newName: string) => {
    const user = get(userAtom);
    if (user) {
      set(userAtom, { ...user, name: newName });
    }
  }
);

Wann Sie Zustand wählen sollten

Zustand ist oft die bessere Wahl, wenn:

  1. Sie einen zentralisierten Store benötigen: Für Anwendungen mit miteinander verbundenem State, der von vielen Komponenten aus zugegriffen und modifiziert werden muss.

  2. Sie von Redux migrieren: Zustand’s API ist Redux-Benutzern vertrauter, was die Migration erleichtert.

  3. Sie Zugriff auf State außerhalb von React benötigen: Zustand erlaubt es, State außerhalb von React-Komponenten zu verwenden und zu modifizieren.

  4. Teamzusammenarbeit ist eine Priorität: Der zentralisierte Store-Ansatz kann in größeren Teams einfacher zu warten sein.

  5. Sie explizite State-Updates bevorzugen: Zustand’s Ansatz macht State-Änderungen nachvollziehbarer.

Wann Sie Jotai wählen sollten

Jotai glänzt, wenn:

  1. Sie feinkörnige Reaktivität benötigen: Für UIs mit vielen unabhängigen State-Teilen, die sich häufig ändern.

  2. Sie komplexe Formulare erstellen: Jotai’s atomarer Ansatz funktioniert gut für Formularfelder, die unabhängig validiert werden müssen.

  3. Sie eine useState-ähnliche API wollen: Wenn Sie eine API bevorzugen, die React’s eingebauten Hooks stark ähnelt.

  4. Sie mit sich schnell ändernden Daten arbeiten: Für Echtzeit-Anwendungen, bei denen die Minimierung von Re-Renders kritisch ist.

  5. Sie abgeleiteten State benötigen: Jotai macht es einfach, berechnete Werte basierend auf anderem State zu erstellen.

Praxisnahe Implementierungsmuster

Schauen wir uns einige häufige Muster an, die in beiden Bibliotheken implementiert sind.

Authentifizierungs-State

Mit Zustand:

const useAuthStore = create((set) => ({
  user: null,
  isAuthenticated: false,
  isLoading: false,
  login: async (credentials) => {
    set({ isLoading: true });
    try {
      const user = await loginApi(credentials);
      set({ user, isAuthenticated: true, isLoading: false });
    } catch (error) {
      set({ isLoading: false });
      throw error;
    }
  },
  logout: async () => {
    await logoutApi();
    set({ user: null, isAuthenticated: false });
  }
}));

Mit Jotai:

const userAtom = atom(null);
const isAuthenticatedAtom = atom((get) => !!get(userAtom));
const isLoadingAtom = atom(false);

const loginAtom = atom(
  null,
  async (get, set, credentials) => {
    set(isLoadingAtom, true);
    try {
      const user = await loginApi(credentials);
      set(userAtom, user);
      set(isLoadingAtom, false);
    } catch (error) {
      set(isLoadingAtom, false);
      throw error;
    }
  }
);

const logoutAtom = atom(
  null,
  async (get, set) => {
    await logoutApi();
    set(userAtom, null);
  }
);

Formular-State-Management

Mit Zustand:

const useFormStore = create((set) => ({
  values: { name: '', email: '', message: '' },
  errors: {},
  setField: (field, value) => set(state => ({
    values: { ...state.values, [field]: value }
  })),
  validate: () => {
    // Validierungslogik
    const errors = {};
    set({ errors });
    return Object.keys(errors).length === 0;
  },
  submit: () => {
    // Submit-Logik
  }
}));

Mit Jotai:

const formAtom = atom({ name: '', email: '', message: '' });
const nameAtom = atom(
  (get) => get(formAtom).name,
  (get, set, name) => set(formAtom, { ...get(formAtom), name })
);
const emailAtom = atom(
  (get) => get(formAtom).email,
  (get, set, email) => set(formAtom, { ...get(formAtom), email })
);
const messageAtom = atom(
  (get) => get(formAtom).message,
  (get, set, message) => set(formAtom, { ...get(formAtom), message })
);

const errorsAtom = atom({});
const validateAtom = atom(
  null,
  (get, set) => {
    // Validierungslogik
    const errors = {};
    set(errorsAtom, errors);
    return Object.keys(errors).length === 0;
  }
);

Migrationsstrategien

Von Redux zu Zustand

// Redux
const counterSlice = createSlice({
  name: 'counter',
  initialState: { value: 0 },
  reducers: {
    increment: state => {
      state.value += 1;
    },
    decrement: state => {
      state.value -= 1;
    }
  }
});

// Zustand
const useCounterStore = create((set) => ({
  value: 0,
  increment: () => set(state => ({ value: state.value + 1 })),
  decrement: () => set(state => ({ value: state.value - 1 }))
}));

Von Context API zu Jotai

// Context API
const CounterContext = createContext();

function CounterProvider({ children }) {
  const [count, setCount] = useState(0);
  return (
    <CounterContext.Provider value={{ count, setCount }}>
      {children}
    </CounterContext.Provider>
  );
}

// Jotai
const countAtom = atom(0);

function App() {
  return <>{/* Kein Provider erforderlich */}</>;
}

Häufige Fallstricke und Best Practices

Zustand-Fallstricke

  1. Store-Fragmentierung: Das Erstellen zu vieler Stores kann zu Verwirrung im State Management führen.
  2. Selector-Memoization: Das Vergessen der Memoization von Selektoren kann unnötige Re-Renders verursachen.
  3. Middleware-Übernutzung: Das Hinzufügen zu vieler Middleware kann die Performance beeinträchtigen.

Jotai-Fallstricke

  1. Atom-Proliferation: Das Erstellen zu vieler Atome ohne Organisation kann den Code schwer nachvollziehbar machen.
  2. Zirkuläre Abhängigkeiten: Das Erstellen von Atomen, die zirkulär voneinander abhängen.
  3. Atom-Duplikation: Das versehentliche Erstellen mehrerer Instanzen desselben Atoms.

Best Practices für beide

  1. Verwandten State organisieren: Gruppieren Sie verwandten State und Aktionen zusammen.
  2. TypeScript verwenden: Beide Bibliotheken profitieren von TypeScript’s Typsicherheit.
  3. Ihre State-Struktur dokumentieren: Machen Sie klar, wie Ihr State organisiert ist.
  4. Ihre State-Logik testen: Schreiben Sie Unit-Tests für Ihren State-Management-Code.

Fazit

Die Wahl zwischen Zustand und Jotai hängt von Ihren spezifischen Projektanforderungen ab. Zustand bietet einen zentralisierten Ansatz, der gut für komplexen, miteinander verbundenen State in größeren Anwendungen funktioniert. Jotai bietet ein atomares Modell, das bei feinkörniger Reaktivität mit minimalen Re-Renders glänzt. Beide Bibliotheken liefern leichtgewichtige, performante Lösungen, die Redux’s Komplexität erheblich verbessern, während sie TypeScript-Kompatibilität beibehalten.

Berücksichtigen Sie die Vertrautheit Ihres Teams mit verschiedenen State-Management-Mustern, die Performance-Anforderungen Ihrer Anwendung und Ihre State-Struktur bei Ihrer Entscheidung. Denken Sie daran, dass Sie sogar beide Bibliotheken in derselben Anwendung verwenden können, indem Sie jede für das nutzen, was sie am besten kann.

Häufig gestellte Fragen

Ja, viele Entwickler verwenden Zustand für globalen Anwendungs-State und Jotai für komponenten-spezifischen State, der über den Komponentenbaum geteilt werden muss.

Ja, beide können bei ordnungsgemäßer Verwendung auf große Anwendungen skalieren. Zustand könnte in großen Team-Umgebungen aufgrund seines zentralisierten Ansatzes einfacher zu warten sein.

Beide sind erheblich leichter und einfacher als Redux. Zustand ist Redux in der Philosophie näher, aber mit viel weniger Boilerplate. Jotai verfolgt einen völlig anderen Ansatz, der sich auf atomaren State konzentriert.

Zustand benötigt standardmäßig keinen Provider. Jotai kann ohne Provider für globale Atome verwendet werden, bietet aber einen Provider für bereichsbezogenen State.

Ja, beide Bibliotheken funktionieren gut mit Next.js und anderen React-Frameworks. Sie bieten spezifische Utilities für Server-Side-Rendering-Unterstützung.

Listen to your bugs 🧘, with OpenReplay

See how users use your app and resolve issues fast.
Loved by thousands of developers