React  

How to Manage Complex Global State in React Using Zustand or Jotai?

Introduction

Managing global state in React can become challenging as your application grows. While React’s built-in state (useState, useReducer, and context) works well for simple cases, it often becomes difficult to handle complex and deeply shared state across multiple components.
To solve this, lightweight state management libraries like Zustand and Jotai offer simple, scalable, and high-performance ways to manage global state with minimal code.

In this article, you’ll learn how Zustand and Jotai work, how to use them in real-world React applications, and how to decide which one fits your needs.

Why Not Just Use React Context?

React Context is useful but has limitations:

  • Causes unnecessary re-renders when state changes

  • Not ideal for large or deeply nested state

  • Harder to scale in big apps

Zustand and Jotai solve these issues with:

  • Better performance

  • Cleaner global state logic

  • Minimal boilerplate

What Is Zustand?

Zustand is a small, fast, and scalable state management library.

Key Features

  • Extremely simple API

  • No reducers or actions required

  • Global store with minimal code

  • Selectors to prevent unnecessary re-renders

  • Great for complex apps

Install Zustand

npm install zustand

Creating a Store in Zustand

Example Store

import { create } from 'zustand';

const useUserStore = create((set) => ({
  user: null,
  setUser: (data) => set({ user: data }),
  clearUser: () => set({ user: null })
}));

Using the Store in a Component

function Profile() {
  const user = useUserStore((state) => state.user);
  const setUser = useUserStore((state) => state.setUser);

  return (
    <div>
      <p>User: {user?.name ?? 'No user'}</p>
      <button onClick={() => setUser({ name: 'Alex' })}>Login</button>
    </div>
  );
}

Why Zustand Works Well

  • Only subscribed components re-render

  • Clean and intuitive store structure

Handling Complex or Nested State with Zustand

Zustand makes complex state easy.

Example

const useCartStore = create((set) => ({
  cart: [],
  addItem: (item) => set((state) => ({ cart: [...state.cart, item] })),
  removeItem: (id) => set((state) => ({ cart: state.cart.filter((i) => i.id !== id) }))
}));

Advantages

  • Immutable updates handled manually but cleanly

  • Supports async logic directly (no thunks needed)

Async State Logic in Zustand

Example Fetching Data

const useProductStore = create((set) => ({
  products: [],
  fetchProducts: async () => {
    const res = await fetch('/api/products');
    const data = await res.json();
    set({ products: data });
  }
}));

Why It’s Powerful

  • No extra middleware required

  • Async functions live directly inside the store

What Is Jotai?

Jotai is a minimalistic state management library based on atoms.

Key Features

  • Simple and flexible

  • Each piece of state is an atom

  • Fine-grained updates (only components using an atom re-render)

  • Great for shared UI state, forms, dynamic UIs

Install Jotai

npm install jotai

Creating Atoms in Jotai

Example Atom

import { atom, useAtom } from 'jotai';

const countAtom = atom(0);

Using an Atom

function Counter() {
  const [count, setCount] = useAtom(countAtom);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount((c) => c + 1)}>Increment</button>
    </div>
  );
}

Why Jotai Works Well

  • Atoms behave like useState, but globally

  • Zero boilerplate

Derived Atoms (Computed State in Jotai)

Jotai allows derived state based on other atoms.

Example

const priceAtom = atom(100);
const taxAtom = atom(10);

const totalAtom = atom((get) => get(priceAtom) + get(taxAtom));

Benefits

  • No reducers or complex selectors

  • Automatically updates when dependencies change

Async Atoms in Jotai

Jotai supports async atoms easily.

Example

const userAtom = atom(async () => {
  const res = await fetch('/api/user');
  return await res.json();
});

Why It’s Useful

  • Async logic integrated directly into state

  • No need for extra middleware

Zustand vs Jotai — Which Should You Use?

FeatureZustandJotai
StyleSingle global storeMany small atoms
Best forComplex and large appsUI state, small to medium apps
BoilerplateMinimalVery minimal
Async logicBuilt-inBuilt-in
PerformanceExcellentExcellent
Learning curveVery easyVery easy

Simple Recommendation

  • Use Zustand for complex logic/state-heavy apps (cart, dashboards, multi-page apps).

  • Use Jotai for flexible UI state, forms, filters, and small atomic states.

Best Practices for Managing Global State

  • Keep global state minimal

  • Use Zustand/Jotai only where necessary

  • Avoid deeply nested objects when possible

  • Split stores/atoms logically

  • Use selectors in Zustand to prevent re-renders

  • Use derived atoms in Jotai for computed state

Advanced Patterns in Zustand

Zustand supports powerful patterns that help manage complex application logic at scale.

1. Middleware: Persisting State to LocalStorage

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

2. Zustand + DevTools Integration

import { devtools } from 'zustand/middleware';

const useStore = create(devtools((set) => ({
  count: 0,
  increment: () => set((s) => ({ count: s.count + 1 }))
})));

3. Zustand Slices for Large Apps

const createUserSlice = (set) => ({
  user: null,
  login: (u) => set({ user: u })
});

const createCartSlice = (set) => ({
  cart: [],
  addToCart: (item) => set((s) => ({ cart: [...s.cart, item] }))
});

const useStore = create((set) => ({
  ...createUserSlice(set),
  ...createCartSlice(set)
}));

Advanced Patterns in Jotai

1. Atom Families (Dynamic Atoms)

Used for dynamic lists like form fields.

import { atomFamily } from 'jotai/utils';

const fieldAtom = atomFamily((id) => atom(`Field-${id}`));

2. Writable Derived Atoms

const firstNameAtom = atom('John');
const lastNameAtom = atom('Doe');

const fullNameAtom = atom(
  (get) => `${get(firstNameAtom)} ${get(lastNameAtom)}`,
  (get, set, value) => {
    const [first, last] = value.split(' ');
    set(firstNameAtom, first);
    set(lastNameAtom, last);
  }
);

3. Splitting Global State Into Logical Atoms

Avoid large objects; break state into atomic units.

Real-World Examples

1. Zustand Cart System

const useCartStore = create((set) => ({
  cart: [],
  add: (item) => set((s) => ({ cart: [...s.cart, item] })),
  remove: (id) => set((s) => ({ cart: s.cart.filter((i) => i.id !== id) })),
  total: () => useCartStore.getState().cart.reduce((t, i) => t + i.price, 0)
}));

2. Jotai Theme Toggle

const themeAtom = atom('light');
const toggleThemeAtom = atom(
  (get) => get(themeAtom),
  (get, set) => set(themeAtom, get(themeAtom) === 'light' ? 'dark' : 'light')
);

3. Jotai Filter State for a Product List

const searchAtom = atom('');
const categoryAtom = atom('all');
const productsAtom = atom(async () => await fetch('/api/products').then(r => r.json()));

const filteredProductsAtom = atom((get) => {
  const search = get(searchAtom).toLowerCase();
  const category = get(categoryAtom);
  return get(productsAtom).filter((p) =>
    p.name.toLowerCase().includes(search) &&
    (category === 'all' || p.category === category)
  );
});

Comparison Chart: Zustand vs Jotai vs Redux Toolkit vs Recoil

FeatureZustandJotaiRedux ToolkitRecoil
BoilerplateVery LowVery LowHighMedium
Learning CurveEasyEasyMediumMedium
Best Use CaseLarge apps, complex logicUI state, atomic stateEnterprise-scale architectureApp-wide state with relationships
PerformanceExcellentExcellentGoodVery Good
Async SupportBuilt-inBuilt-inNeeds Thunks/SagasBuilt-in
DevToolsYesYesYesYes
State ModelSingle StoreAtomsReducers/ActionsAtoms/Selectors

Summary

  • Choose Zustand for large, logic-heavy apps.

  • Choose Jotai for highly dynamic UI state.

  • Choose Redux Toolkit for enterprise-level structure + strict patterns.

  • Choose Recoil for dependency-based state graph needs.

Conclusion

Managing global state in React becomes much easier with Zustand and Jotai. Both libraries are lightweight, fast, and developer-friendly, offering clean APIs for sharing and updating state across components. Zustand is ideal for larger, more complex apps, while Jotai is perfect for atomic UI state and modular architecture. With the right choice and good practices, you can maintain clean, scalable, and high-performance React applications.