DEV Community

Cover image for Zustand, When, how and why
Ricardo Esteves
Ricardo Esteves

Posted on

Zustand, When, how and why

Table of Contents

  1. Introduction to State Management
  2. Understanding Zustand
  3. Types of State
  4. Getting Started with Zustand
  5. Key Features of Zustand
  6. Implementing Zustand
  7. Zustand vs. Other State Management Solutions
  8. System Design of Zustand
  9. Persisting State with Zustand
  10. Using Zustand Outside React Components
  11. Real-World Examples
  12. Conclusion
  13. Tips: Handling Asynchronous Code with Zustand

1. Introduction to State Management

State management is a crucial aspect of modern web development, especially in complex applications. It involves handling the data that can change over time and ensuring that this data is consistently represented across the entire application. Effective state management leads to more predictable and maintainable applications.

2. Understanding Zustand

Zustand is a small, fast, and scalable state management solution for React applications. Created by Jared Palmer and Daishi Kato, Zustand offers a simple and intuitive API that makes state management less cumbersome compared to other solutions.

3. Types of State

Before diving deeper into Zustand, let's understand the different types of state in web applications:

  1. Local State: State that is specific to a component and doesn't need to be shared with other parts of the application.
  2. Global State: State that needs to be accessed and modified by multiple components across the application.
  3. Remote State: State that represents data fetched from an external source, typically an API.

Zustand excels in managing both local and global state, and can be integrated with solutions for remote state management.

4. Getting Started with Zustand

To start using Zustand, first install it via npm, yarn or pnpm:

npm install zustand
# or
yarn add zustand
# or
pnpm add zustand
Enter fullscreen mode Exit fullscreen mode

5. Key Features of Zustand

Zustand comes with several features that make it stand out:

  1. Simplicity: Zustand has a minimal API, making it easy to learn and use.
  2. No boilerplate: Unlike some other state management solutions, Zustand requires very little setup code.
  3. Hooks-based: Zustand leverages React Hooks, making it feel native to modern React development.
  4. TypeScript support: Zustand works great with TypeScript, providing excellent type inference.
  5. Middleware support: Zustand allows you to extend its functionality through middleware.
  6. Devtools support: Zustand integrates well with Redux DevTools for debugging.
  7. Framework agnostic: While primarily used with React, Zustand can be used in any JavaScript environment.

6. Implementing Zustand

Let's look at a basic implementation of Zustand:

import { create } from 'zustand'

const useStore = create((set) => ({
  bears: 0,
  increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
  removeAllBears: () => set({ bears: 0 }),
}))

function BearCounter() {
  const bears = useStore((state) => state.bears)
  return <h1>{bears} around here...</h1>
}

function Controls() {
  const increasePopulation = useStore((state) => state.increasePopulation)
  return <button onClick={increasePopulation}>one up</button>
}
Enter fullscreen mode Exit fullscreen mode

In this example, we create a store with a bears state and two actions to modify it. The BearCounter and Controls components can then access and modify the state using the useStore hook.

7. Zustand vs. Other State Management Solutions

Let's compare Zustand with other popular state management solutions:

Zustand vs. Redux

Pros of Zustand:

  • Simpler API with less boilerplate
  • No need for action creators or switch statements
  • Smaller bundle size

Cons:

  • Less established ecosystem
  • Fewer middleware options

Zustand vs. MobX

Pros of Zustand:

  • More straightforward, less magical API
  • Better performance for larger applications

Cons:

  • Less reactive by default
  • No computed values out of the box

Zustand vs. Recoil

Pros of Zustand:

  • Simpler mental model
  • Works outside of React

Cons:

  • Less granular reactivity
  • No built-in atom family concept

8. System Design of Zustand

Zustand's system design is based on a few key principles:

  1. Single store: Zustand encourages the use of a single store for all application state.
  2. Immutability: State updates are handled immutably, ensuring predictable behavior.
  3. Subscriptions: Components subscribe to specific parts of the state, re-rendering only when those parts change.
  4. Middleware: Zustand uses a middleware system for extending functionality.

This design allows Zustand to be both simple and powerful, providing excellent performance even in large applications.

9. Persisting State with Zustand

Zustand makes it easy to persist state, which is crucial for many applications. Here's an example using the persist middleware:

import { create } from 'zustand'
import { persist } from 'zustand/middleware'

const useStore = create(persist(
  (set, get) => ({
    fishes: 0,
    addAFish: () => set({ fishes: get().fishes + 1 }),
  }),
  {
    name: 'food-storage', // unique name
    getStorage: () => localStorage, // (optional) by default, 'localStorage' is used
  }
))
Enter fullscreen mode Exit fullscreen mode

This will automatically save the state to localStorage and rehydrate it when the app reloads.

10. Using Zustand Outside React Components

One of Zustand's strengths is that it can be used outside of React components. This is particularly useful for integrating with other parts of your application or for testing:

const { getState, setState } = useStore

// Getting state
console.log(getState().bears)

// Setting state
setState({ bears: 10 })

// Using actions
getState().increasePopulation()
Enter fullscreen mode Exit fullscreen mode

11. Real-World Examples

Let's look at some real-world examples of using Zustand:

Authentication State

import { create } from 'zustand'

const useAuthStore = create((set) => ({
  user: null,
  isAuthenticated: false,
  login: (userData) => set({ user: userData, isAuthenticated: true }),
  logout: () => set({ user: null, isAuthenticated: false }),
}))

// Usage in a component
function LoginButton() {
  const { isAuthenticated, login, logout } = useAuthStore()

  const handleAuth = () => {
    if (isAuthenticated) {
      logout()
    } else {
      // Simulate login
      login({ id: 1, name: 'John Doe' })
    }
  }

  return (
    <button onClick={handleAuth}>
      {isAuthenticated ? 'Logout' : 'Login'}
    </button>
  )
}
Enter fullscreen mode Exit fullscreen mode

Shopping Cart

import { create } from 'zustand'

const useCartStore = create((set) => ({
  items: [],
  addItem: (item) => set((state) => ({ items: [...state.items, item] })),
  removeItem: (itemId) => set((state) => ({
    items: state.items.filter((item) => item.id !== itemId),
  })),
  clearCart: () => set({ items: [] }),
  total: 0,
  updateTotal: () => set((state) => ({
    total: state.items.reduce((sum, item) => sum + item.price, 0),
  })),
}))

// Usage in components
function CartSummary() {
  const { items, total, removeItem } = useCartStore()

  return (
    <div>
      {items.map((item) => (
        <div key={item.id}>
          {item.name} - ${item.price}
          <button onClick={() => removeItem(item.id)}>Remove</button>
        </div>
      ))}
      <div>Total: ${total}</div>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Theme Switcher

import { create } from 'zustand'
import { persist } from 'zustand/middleware'

const useThemeStore = create(persist(
  (set) => ({
    theme: 'light',
    toggleTheme: () => set((state) => ({
      theme: state.theme === 'light' ? 'dark' : 'light',
    })),
  }),
  {
    name: 'theme-storage',
  }
))

// Usage in a component
function ThemeToggle() {
  const { theme, toggleTheme } = useThemeStore()

  return (
    <button onClick={toggleTheme}>
      Switch to {theme === 'light' ? 'dark' : 'light'} mode
    </button>
  )
}
Enter fullscreen mode Exit fullscreen mode

12. Conclusion

Zustand offers a refreshing approach to state management in React applications. Its simplicity, flexibility, and performance make it an excellent choice for both small and large projects. By reducing boilerplate and providing a straightforward API, Zustand allows developers to focus on building features rather than managing complex state logic.

While it may not have the extensive ecosystem of some older state management solutions, Zustand's design principles and ease of use make it a compelling option for modern React development. Its ability to work outside of React components and easy integration with persistence solutions further extend its utility.

For many React applications, Zustand strikes an excellent balance between simplicity and power, making it worth considering for your next project.

Bonus tips:

Zustand also handles asynchronous functions/code really well and without the need for any Middleware setup.

Let's talk a bit about that:

13. Handling Asynchronous Code with Zustand

One of Zustand's strengths is its simplicity in handling asynchronous operations without the need for additional middleware or complex setups. This makes it particularly easy to work with API calls, data fetching, and other asynchronous tasks.

How Zustand Handles Asynchronous Code

Zustand's approach to asynchronous code is straightforward:

  1. Direct Integration: Asynchronous functions can be defined directly in the store.
  2. No Special Syntax: You don't need to use special action creators or thunks.
  3. State Updates: You can update the state within async functions using the set function.
  4. Error Handling: Error states can be managed directly within the async functions.

Implementing Asynchronous Code

Here's an example of how to implement asynchronous code in Zustand:

import { create } from 'zustand'

const useUserStore = create((set) => ({
  user: null,
  isLoading: false,
  error: null,
  fetchUser: async (userId) => {
    set({ isLoading: true, error: null });
    try {
      const response = await fetch(`https://api.example.com/users/${userId}`);
      if (!response.ok) throw new Error('Failed to fetch user');
      const userData = await response.json();
      set({ user: userData, isLoading: false });
    } catch (error) {
      set({ error: error.message, isLoading: false });
    }
  },
}));

// Usage in a component
function UserProfile({ userId }) {
  const { user, isLoading, error, fetchUser } = useUserStore();

  React.useEffect(() => {
    fetchUser(userId);
  }, [userId]);

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  if (!user) return null;

  return (
    <div>
      <h1>{user.name}</h1>
      <p>Email: {user.email}</p>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

In this example:

  1. We define an async fetchUser function directly in the store.
  2. The function manages loading and error states alongside the user data.
  3. We use the set function to update the state at different points in the async operation.
  4. In the component, we can use the store's state and actions as usual, with React hooks handling the component lifecycle.

Benefits of Zustand's Approach to Async Code

  1. Simplicity: No need for additional middleware or complex action creators.
  2. Flexibility: You can structure your async logic however you prefer.
  3. Readability: Async operations are defined close to the relevant state.
  4. Easy Testing: Async functions can be easily unit tested.

Comparison with Other Solutions

Unlike Redux, which often requires middleware like Redux Thunk or Redux Saga for handling async operations, Zustand's approach is much more straightforward. This simplicity can lead to less boilerplate and a gentler learning curve, especially for developers new to state management.

MobX and Recoil also offer ways to handle async operations, but Zustand's approach might be considered more intuitive due to its direct use of async/await syntax without additional abstractions.

In conclusion for async

Zustand's handling of asynchronous code exemplifies its philosophy of simplicity and flexibility. By allowing developers to write async functions directly in the store without special syntax or middleware, Zustand makes it easy to manage complex state operations while keeping the codebase clean and readable.

This approach to async code, combined with Zustand's other features like its small bundle size and easy setup, makes it an excellent choice for projects of all sizes, particularly those that involve significant asynchronous state management.

Hope this "kinda guide" was useful and insightful for any of you that's thinking on how to manage your global application state.
Thank you and Happy coding.

Have a look on my website at https://www.ricardogesteves.com

Follow me @ricardogesteves
X(twitter)

RicardoGEsteves (Ricardo Esteves) · GitHub

Full-Stack Developer | Passionate about creating intuitive and impactful user experiences | Based in Lisbon, Portugal 🇵🇹 - RicardoGEsteves

favicon github.com

Top comments (4)

Collapse
 
rajgohel profile image
Raj Gohel

Awesome 👌

Collapse
 
ricardogesteves profile image
Ricardo Esteves

hi @rajgohel , thank you. I'm glad you like it.

Collapse
 
dgesteves profile image
Diogo Esteves

Nice article!!!

Collapse
 
ricardogesteves profile image
Ricardo Esteves

Thank you a lot @dgesteves