For a long time, state management felt more complicated than it needed to be.
I understood React's useState.
I understood passing props.
I understood lifting state up.
But the moment applications became larger, everything started getting messy.
Then I started learning Zustand.
At first, I was confused.
Not because Zustand was complicated.
So I decided to stop memorizing syntax and instead understand the problem Zustand was trying to solve.
This article is the explanation I wish I had when I first started learning it.
The Problem: React State Doesn't Scale Forever
Let's start with plain React.
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
{count}
</button>
);
}
Simple.
Beautiful.
No problems.
But now imagine you're building a real application.
You have:
A Navbar
A Dashboard
A Profile page
Protected routes
Notifications
All of them need access to the logged-in user.
Where do you store the user?
You might start with:
const [user, setUser] = useState(null);
Then pass it down as props.
Then:
Then:
Then:
Suddenly you're passing the same piece of data through multiple components.
Some components don't even use it.
They're just forwarding it.
This is called prop drilling.
As applications grow, prop drilling becomes frustrating.
What Zustand Actually Solves
Zustand gives your application a shared state outside React components.
Think of it like this:
Without Zustand:
Component A
↓
Component B
↓
Component C
↓
Component D
Every level passes props.
With Zustand:
Store
/ | \
/ | \
Navbar Dashboard Profile
Every component talks directly to the store.
No middleman.
No prop drilling.
The Mental Model That Made Zustand Click
Forget the library for a moment.
A Zustand store is just:
{
state,
actions
}
State = Data
Actions = Functions that modify the data
That's it.
Example:
{
count: 0,
increment() {
count++;
}
}
Zustand simply gives React a way to access and update that object.
Creating Your First Store
Install Zustand:
npm install zustand
Create a store:
import { create } from "zustand";
const useCounterStore = create((set) => ({
count: 0,
increment: () =>
set((state) => ({
count: state.count + 1,
})),
}));
plaintext
Notice two things:
State
count: 0
Action
increment: () => ...
plaintext
That's the entire pattern.
What Is set()?
This was one of my biggest questions.
Think of set() as Zustand's version of React's state updater.
React:
setCount(count + 1);
Zustand:
set({
count: 1,
});
Or:
set((state) => ({
count: state.count + 1,
}));
javascript
Whenever you want to update state, use set().
Using a Store Inside Components
function Counter() {
const count = useCounterStore(
(state) => state.count
);
const increment = useCounterStore(
(state) => state.increment
);
return (
<button onClick={increment}>
{count}
</button>
);
}
javascript
Here we're selecting specific pieces of state.
This is important because Zustand only re-renders components that depend on changed values.
That's one reason it's so fast.
Building a Real Authentication Store
Let's create something closer to production.
We need:
User
Token
Notifications
Login
Logout
Initial State
user: null,
token: null,
notifications: [],
But we have a problem.
Refreshing the page resets state.
If the user refreshes:
user = null
token = null
They're logged out.
Not ideal.
Introducing localStorage
The browser provides:
localStorage
Data stored there survives page refreshes.
Save:
localStorage.setItem("token", token);
Read:
localStorage.getItem("token");
Remove:
localStorage.removeItem("token");
Why We Use JSON.stringify()
localStorage only stores strings.
This works:
localStorage.setItem(
"token",
"abc123"
);
javascript
This doesn't:
localStorage.setItem(
"user",
{ name: "Jeffrey" }
);
javascript
Objects must become strings.
JSON.stringify(user);
When reading:
JSON.parse(
localStorage.getItem("user")
);
plaintext
Complete Auth Store
import { create } from "zustand";
export const useAuthStore = create(
(set) => ({
user:
JSON.parse(
localStorage.getItem("user")
) || null,
token:
localStorage.getItem("token") ||
null,
notifications: [],
login: (user, token) => {
localStorage.setItem(
"user",
JSON.stringify(user)
);
localStorage.setItem(
"token",
token
);
set({ user, token });
},
logout: () => {
localStorage.removeItem("user");
localStorage.removeItem("token");
set({
user: null,
token: null,
});
},
setNotifications: (
notifications
) => {
set({ notifications });
},
})
);
javascript
Why Notifications Might Belong in Global State
This was another question I had.
Why not just keep notifications in component state?
Because notifications are usually displayed globally.
Example:
Navbar bell icon
Dashboard
Activity page
Real-time updates
Multiple unrelated components need the same data.
That's a strong sign the data belongs in a global store.
Why Not Use Cookies Instead?
Many wise developers(like me ofc) ask this.
Cookies and localStorage solve different problems.
Cookies:
Sent automatically with every request
Can expire automatically
Good for authentication when using HTTP-only cookies
localStorage:
Easy JavaScript access
Simpler API
Great for client-side persistence
Many production systems actually use:
Access Token → Memory
Refresh Token → HTTP-only Cookie
But for learning projects and many React applications, localStorage is easier to understand.
Zustand vs Redux
A quick comparison.
Redux:
dispatch({
type: "LOGIN",
payload: user,
});
Reducer:
switch(action.type) {
case "LOGIN":
}
Zustand:
login(user);
That's it.
Less boilerplate.
Less configuration.
Less mental overhead.
For many projects, that's enough.
Common Beginner Mistakes
Forgetting to Return an Object
Wrong:
create((set) => {
count: 0;
});
javascript
Correct:
create((set) => ({
count: 0,
}));
javascript
Using Semicolons Instead of Commas
Wrong:
{
count: 0;
user: null;
}
javascript
Correct:
{
count: 0,
user: null,
}
javascript
Parsing Non-JSON Data
Wrong:
JSON.parse(
localStorage.getItem("token")
);
If the token is already a string:
localStorage.getItem("token");
is enough.
Final Thoughts
The biggest lesson I learned wasn't a Zustand API.
It was this:
State management libraries aren't really about managing state. They're about managing complexity.
When your application grows beyond a handful of components, sharing data becomes harder than creating it.
Zustand solves that problem with an incredibly small API:
State
Actions
set()
That's essentially the whole library.
Once I stopped focusing on the syntax and started focusing on the problem, Zustand finally clicked.
And honestly, that's true for most technologies we learn as developers.
We don't learn tools by memorizing APIs.
We learn them by understanding why they exist.
Once again, write code as art.
Top comments (0)