Back

Zustand vs Jotai: Choosing the Right State Manager for Your React App

Zustand vs Jotai: Choosing the Right State Manager for Your React App

New to Zustand or Jotai? Check out our in-depth guides first:

React state management has evolved significantly beyond Redux’s complexity. For small to medium projects, lightweight alternatives like Zustand and Jotai have gained popularity. But which one should you choose? This article compares these two libraries created by the same developer (Daishi Kato) to help you make an informed decision based on your project needs.

Key Takeaways

  • Zustand uses a centralized, top-down approach ideal for interconnected state and team collaboration
  • Jotai uses an atomic, bottom-up approach perfect for fine-grained reactivity and rapidly changing data
  • Both libraries are lightweight, performant, and TypeScript-friendly
  • Zustand is often better for larger applications with complex state relationships
  • Jotai excels in scenarios requiring independent pieces of state with minimal re-renders

The Origins and Philosophy of Zustand and Jotai

Both libraries were created to solve specific problems in the React ecosystem, but with different approaches:

Zustand: The Top-Down Approach

Zustand (German for “state”) was released in 2019 as a simpler alternative to Redux. It follows a centralized, top-down approach to 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: The Bottom-Up Approach

Jotai (Japanese for “state”) was released in 2020 and takes inspiration from Recoil. It uses an atomic, bottom-up approach where state is broken into small, independent atoms.

import { atom, useAtom } from 'jotai'

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

Mental Models: How They Approach State Differently

Understanding the mental model behind each library is crucial for choosing the right one for your project.

Zustand’s Store-Based Model

Zustand uses a single store that contains all your state and actions. This model is familiar to developers who have used Redux:

// Creating a store
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 });
    }
  }
}));

// Using the store in a component
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 Atomic Model

Jotai breaks state into atoms that can be composed together. This is similar to React’s own useState but with the ability to share state across components:

// Creating atoms
const userAtom = atom(null);
const isLoadingAtom = atom(false);
const errorAtom = atom(null);

// Creating a derived atom
const userStatusAtom = atom(
  (get) => ({
    user: get(userAtom),
    isLoading: get(isLoadingAtom),
    error: get(errorAtom)
  })
);

// Creating an action atom
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);
    }
  }
);

// Using atoms in a component
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 Considerations: Zustand vs Jotai

Both libraries are designed to be performant, but they optimize for different scenarios.

Zustand’s Performance Profile

  • Selective Subscriptions: Components only re-render when their selected state changes
  • Bundle Size: ~2.8kB minified and gzipped
  • Middleware Support: Built-in middleware for performance optimization
  • Batched Updates: Automatically batches state updates

Jotai’s Performance Profile

  • Granular Updates: Only components using specific atoms re-render
  • Bundle Size: ~3.5kB minified and gzipped (core package)
  • Atom-Level Optimization: Fine-grained control over which state changes trigger re-renders
  • Derived State: Efficiently handles computed values

For rapidly changing data that affects only specific parts of your UI, Jotai’s atomic approach often results in fewer re-renders. For interconnected state that changes less frequently, Zustand’s approach can be more efficient.

TypeScript Integration

Both libraries provide excellent TypeScript support, but with different approaches.

Zustand with 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 with 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 });
    }
  }
);

When to Choose Zustand

Zustand is often the better choice when:

  1. You need a centralized store: For applications with interconnected state that needs to be accessed and modified from many components.

  2. You’re transitioning from Redux: Zustand’s API is more familiar to Redux users, making migration easier.

  3. You need non-React access to state: Zustand allows you to access and modify state outside React components.

  4. Team collaboration is a priority: The centralized store approach can be easier to maintain in larger teams.

  5. You prefer explicit state updates: Zustand’s approach makes state changes more traceable.

When to Choose Jotai

Jotai shines when:

  1. You need fine-grained reactivity: For UIs with many independent pieces of state that change frequently.

  2. You’re building complex forms: Jotai’s atomic approach works well for form fields that need to be independently validated.

  3. You want a useState-like API: If you prefer an API that closely resembles React’s built-in hooks.

  4. You’re working with rapidly changing data: For real-time applications where minimizing re-renders is critical.

  5. You need derived state: Jotai makes it easy to create computed values based on other state.

Real-World Implementation Patterns

Let’s look at some common patterns implemented in both libraries.

Authentication State

With 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 });
  }
}));

With 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);
  }
);

Form State Management

With Zustand:

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

With 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) => {
    // Validation logic
    const errors = {};
    set(errorsAtom, errors);
    return Object.keys(errors).length === 0;
  }
);

Migration Strategies

From Redux to 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 }))
}));

From Context API to 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 <>{/* No provider needed */}</>;
}

Common Pitfalls and Best Practices

Zustand Pitfalls

  1. Store Fragmentation: Creating too many stores can lead to state management confusion.
  2. Selector Memoization: Forgetting to memoize selectors can cause unnecessary re-renders.
  3. Middleware Overuse: Adding too many middleware can impact performance.

Jotai Pitfalls

  1. Atom Proliferation: Creating too many atoms without organization can make code hard to follow.
  2. Circular Dependencies: Creating atoms that depend on each other in a circular way.
  3. Atom Duplication: Accidentally creating multiple instances of the same atom.

Best Practices for Both

  1. Organize Related State: Group related state and actions together.
  2. Use TypeScript: Both libraries benefit from TypeScript’s type safety.
  3. Document Your State Structure: Make it clear how your state is organized.
  4. Test Your State Logic: Write unit tests for your state management code.

Conclusion

Choosing between Zustand and Jotai depends on your specific project requirements. Zustand offers a centralized approach that works well for complex, interconnected state in larger applications. Jotai provides an atomic model that excels at fine-grained reactivity with minimal re-renders. Both libraries deliver lightweight, performant solutions that significantly improve upon Redux’s complexity while maintaining TypeScript compatibility.

Consider your team’s familiarity with different state management patterns, your application’s performance needs, and your state structure when making your decision. Remember that you can even use both libraries in the same application, leveraging each for what it does best.

FAQs

Yes, many developers use Zustand for global application state and Jotai for component-specific state that needs to be shared across the component tree.

Yes, both can scale to large applications when used properly. Zustand might be easier to maintain in large team settings due to its centralized approach.

Both are significantly lighter and simpler than Redux. Zustand is closer to Redux in philosophy but with much less boilerplate. Jotai takes a completely different approach focused on atomic state.

Zustand doesn't require a provider by default. Jotai can be used without a provider for global atoms but offers a provider for scoped state.

Yes, both libraries work well with Next.js and other React frameworks. They provide specific utilities for server-side rendering support.

Listen to your bugs 🧘, with OpenReplay

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