What Is Zustand? The Lightweight State Manager AI Loves
When you ask AI to build a React app with shared data, there's a good chance it reaches for Zustand. Here's why — and what to watch for when it does.
TL;DR
Zustand is a tiny state management library for React. It gives every component in your app access to shared data — like a shopping cart or user login status — without the boilerplate headaches of Redux. AI tools love it because it's simple, fast, and hard to mess up. Think of it as a shared clipboard on a job site: everyone can read it and update it, and everyone sees the same information.
Why AI Coders Need to Know This
Here's a scenario you'll hit fast. You're building a React app with Cursor or Claude. You've got a navbar that shows how many items are in the cart. You've got a product page where people add items. And you've got a checkout page that lists everything. Three different components, one set of data.
Without a state manager, you're stuck passing data up and down through every component in between — like a game of telephone where the message gets corrupted at every handoff. Developers call this "prop drilling," and it's the #1 reason AI-generated React apps start falling apart around the 10-component mark.
State management solves this. It creates a single, shared source of truth that any component can tap into directly. No telephone game. No middlemen.
And when AI builds your state management, it's increasingly choosing Zustand over Redux. Here's why that matters to you:
- You'll see Zustand in AI-generated code constantly. If you don't recognize it, you can't debug it.
- AI gets specific things wrong with Zustand. Knowing what those are saves you hours of frustration.
- Understanding the store pattern makes you better at prompting for any state management task.
- Zustand is genuinely simple. Unlike Redux, you can understand the whole thing in one sitting.
Real Scenario
Your prompt to the AI
"Build me a shopping cart for my React app. I need to add items from a product page, show the cart count in the navbar, and list all items on a checkout page. Use Zustand for state management."
This is a bread-and-butter Zustand use case. You've got data (cart items) that three totally different parts of your app need to access. Without shared state, you'd need to thread that cart data through every single component between your product page and your navbar — even components that don't care about the cart at all.
Think of it this way. On a construction site, you've got a shared clipboard hanging in the job trailer. The framing crew checks it, the electricians check it, the plumbers check it. Everyone reads from the same source, and anyone can update it. Nobody needs to personally hand the clipboard to every other crew. That clipboard is your Zustand store.
What AI Generated
Here's what Cursor or Claude will typically produce when you ask for a Zustand shopping cart. Two files: the store (the shared clipboard) and a component that uses it.
The Zustand Store
// store/cartStore.js
import { create } from 'zustand';
const useCartStore = create((set, get) => ({
// The data (state)
items: [],
// Add an item to the cart
addItem: (product) => set((state) => ({
items: [...state.items, { ...product, quantity: 1 }]
})),
// Remove an item by its ID
removeItem: (productId) => set((state) => ({
items: state.items.filter(item => item.id !== productId)
})),
// Get total number of items
getItemCount: () => {
return get().items.reduce((total, item) => total + item.quantity, 0);
},
// Clear the entire cart
clearCart: () => set({ items: [] })
}))
export default useCartStore;
A React Component Using the Store
// components/Navbar.jsx
import useCartStore from '../store/cartStore';
function Navbar() {
// Grab just what you need from the store
const itemCount = useCartStore((state) => state.items.length);
return (
<nav>
<h1>My Store</h1>
<div className="cart-icon">
🛒 Cart ({itemCount})
</div>
</nav>
);
}
export default Navbar;
// components/ProductCard.jsx
import useCartStore from '../store/cartStore';
function ProductCard({ product }) {
// Grab the addItem function from the store
const addItem = useCartStore((state) => state.addItem);
return (
<div className="product-card">
<h3>{product.name}</h3>
<p>${product.price}</p>
<button onClick={() => addItem(product)}>
Add to Cart
</button>
</div>
);
}
export default ProductCard;
Notice something? There's no <Provider> wrapper. No dispatch. No action types. No reducer files. The ProductCard component just imports the store and calls addItem. The Navbar imports the same store and reads the item count. They don't know about each other. They just both look at the same clipboard.
Understanding Each Part
The create function — building the clipboard
create is the only function you need from Zustand. You give it a function that returns an object, and that object becomes your store. The object has two kinds of things inside it:
- Data (like
items: []) — the actual information your app shares. - Actions (like
addItem,removeItem) — functions that change the data.
That's really it. Your store is just data + the functions that update that data, bundled together in one place.
The set function — updating the clipboard
Every action inside your store uses set to update state. You call set with the new values, and Zustand handles the rest — it updates the store and tells every component that's watching to re-render with the new data.
// Simple: just replace a value
clearCart: () => set({ items: [] })
// With a function: when new state depends on old state
addItem: (product) => set((state) => ({
items: [...state.items, product]
}))
The function version is important. When your new state depends on the current state (like adding to a list), always use the function form. If you just write set({ items: [...items, product] }) without the function, you might be reading a stale version of items. More on this in the "What AI Gets Wrong" section.
The get function — reading from inside the store
Sometimes an action inside the store needs to read the current state. That's what get is for. In the shopping cart example, getItemCount uses get() to access the current items array and add up all the quantities.
The useCartStore hook — components reading the clipboard
When a component needs data from the store, it calls the hook with a selector — a tiny function that picks out exactly the piece of data it needs:
// Only re-renders when items.length changes
const itemCount = useCartStore((state) => state.items.length);
// Only re-renders when the addItem function changes (it won't)
const addItem = useCartStore((state) => state.addItem);
This is one of Zustand's superpowers. Each component only subscribes to the specific data it uses. If the cart items change but a component only uses the user's name from the store, that component won't re-render. On a construction site, the electrician only needs to check the electrical section of the clipboard — they don't need to read the plumbing notes every time those get updated.
Why there's no Provider
Redux and React Context both require you to wrap your app in a <Provider> component. Forget it and nothing works. Zustand skips this entirely. The store lives outside of React's component tree. Any component anywhere in your app can import it and use it. This is a huge reason AI prefers Zustand — one less thing to wire up means one less thing to get wrong.
What AI Gets Wrong About This
Zustand is simple, but AI still trips up in predictable ways. Here are the three problems you'll see most often.
1. Stale closures inside actions
AI sometimes writes store actions that reference variables from the outer scope instead of using set's function form or get(). This creates a stale closure — the action captures a snapshot of the data at creation time and never sees updates.
// ❌ Bug: AI uses items directly (stale closure)
addItem: (product) => set({
items: [...items, product] // 'items' is stale!
})
// ✅ Fix: use the function form to get current state
addItem: (product) => set((state) => ({
items: [...state.items, product] // always current
}))
Prompt to fix: "Use the function form of set() in all Zustand actions so they always read current state, not stale closures."
2. Missing persistence — state disappears on refresh
By default, Zustand stores live in memory only. Refresh the page and everything is gone. AI almost never adds persistence unless you specifically ask for it. Your user adds six items to their cart, refreshes the page, and the cart is empty.
// ❌ No persistence — data lost on refresh
const useCartStore = create((set) => ({
items: [],
// ...
}))
// ✅ With persist middleware — survives page refresh
import { persist } from 'zustand/middleware';
const useCartStore = create(
persist(
(set) => ({
items: [],
// ...
}),
{ name: 'cart-storage' } // saved to localStorage
)
)
Prompt to fix: "Add Zustand's persist middleware to the cart store so items survive page refresh. Use localStorage."
3. Over-storing — dumping everything into one store
AI tends to create one massive store and throw every piece of state into it: cart items, user auth, UI toggles, form data, theme preferences, API loading states. This creates performance problems because changing anything potentially re-renders components that subscribe to everything.
The fix is simple: use multiple stores. Cart data in one store. Auth in another. UI state in a third. Each component subscribes only to what it needs.
Prompt to fix: "Split this into separate Zustand stores — one for cart, one for auth, one for UI state. Each store in its own file."
4. Selecting the entire store instead of specific slices
AI often writes const store = useCartStore() — grabbing the entire store object. This means the component re-renders whenever anything in the store changes, even data it doesn't use. Always use a selector to pick out only what you need.
// ❌ Re-renders on every store change
const store = useCartStore();
const items = store.items;
// ✅ Only re-renders when items changes
const items = useCartStore((state) => state.items);
Prompt to fix: "Use selectors in every useCartStore call so components only re-render when their specific data changes."
How to Debug with AI
Here are the two most common Zustand bugs and exactly how to describe them to your AI so it can actually help.
Problem: State updates but component doesn't re-render
You click "Add to Cart" and the store updates (you can verify with console.log inside the action), but the Navbar still shows "Cart (0)".
What's happening: The component is probably selecting the whole store or comparing objects by reference. Zustand uses strict equality (===) to decide whether to re-render. If you return a new object from your selector every time, it always looks different and re-renders constantly. If you return the same reference, it never re-renders.
Debugging prompt: "My Navbar component reads from the Zustand cart store but doesn't update when I add items. The addItem action logs correctly. I think the selector might be returning a new reference every render. Can you check my selector and fix the re-render issue?"
Problem: State resets on page navigation
You add items on the product page, navigate to the checkout page, and the cart is empty.
What's happening: If you're using a framework that does full page reloads between routes (or if you accidentally created the store inside a component instead of at the module level), the store gets recreated on every navigation.
Debugging prompt: "My Zustand cart store resets when I navigate between pages. The store file exports a create() call at the top level. I'm using Next.js with the app router. Is the store being recreated on navigation? Should I add persist middleware?"
Problem: Two components show different values from the same store
What's happening: You might accidentally have two separate store files, or you're importing from different paths (e.g., '../store/cartStore' vs '../../store/cartStore' that resolve to different modules).
Debugging prompt: "My Navbar and Checkout components both import useCartStore but seem to have different data. Could there be duplicate store instances? Check all import paths point to the same file."
Quick debug toolkit
Drop this into your browser console to inspect any Zustand store at any time:
// Add this to your store file temporarily for debugging
const useCartStore = create((set, get) => {
// Log every state change
const loggedSet = (...args) => {
set(...args);
console.log('🔍 Cart state:', get());
};
return {
items: [],
addItem: (product) => loggedSet((state) => ({
items: [...state.items, product]
})),
// ... rest of actions using loggedSet
};
});
Pro tip: Zustand also has official devtools middleware that connects to the Redux DevTools browser extension. Ask your AI: "Add Zustand devtools middleware to my store so I can inspect state changes in Redux DevTools."
What to Learn Next
Zustand is one piece of the React state puzzle. These articles fill in the surrounding picture:
- What Is State Management? — The big-picture concept behind Zustand, Redux, and Context. Start here if "state" still feels fuzzy.
- What Is React? — Zustand is built for React. Understanding components, props, and re-renders makes everything about Zustand click.
- What Are Closures? — The stale closure bug is Zustand's most common gotcha. This article explains what closures are and why they go stale.
- What Is TanStack Query? — Zustand handles client state (UI toggles, form data). TanStack Query handles server state (API data). Together, they cover everything your React app needs.
- How to Debug AI-Generated Code — A general framework for diagnosing problems in code your AI wrote, including state management bugs.
- What Is JavaScript? — The language Zustand and React are built on. If you're still getting comfortable with JavaScript basics, this is your foundation.
Key Insight
Zustand isn't doing anything magical. It's just a shared object that tells React components "hey, the data changed, update yourself." The reason AI loves it — and the reason you should too — is that it does this with almost no ceremony. No wrapping, no dispatching, no boilerplate. Just data and functions in a store.
FAQ
Zustand is a small, fast state management library for React. It lets you create a shared store that any component can read from or write to — without wrapping your app in provider components or writing boilerplate code. Think of it as a shared clipboard that every part of your app can access.
AI tools like Cursor and Claude prefer Zustand because it requires dramatically less code. A Redux store needs action types, action creators, reducers, and a Provider wrapper. Zustand does the same job in about 10 lines. Less code means fewer places for AI to make mistakes, and fewer files for you to debug.
Yes. Zustand is one of the most beginner-friendly state management libraries available. You create a store with create(), put your data and functions inside it, and use it in any component with a hook. There are no new patterns to learn beyond basic React. If you can write a JavaScript object and call a function, you can use Zustand.
Not entirely. useState is still perfect for state that lives inside a single component — like whether a dropdown menu is open or what text is in an input field. Zustand is for state that needs to be shared across multiple components, like a shopping cart, user login status, or theme preferences. Use both: useState for local, Zustand for shared.
Yes, but not by default. Zustand includes a persist middleware that automatically saves your store data to localStorage. AI sometimes forgets to add this, which is why your cart might empty on page refresh. Ask your AI: "Add Zustand's persist middleware to the store so state survives page refresh."