Modern React applications often need to share state across many components. At first, local state with useState() may be enough. However, as an application grows, passing data through many component levels can become difficult to manage.
This is where Zustand becomes useful.
Zustand is a lightweight state-management library for React and JavaScript applications. It provides a simple way to create a central store, read state from any component, and update that state without writing large amounts of boilerplate.
Unlike more structured state-management libraries, Zustand focuses on simplicity. You usually create a store with a small amount of code, then access only the values your component needs.
In this complete beginner’s guide, you will learn what Zustand is, how it works, how to install it, how to create a store, how to update state, how selectors work, how Zustand compares with Redux and Context API, when you should use it, and what beginners should avoid.
What Is Zustand?
Zustand is a small state-management library commonly used in React applications to manage shared client-side state.
The word “Zustand” means “state” in German.
A Zustand store can contain:
- values
- objects
- arrays
- actions
- computed logic
- async functions
For example, a simple counter store can look like this:
import { create } from "zustand";
const useCounterStore = create((set) => ({
count: 0,
increase: () =>
set((state) => ({
count: state.count + 1,
})),
decrease: () =>
set((state) => ({
count: state.count - 1,
})),
}));
A React component can then use that store:
function Counter() {
const count = useCounterStore((state) => state.count);
const increase = useCounterStore((state) => state.increase);
return (
<div>
<p>Count: {count}</p>
<button onClick={increase}>
Increase
</button>
</div>
);
}
This example already shows one of Zustand’s biggest strengths: shared state with very little setup.
Why Do We Need Zustand?
React already provides state-management tools such as:
useState
useReducer
Context API
So why use Zustand?
The main reason is that shared state can become difficult when many components need the same data.
Suppose an application contains:
Navbar
Sidebar
Product Page
Cart
Checkout
Profile
Settings
Several parts of the application may need access to:
- shopping cart
- logged-in user state
- theme
- filters
- sidebar status
- selected language
- notifications
- app preferences
You could pass these values through props.
However, this can create prop drilling.
For example:
App
↓
Layout
↓
Dashboard
↓
Sidebar
↓
Menu
↓
Button
If the final button needs a value stored in App, that value may need to pass through every component in between.
Zustand avoids this problem by letting components access the store directly.
What Is Global State?
Global state is information that multiple parts of an application need to access.
For example:
Dark mode
Shopping cart
User preferences
Authentication state
Sidebar visibility
Selected language
Global filters
Instead of storing these values separately in many components, you can keep them in a central store.
Then any component can read or update them.
This is the main use case for Zustand.
How Does Zustand Work?
Zustand uses a simple store-based model.
The basic flow looks like this:
Component
↓
Zustand Store
↓
Read State
↓
Update State
↓
Subscribed Components Update
Unlike some state libraries, Zustand does not require reducers, actions, or providers for normal usage.
That makes its mental model relatively simple.
How to Install Zustand
In a React project, install Zustand with:
npm install zustand
You can also use:
yarn add zustand
or:
pnpm add zustand
After installation, import create:
import { create } from "zustand";
Then you can create your first store.
Creating Your First Zustand Store
Let’s create a simple counter.
import { create } from "zustand";
const useCounterStore = create((set) => ({
count: 0,
increase: () =>
set((state) => ({
count: state.count + 1,
})),
decrease: () =>
set((state) => ({
count: state.count - 1,
})),
reset: () =>
set({
count: 0,
}),
}));
export default useCounterStore;
This store contains:
count
increase
decrease
reset
The set() function updates the store.
Using a Zustand Store in React
Now use the store inside a component.
import useCounterStore from "./store";
function Counter() {
const count = useCounterStore((state) => state.count);
const increase = useCounterStore((state) => state.increase);
const decrease = useCounterStore((state) => state.decrease);
const reset = useCounterStore((state) => state.reset);
return (
<div>
<h2>{count}</h2>
<button onClick={increase}>
Increase
</button>
<button onClick={decrease}>
Decrease
</button>
<button onClick={reset}>
Reset
</button>
</div>
);
}
The component subscribes only to the values it requests.
When count changes, the component updates.
What Is the create() Function?
The create() function is used to create a Zustand store.
Example:
const useStore = create((set) => ({
name: "Alex",
age: 25,
}));
The callback receives functions such as:
set
get
The set function changes state.
The get function can read the current state inside store logic.
How Does set() Work?
The set() function updates the store.
For example:
const useStore = create((set) => ({
name: "Alex",
changeName: () =>
set({
name: "John",
}),
}));
You can also calculate the next state from the current state:
increase: () =>
set((state) => ({
count: state.count + 1,
}))
This pattern is useful when the new value depends on the previous value.
Using get() in Zustand
Sometimes an action needs to read another value from the same store.
For this, you can use get().
Example:
const useStore = create((set, get) => ({
price: 100,
quantity: 2,
getTotal: () => {
return get().price * get().quantity;
},
}));
Now:
getTotal()
returns:
200
This is useful for internal store logic.
Zustand Selectors Explained
Selectors allow a component to subscribe only to part of the store.
Example:
const count = useCounterStore(
(state) => state.count
);
Instead of reading the entire store, the component reads only count.
This is important because it can reduce unnecessary re-renders.
For example:
const useStore = create((set) => ({
count: 0,
name: "Alex",
theme: "dark",
}));
A component that only needs count should use:
const count = useStore((state) => state.count);
Another component can read only theme:
const theme = useStore((state) => state.theme);
As a result, components can subscribe more precisely.
Why Are Selectors Important?
Consider a large store:
user
cart
theme
notifications
sidebar
filters
settings
If every component subscribed to the whole store, many components could update unnecessarily.
Selectors allow more focused subscriptions.
For example:
const cart = useStore((state) => state.cart);
or:
const sidebarOpen = useStore(
(state) => state.sidebarOpen
);
This is one of the most important Zustand habits to learn.
Zustand with Objects
Zustand can store objects easily.
Example:
const useUserStore = create((set) => ({
user: {
name: "Alex",
email: "alex@example.com",
age: 25,
},
updateName: (name) =>
set((state) => ({
user: {
...state.user,
name,
},
})),
}));
Then use it like this:
const user = useUserStore((state) => state.user);
You can also select just one property:
const name = useUserStore(
(state) => state.user.name
);
Zustand with Arrays
You can also manage arrays.
For example, a todo store:
const useTodoStore = create((set) => ({
todos: [],
addTodo: (todo) =>
set((state) => ({
todos: [...state.todos, todo],
})),
removeTodo: (id) =>
set((state) => ({
todos: state.todos.filter(
(todo) => todo.id !== id
),
})),
}));
This pattern works well for:
- carts
- todos
- local notifications
- selected items
- drafts
- favorites
Zustand Shopping Cart Example
A shopping cart is a common Zustand use case.
import { create } from "zustand";
const useCartStore = create((set) => ({
items: [],
addItem: (product) =>
set((state) => ({
items: [...state.items, product],
})),
removeItem: (productId) =>
set((state) => ({
items: state.items.filter(
(item) => item.id !== productId
),
})),
clearCart: () =>
set({
items: [],
}),
}));
Now a cart button can read the item count:
function CartButton() {
const items = useCartStore((state) => state.items);
return (
<button>
Cart ({items.length})
</button>
);
}
Meanwhile, a product card can call:
const addItem = useCartStore(
(state) => state.addItem
);
Both components share the same store.
Zustand Theme Store Example
Zustand also works well for theme management.
const useThemeStore = create((set) => ({
theme: "light",
toggleTheme: () =>
set((state) => ({
theme:
state.theme === "light"
? "dark"
: "light",
})),
}));
Then:
function ThemeButton() {
const theme = useThemeStore((state) => state.theme);
const toggleTheme = useThemeStore(
(state) => state.toggleTheme
);
return (
<button onClick={toggleTheme}>
Current Theme: {theme}
</button>
);
}
Async Actions in Zustand
Zustand actions can also be asynchronous.
For example:
const useUserStore = create((set) => ({
users: [],
loading: false,
error: null,
fetchUsers: async () => {
set({
loading: true,
error: null,
});
try {
const response = await fetch("/api/users");
if (!response.ok) {
throw new Error("Failed to fetch users");
}
const users = await response.json();
set({
users,
loading: false,
});
} catch (error) {
set({
error: error.message,
loading: false,
});
}
},
}));
A component can then call:
const fetchUsers = useUserStore(
(state) => state.fetchUsers
);
However, this leads to an important question:
Should Zustand manage server data?
Zustand for Client State vs Server State
Zustand can technically store API data.
However, it is mainly useful for client state.
Examples include:
Theme
Cart
Sidebar
Local filters
App preferences
Temporary forms
UI state
For advanced server-state requirements, a specialized tool such as TanStack Query is often better.
TanStack Query provides features such as:
- caching
- background refetching
- retries
- stale data handling
- invalidation
- pagination
- server synchronization
Therefore, a common architecture is:
Zustand
→ Client State
TanStack Query
→ Server State
Zustand vs TanStack Query
These two tools solve different problems.
| Feature | Zustand | TanStack Query |
|---|---|---|
| Main Purpose | Client state | Server state |
| Global UI State | Excellent | Not primary purpose |
| API Caching | Manual | Built in |
| Background Refetching | Manual | Built in |
| Query Invalidation | Manual | Built in |
| Shopping Cart | Excellent | Usually not primary use |
| Theme State | Excellent | Not ideal |
| API Data | Possible | Excellent |
| Simple Store | Yes | Query cache model |
| Async Requests | Possible | Core feature |
You can use both together.
Zustand vs Redux
Zustand is frequently compared with Redux because both can manage global client state.
However, they differ in complexity and structure.
| Feature | Zustand | Redux Toolkit |
|---|---|---|
| Setup | Very small | More structured |
| Boilerplate | Low | Moderate |
| Central Store | Yes | Yes |
| Reducers Required | No | Yes, through slices |
| Actions Required | Can be simple functions | Structured actions |
| Provider Required | Usually no | Yes |
| Devtools | Supported | Excellent |
| Middleware | Supported | Strong ecosystem |
| Learning Curve | Lower | Higher |
| Large Complex Workflows | Good | Excellent |
| Small to Medium Apps | Excellent | Good |
Zustand is often attractive when you want simple global state without much ceremony.
Redux can be better when a large application benefits from stricter conventions and highly structured state changes.
Simple Zustand vs Redux Example
Suppose you want to manage:
count = 0
Zustand
const useStore = create((set) => ({
count: 0,
increase: () =>
set((state) => ({
count: state.count + 1,
})),
}));
Redux Toolkit
You would typically create:
Store
Slice
Reducer
Action
Provider
Selector
Redux Toolkit makes this easier than old Redux, but Zustand still requires less setup for small use cases.
Zustand vs React Context API
React Context can share values across components.
Example:
Theme
Language
Authentication
However, Context and Zustand have different strengths.
| Feature | Zustand | Context API |
|---|---|---|
| External Library | Yes | No |
| Setup | Low | Low |
| Provider Required | Usually no | Yes |
| Selective Subscriptions | Strong | More limited |
| Frequent Updates | Better suited | Can become inefficient |
| Large Shared State | Good | Can become harder |
| Simple Static Values | Good | Excellent |
Context is great for values that do not change often.
For example:
Theme
Language
Configuration
Zustand can be more convenient when shared state changes frequently or becomes more complex.
Zustand vs useState
useState() is local to a component.
Example:
const [count, setCount] = useState(0);
This is perfect when only one component or a small component tree needs the value.
Zustand becomes useful when many unrelated components need the same state.
A useful rule is:
Local State
→ useState
Shared Global Client State
→ Zustand
Do not move every state variable into Zustand automatically.
Zustand vs useReducer
useReducer() is useful for complex local state transitions.
Example:
Form workflow
Local editor state
Complex component logic
Zustand can provide a similar action-based structure across the entire application.
Therefore:
Complex local state
→ useReducer
Complex shared state
→ Zustand
This is not a strict rule, but it is a useful guideline.
Does Zustand Require a Provider?
For normal client-side usage, Zustand stores generally do not require wrapping your application in a provider.
For example:
function App() {
return <Dashboard />;
}
A nested component can simply import the store:
import useStore from "./store";
This is one reason Zustand can feel simpler than Context or Redux for many projects.
Can You Use Multiple Zustand Stores?
Yes.
In fact, splitting state into multiple stores can make applications easier to maintain.
For example:
useAuthStore
useCartStore
useThemeStore
useSettingsStore
useFilterStore
Instead of creating one huge store:
useEverythingStore
you can organize state by domain.
This often makes code easier to understand.
One Big Store vs Multiple Stores
For a small app, one store may be enough.
For example:
const useAppStore = create(() => ({
theme: "light",
sidebarOpen: false,
}));
However, larger applications may benefit from separate stores.
For example:
Auth Store
Cart Store
UI Store
Settings Store
The best approach depends on project complexity.
Zustand Actions
Actions are simply functions inside your store.
For example:
const useStore = create((set) => ({
count: 0,
increment: () =>
set((state) => ({
count: state.count + 1,
})),
}));
Unlike Redux, Zustand does not force you to separate actions and reducers.
This makes small stores easier to write.
Resetting Zustand State
A reset action is useful for logout or form cleanup.
Example:
const initialState = {
name: "",
email: "",
};
const useUserStore = create((set) => ({
...initialState,
setName: (name) => set({ name }),
reset: () => set(initialState),
}));
Then:
reset();
restores the initial values.
Persisting Zustand State
Sometimes you want state to survive a browser refresh.
For example:
Theme
Cart
Language
User preferences
Zustand provides middleware that can persist selected state.
A typical example looks like:
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",
}
)
);
The state can then be stored in browser storage.
This is useful for preferences that should remain after refresh.
Zustand Devtools
Zustand can also integrate with Redux DevTools through middleware.
This can help you inspect:
- state updates
- actions
- current store state
- debugging history
For larger stores, development tools can make debugging much easier.
Middleware in Zustand
Middleware can add behavior to stores.
Common examples include:
persist
devtools
subscribeWithSelector
immer
Middleware can help with:
- persistence
- debugging
- subscriptions
- immutable update patterns
Beginners do not need to learn all middleware immediately.
Start with basic stores first.
Using Zustand with TypeScript
Zustand works well with TypeScript.
For example:
import { create } from "zustand";
type CounterStore = {
count: number;
increase: () => void;
};
const useCounterStore = create<CounterStore>((set) => ({
count: 0,
increase: () =>
set((state) => ({
count: state.count + 1,
})),
}));
TypeScript provides stronger type safety for:
- state
- actions
- parameters
- returned values
This can be especially useful in larger applications.
Example: Authentication Store
A simple authentication store could look like:
const useAuthStore = create((set) => ({
user: null,
isAuthenticated: false,
login: (user) =>
set({
user,
isAuthenticated: true,
}),
logout: () =>
set({
user: null,
isAuthenticated: false,
}),
}));
Then:
const isAuthenticated = useAuthStore(
(state) => state.isAuthenticated
);
However, sensitive authentication data should still be handled carefully. A global store should not be treated as secure storage for secrets.
Example: Sidebar Store
For UI state:
const useUIStore = create((set) => ({
sidebarOpen: false,
openSidebar: () =>
set({
sidebarOpen: true,
}),
closeSidebar: () =>
set({
sidebarOpen: false,
}),
toggleSidebar: () =>
set((state) => ({
sidebarOpen: !state.sidebarOpen,
})),
}));
This is a perfect example of shared client state.
Example: Filter Store
An e-commerce filter store might contain:
const useFilterStore = create((set) => ({
category: "all",
minPrice: 0,
maxPrice: 1000,
setCategory: (category) =>
set({ category }),
setPriceRange: (minPrice, maxPrice) =>
set({
minPrice,
maxPrice,
}),
}));
Then different components can use the same filters.
For example:
Filter Sidebar
Product Header
Mobile Filters
Active Filter Chips
All can share one Zustand store.
Advantages of Zustand
Zustand offers several benefits.
1. Very Little Boilerplate
A store can be created with only a few lines of code.
2. Easy to Learn
Developers who understand React state can usually understand basic Zustand quickly.
3. No Provider for Normal Usage
You can import a store directly wherever it is needed.
4. Selective Subscriptions
Components can subscribe only to specific values.
5. Flexible Store Design
You can store values and actions together.
6. Async Logic Is Simple
Async functions can live directly inside the store.
7. TypeScript Support
Zustand works well in TypeScript projects.
8. Useful Middleware
Persistence and DevTools support are available.
9. Small Mental Model
You mainly need to understand:
create
state
set
get
selectors
actions
This makes Zustand approachable for beginners.
Disadvantages of Zustand
Zustand also has limitations.
Less Structure Than Redux
The flexibility can become a disadvantage in large teams if developers create stores inconsistently.
Easy to Create Huge Stores
Beginners may place every application value into one store.
This can make maintenance difficult.
Not a Server-State Specialist
Although it can fetch API data, it does not automatically provide the same caching and synchronization features as TanStack Query.
Global State Can Be Overused
Not every state needs to be global.
For example, a simple modal used by only one component may be better managed locally.
Architecture Requires Discipline
Large projects still need clear conventions for:
- store organization
- naming
- actions
- persistence
- selectors
When Should You Use Zustand?
Zustand is a good choice when multiple parts of your frontend need shared client state.
Common use cases include:
- shopping carts
- theme management
- authentication UI state
- application settings
- global filters
- dashboards
- sidebar state
- multi-step workflows
- media player state
- editors
- local notifications
- user preferences
It is especially attractive for small and medium React applications that need more than useState() but do not need a highly structured Redux architecture.
When Should You Avoid Zustand?
You may not need Zustand when:
- the state belongs to one component
- Context is enough for a simple shared value
- your application has almost no global state
- most of your data is server data that TanStack Query can manage
- adding another dependency provides no real benefit
For example:
const [modalOpen, setModalOpen] = useState(false);
is often better than creating a global store if only one component needs the modal state.
Should You Store API Data in Zustand?
You can.
However, ask whether the data is really client state or server state.
Suppose you fetch:
Products
Users
Orders
Comments
If you need advanced caching, retries, invalidation, stale-data handling, and background synchronization, TanStack Query is usually a better fit.
A practical architecture is:
Zustand
├── Theme
├── Cart
├── Sidebar
├── Local filters
└── Client preferences
TanStack Query
├── Products
├── Users
├── Orders
└── Notifications
This keeps responsibilities clear.
Zustand Project Structure
A small project might use:
src/
├── stores/
│ ├── useAuthStore.js
│ ├── useCartStore.js
│ └── useThemeStore.js
│
├── components/
├── pages/
└── App.jsx
For larger projects, you may organize stores by feature.
For example:
src/
├── features/
│ ├── cart/
│ │ ├── cartStore.js
│ │ └── Cart.jsx
│ │
│ ├── auth/
│ │ ├── authStore.js
│ │ └── Login.jsx
│ │
│ └── settings/
The exact structure is less important than consistency.
Common Zustand Beginner Mistakes
1. Storing Everything Globally
Do not place every input, button, and modal into Zustand.
Keep truly local state local.
2. Reading the Entire Store
Avoid:
const state = useStore();
when you only need one value.
Prefer:
const count = useStore(
(state) => state.count
);
This creates a more focused subscription.
3. Creating One Massive Store
A store containing:
auth
cart
theme
notifications
editor
filters
settings
payments
can become difficult to maintain.
Split stores when it improves clarity.
4. Using Zustand as an API Cache
You can manually create an API cache in Zustand.
However, if your main problem is server-state synchronization, use a tool designed for that purpose.
5. Persisting Too Much Data
Not every store should be stored in localStorage.
Persist only what should survive a refresh.
For example:
Theme
Language
Cart
User preferences
Temporary UI state usually does not need persistence.
Zustand Learning Roadmap
A beginner can follow this order.
Step 1: Learn React State
Understand:
useState
props
component state
Step 2: Understand Prop Drilling
Learn why deeply passing shared state can become inconvenient.
Step 3: Learn create()
Create a simple counter store.
Step 4: Learn set()
Practice updating primitive values, arrays, and objects.
Step 5: Learn Selectors
Subscribe only to required state.
Step 6: Add Actions
Create functions such as:
addItem
removeItem
toggleTheme
logout
Step 7: Learn Multiple Stores
Organize state by application domain.
Step 8: Learn Persistence
Save selected preferences between browser sessions.
Step 9: Learn TypeScript
Type your state and actions.
Step 10: Combine with Server-State Tools
Learn how Zustand and TanStack Query can work together.
Zustand Project Ideas for Beginners
Practice with projects such as:
- Counter app
- Todo app
- Shopping cart
- Theme switcher
- Notes app
- Expense tracker
- Music player
- Admin dashboard
- E-commerce filters
- Multi-step checkout
- Kanban board
- Task manager
A shopping cart is particularly useful because it teaches arrays, actions, derived information, and persistence.
Is Zustand Good for Large Applications?
Yes, Zustand can be used in large applications.
However, large applications need good organization.
Instead of one giant store, consider:
useAuthStore
useCartStore
useUIStore
useEditorStore
You should also establish conventions for:
- naming
- actions
- selectors
- persistence
- async logic
- store boundaries
Zustand provides flexibility, but teams still need discipline.
Is Zustand Better Than Redux?
Not universally.
Zustand is often simpler.
Redux is often more structured.
Use Zustand when:
- you want less boilerplate
- your client state is relatively straightforward
- you prefer a lightweight store
- your team values flexibility
Use Redux Toolkit when:
- the application has complex client workflows
- strict conventions are useful
- the team already uses Redux
- advanced Redux ecosystem tools are important
The right choice depends on the project.
Is Zustand Better Than Context API?
For simple values, Context may be enough.
For example:
Theme
Language
Configuration
However, Zustand can be easier when:
- many components need shared state
- updates are frequent
- selective subscriptions matter
- the store contains several actions
- state becomes more complex
Therefore, Context and Zustand serve overlapping but different use cases.
Is Zustand Worth Learning?
Yes, especially for React developers.
It gives you a useful middle ground between:
useState / Context
and:
Redux Toolkit
Zustand is simple enough for small applications but flexible enough for many larger projects.
More importantly, learning Zustand helps you understand an important frontend concept:
Not all state belongs in the same place.
Some state should remain local.
Some should be globally shared.
Meanwhile, server state often belongs in a dedicated server-state solution.
Frequently Asked Questions
What is Zustand used for?
Zustand is used to manage shared client-side state in React and JavaScript applications.
It is useful for carts, themes, filters, settings, UI state, workflows, and other globally shared frontend data.
Is Zustand a replacement for Redux?
It can replace Redux in many small and medium applications.
However, Redux Toolkit may still be preferable when you need stronger conventions and highly structured client state.
Is Zustand better than Redux?
Neither is always better.
Zustand is usually simpler, while Redux provides more structure.
Does Zustand replace TanStack Query?
No.
Zustand focuses mainly on client state, while TanStack Query focuses on server state.
Can Zustand fetch API data?
Yes.
Async actions can call APIs.
However, Zustand does not automatically provide advanced API caching and server synchronization features.
Does Zustand need a Provider?
For normal store usage, usually no.
Components can import the store directly.
Can Zustand persist data?
Yes.
Its persistence middleware can save selected state to browser storage.
Does Zustand support TypeScript?
Yes.
Zustand works well with TypeScript.
Can I use Zustand and TanStack Query together?
Yes.
This is a useful combination:
Zustand → Client state
TanStack Query → Server state
Is Zustand good for beginners?
Yes.
Its small API and low boilerplate make it one of the easier React state-management libraries to learn.
Is Zustand only for React?
It is strongly associated with React, but Zustand also exposes APIs that can be used outside React. For most beginners, however, React is the common use case.
Should I use Zustand for every state variable?
No.
Keep small component-specific values in local React state. Use Zustand when state genuinely needs to be shared.
Conclusion
Zustand is a lightweight and flexible state-management library that makes shared client-side state easier to manage in React applications.
Instead of creating a large amount of setup code, you can create a store with:
const useStore = create((set) => ({
count: 0,
increase: () =>
set((state) => ({
count: state.count + 1,
})),
}));
Then components can subscribe only to the values they need.
The most important Zustand concepts for beginners are:
create()
set()
get()
selectors
actions
stores
persistence
middleware
A useful mental model is:
Local component state
↓
useState
Shared client state
↓
Zustand
Server/API state
↓
TanStack Query
Zustand is particularly useful for shopping carts, themes, global filters, settings, sidebars, multi-step workflows, and shared UI state.
However, avoid moving every value into a global store. Local state should remain local, while API-heavy server data is often better handled by a specialized solution such as TanStack Query.
If you already understand React basics and want a simpler alternative to more structured global state-management libraries, Zustand is an excellent next tool to learn.




