If you’ve been working with React for a while, you’ve probably gone through the classic path: first useState, then useContext, and suddenly Redux appears — along with a ton of files, actions, reducers, and the feeling that everything has become too complicated for a real task. It’s at this point that you might encounter a solution like the Zustand library. Many developers meet it for the first time because they’re used to working with Redux.
Part 1. What the Zustand Library Is and Why It’s Needed in React Projects
Zustand is a modern state management tool for React applications. Its main goal is to make working with state simple and understandable, without unnecessary architecture, complex patterns, or bulky files. Essentially, Zustand allows you to store state separately from components and access it exactly where it’s needed, without unnecessary wrappers or intermediate layers.
To put it very simply, Zustand solves one main problem: how to work with state conveniently and efficiently so that the code remains clean and logical, rather than turning into a bureaucratic labyrinth.
Why state management is so important
React out of the box does not provide a complete solution for global state. Component-level useState works well for individual components, but when the same data is needed in multiple parts of the application, compromises must be made.
You can lift state higher in the component tree, pass it through props, or use the Context API. All of these options work, but in practice, they quickly make project scaling more complicated.
This is exactly where Zustand shows its strength: it provides convenient global state without adding unnecessary complexity.
Core idea of the library
The essence of Zustand is that state is stored outside of React but used inside React in the most natural way possible.
You create a store — a simple function with state and methods to change it. Any component can subscribe only to the data it really needs and automatically update when that data changes.
Thanks to this approach:
- Components subscribe only to relevant fragments of state, not the whole state;
- Application performance remains high;
- Application behavior becomes predictable and manageable.
This is why Zustand feels like the “React way,” rather than a foreign solution.
Why Zustand library is often chosen over Redux
Many developers come to Zustand after working with Redux. Not because Redux is bad, but because it can sometimes be overkill for real projects.
Redux is excellent for large enterprise applications where strict architecture and predictability are essential. But in everyday development, you often want something simpler — without extra files, templates, and a dozen rules to remember.
Zustand wins with simplicity: it doesn’t impose a project structure or force rigid patterns. You decide how to organize state while paying only minimal complexity for it.
Minimal example of using Zustand
For clarity, here’s a basic example:
import { create } from 'zustand';
const useStore = create((set) => ({
count: 0,
increase: () => set((state) => ({ count: state.count + 1 })),
}));
function Counter() {
const count = useStore(state => state.count);
const increase = useStore(state => state.increase);
return (
<button onClick={increase}>
Count: {count}
</button>
);
}
There’s nothing extra here: no providers, wrappers, or abstractions for the sake of abstraction. This is a simple demonstration of Zustand’s philosophy: convenience, transparency, and predictability.
Projects where Zustand library is best suited
In practice, Zustand works especially well in projects where:
- The application is actively growing and becoming more complex;
- Maintaining readable code is important;
- The team doesn’t want to spend time on complicated architecture;
- High performance is required.
It works confidently both in small SPAs and in large interfaces with rich business logic. Zustand is not just “another state manager,” but a real helper for developers, allowing them to manage state without losing control over the application.
In short, Zustand does not promise magic and does not try to replace React. It simply makes state management convenient, predictable, and transparent for every developer.
Part 2. History of Zustand library and Its Philosophy
Looking at Zustand today, it might seem like it appeared out of nowhere: simple, convenient, and without extra solutions. But in reality, its emergence is a logical response to the evolution of React and its ecosystem over the past few years.
To understand Zustand’s philosophy, it’s important not only to know what it does but why it appeared at all.
Context: why the world needed Zustand library
At the time of Zustand’s creation, React developers already had choices. Redux was long considered the de facto standard, Context API was actively developing, MobX offered a reactive approach, and later atom-based solutions started gaining popularity.
However, most of these tools were too heavy for simple and medium projects. Often, developers had to carry along architecture that was overkill, leading to fatigue from “too much code for the sake of code.”
Many developers thought:
“I just need to store state and change it conveniently. Why does it require so many extra steps?”
Zustand was born in this context — to make state management light, transparent, and natural.
Who created Zustand
The library was created by Sven Sauleau, a developer familiar with React’s inner workings and the practical challenges teams face. His goal was down-to-earth: create a tool that helps write code, not get in the way.
Zustand was not conceived as a “Redux killer” or revolutionary solution. Rather, it’s an attempt to remove everything unnecessary and leave only what’s truly needed for state management.
The main principle throughout the library: minimal abstractions, maximum practical benefit.
Philosophy: “less magic, more control”
Zustand is built on the idea that state should be as transparent as possible. The developer always sees:
- Where the state is created;
- How it changes;
- Who subscribes to it.
There’s no hidden reactivity or mysterious mechanisms “under the hood.” Everything is based on plain functions and explicit state updates.
At the same time, the library doesn’t impose strict rules: you decide how to organize the store, file structure, and patterns. Flexibility is a conscious choice by the authors.
Zustand as a response to overloaded architecture
Looking at frontend history, a trend emerges: first, the community leaned toward strict architectures, then toward simplification. Redux symbolized the first stage: everything predictable and strict, but often too complex for real projects.
Zustand emerged as a tool for the second stage — when developers began valuing simplicity and development speed as much as formal correctness.
It was born at the moment it became clear: not every project needs massive architecture, but every project needs convenient and understandable state.
Why Zustand library is not tied to React Context
A key architectural decision was not to use React Context as the main state store. This is intentional: Context is convenient, but it has limitations, especially with performance and unnecessary re-renders.
Zustand stores state outside React, using it only for subscriptions. This keeps simplicity and efficiency, even with many components.
Minimalism as the library’s foundation
Looking at the source code, it’s clear that every line has a practical purpose. There are no layers for extensibility or “beautiful” abstractions that are rarely used in real development.
This is why Zustand is easy to understand even for newcomers; a beginner can grasp it in one evening.
How philosophy affects the developer experience
The library’s philosophy directly impacts how teams work with it:
- Code reads almost like plain JavaScript;
- There’s no feeling of struggling with the tool;
- State feels like part of logic, not a separate “alien world.”
Many teams adopt Zustand because it requires little learning and allows writing clean and convenient code immediately.
In short, Zustand’s history is about simplification and practical benefit. It wasn’t created for hype or trends, but to give developers a tool that’s comfortable for real React tasks.
Part 3. Zustand library Architecture: How It Works Under the Hood
When you first encounter Zustand, it may seem too simple to be a serious tool. But this impression is misleading. Behind this simplicity lies a well-thought-out architecture that makes Zustand fast, flexible, and convenient in real-world projects.
In this section, we’ll explore how Zustand works internally, without diving into the depths of the source code — only what really helps understand what’s happening in the application.
Store as the central element
At the core of Zustand is the concept of a store. Essentially, a store is a regular JavaScript object that contains state and functions to modify it.
It’s important to understand that this store:
- Exists outside the React component lifecycle;
- Is created once;
- Is not recreated on every render.
This is a fundamental difference from local component state and one reason why Zustand scales so well.
When you call create, Zustand creates the store and returns a hook through which components can connect to it.
Subscription instead of global re-renders
One of Zustand’s strongest architectural features is the subscription mechanism. A component doesn’t “listen” to the entire state, but subscribes only to a specific piece of data.
Practically, it looks like this:
you pass a selector function to the hook, and Zustand tracks whether the specific value returned by that function has changed.
- If the value hasn’t changed — the component doesn’t re-render.
- If the value has changed — the component updates.
Thanks to this approach, Zustand avoids the typical problem of global stores, where any change triggers a cascade of re-renders.
How Zustand library updates state
State updates in Zustand happen through the set function. This is deliberately simple and transparent.
When you call set, the library:
- Computes the new state;
- Compares it with the previous state;
- Notifies only those subscribers for whom the change is actually relevant.
There are no complex queues, actions, or reducers. Everything happens synchronously and predictably, which greatly simplifies debugging.
This is why, when working with Zustand, you rarely feel that “something updated on its own.”
Immutability — optional, not mandatory
Unlike Redux, Zustand does not force strict immutability. You decide how to update state: return a new object or use additional tools like Immer.
This approach might seem risky, but in practice it gives the developer freedom. In simple cases, you can do ordinary updates, and in more complex ones — integrate middleware and work the usual way.
This is another example of Zustand’s philosophy: the library does not impose rules, it provides tools.
Why Zustand library doesn’t use the Virtual DOM directly
Zustand’s architecture is deliberately built not to interfere with React. It doesn’t manage renders directly and doesn’t attempt to optimize the Virtual DOM for React.
Zustand’s task is simple: notify the component correctly and quickly that the required data has changed. React handles everything else.
This separation of responsibilities makes the architecture cleaner and reduces the likelihood of conflicts with future React updates.
Middleware as an extension, not a requirement
Another important part of Zustand’s architecture is middleware. They exist, but they are not mandatory.
You can use Zustand without any middleware and it will be fully functional. Or you can gradually add extra features like:
- State persistence;
- Devtools integration;
- Immutability handling;
- Advanced subscriptions.
From an architectural perspective, this means the core of the library remains small, and everything else is optional.
Why Zustand’s architecture works well in real projects
In real applications, architecture often breaks not because of complex logic, but because of excessive abstractions. Zustand aims to avoid this.
Zustand’s architecture:
- Is easy to read;
- Is easy to explain to new developers;
- Does not require strict adherence to patterns;
- Scales as the project grows.
You can start with a single simple store and later divide state into domain areas — without rewriting half of the application.
Zustand’s architecture is built around a simple idea: store state separately, update it explicitly, and subscribe components only to the data they need.
There’s no complex magic here, but that is exactly why Zustand feels reliable and predictable. It doesn’t interfere with React but complements it precisely where the built-in tools are insufficient.
Part 4. Using Zustand library in Practice: Basic Scenarios and Patterns
Once you understand the idea and architecture of Zustand, the main question arises: how to apply it in a real project without regret? This part addresses that.
It’s important to say right away: Zustand does not impose strict rules. But over time, patterns have emerged in the community and in real projects that make store usage more organized and predictable.
Creating the first store: where to start
Practical introduction to Zustand almost always begins with a small store. Usually, it contains simple state: a counter, a loading flag, user data, or UI settings.
A simple store looks like this:
import { create } from 'zustand';
const useAppStore = create((set) => ({
isAuth: false,
login: () => set({ isAuth: true }),
logout: () => set({ isAuth: false }),
}));
Nothing complicated here: state and functions to modify it live in one place. This makes the code clear and easy to maintain.
Using the store in components
One of Zustand’s strengths is how naturally it integrates into components. You just call the hook and get the data you need.
function Header() {
const isAuth = useAppStore(state => state.isAuth);
const logout = useAppStore(state => state.logout);
return (
<header>
{isAuth && <button onClick={logout}>Logout</button>}
</header>
);
}
The component subscribes only to isAuth. If something else in the store changes, this component does not re-render. In real interfaces, this quickly becomes a major advantage.
One store or several: which is better
A common question is whether to create one large store or several small ones. Zustand allows both approaches, but in practice decomposition is preferred.
State is usually divided by domain: user, settings, data, UI state. Each store is responsible for its own area and does not know about the internals of others.
This approach simplifies maintenance and makes code more modular, especially as the project grows.
Working with asynchronous logic
Asynchronous operations are inevitable in any real application. The good news is that Zustand does not require special solutions for async code.
You simply write an asynchronous function directly in the store:
const useUserStore = create((set) => ({
user: null,
loading: false,
fetchUser: async () => {
set({ loading: true });
const response = await fetch('/api/user');
const data = await response.json();
set({ user: data, loading: false });
},
}));
It looks like ordinary JavaScript, without additional abstractions. This straightforwardness is one reason why Zustand is often praised.
Selectors as the key to performance
If you use Zustand “straight out of the box,” you might easily miss an important point — selectors. They unlock the full performance potential.
Best practice: always take from the store only what the component really needs. Not the entire object, but specific fields or computed values.
This makes the code not only faster but also clearer.
Logic in the store, not in components
Over time, it becomes clear that the store is not just a data container, but a place for business logic. Validations, transformations, side effects — all of this logically belongs there.
Components in this approach become maximally “dumb”: they just display data and call methods. This makes the UI more stable and easier to test.
Common beginner mistakes
When working with Zustand, beginners often:
- Pull the entire store into a component without selectors;
- Store too much UI state mixed with business data;
- Duplicate logic across different components.
All these problems are easily solved if you treat the store as a separate layer of the application, not just “another hook.”
In practice, Zustand proves to be a very flexible tool. It does not limit the developer or impose complex rules but encourages neat code organization.
If used consciously — with selectors, decomposition, and externalized logic — Zustand easily becomes the central state management point in the application.
Part 5. Middleware in Zustand library and Working with localStorage
When an application becomes slightly more complex than just “showing a counter,” a bunch of practical questions immediately arise. How do you persist state across page reloads? How do you debug changes? How do you work cleanly with side effects?
This is where Zustand reveals another strong point — through middleware. Importantly, middleware here are optional. They are not the foundation but neat extensions that you connect only when really needed.
What is middleware in Zustand, in simple terms
Middleware in Zustand are wrappers around the store that allow you to intercept state updates and extend the store’s behavior. It feels somewhat similar to Redux middleware but is implemented much more simply and transparently.
You still work with the same store, the same set and get functions, but extra logic runs between the state change and the result.
Why use middleware at all
In real projects, middleware are most often needed for three purposes:
- State persistence;
- Debugging;
- Simplifying immutability handling.
And the best part — you decide whether to connect them or not. Zustand does not force you to do it from day one.
Persist middleware and localStorage synchronization
The most popular middleware in Zustand is persist. It’s used when you need to keep state across page reloads.
For example: user settings, tokens, theme, or shopping cart contents.
Example of a store with localStorage persistence:
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
const useSettingsStore = create(
persist(
(set) => ({
theme: 'light',
toggleTheme: () =>
set((state) => ({
theme: state.theme === 'light' ? 'dark' : 'light',
})),
}),
{
name: 'settings-storage',
}
)
);
Here a very important thing happens: Zustand handles serialization and restoring of state automatically. You don’t need to manually write localStorage.setItem or think about when to load the data.
For most cases, this is more than enough.
What to keep in mind when working with localStorage
Although persist simplifies life, there are some points to remember. Not all state makes sense to store in localStorage. For example, temporary UI flags or easily recalculated data are better kept in memory.
Good practice: persist only what truly needs to survive a page reload. This keeps state cleaner and reduces the risk of weird bugs.
Devtools middleware and state debugging
Another popular middleware is devtools. It allows connecting Zustand to Redux DevTools in the browser and seeing how the state changes over time.
This is especially useful during development, when you need to understand when and why the store changed.
Connection is very simple:
import { devtools } from 'zustand/middleware';
const useStore = create(
devtools((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}))
);
Even if you’ve used Redux DevTools before, everything here will feel familiar.
Combining middleware
Zustand allows combining middleware. For example, you can use persist and devtools simultaneously. This is done without any complex setup or “magic.”
It’s important only to follow the order and remember that each middleware is an additional layer of logic. But in most cases, the overhead is minimal.
Why middleware in Zustand feel simpler than in Redux
The main difference is experience. Middleware in Zustand don’t change your mindset. You still write normal JavaScript code, instead of adapting to the library’s infrastructure.
This fits well with Zustand’s philosophy: solve the problem first, then think about tools.
Middleware is where Zustand stops being just a “simple state manager” and becomes a fully-fledged tool for real applications.
Working with localStorage, debugging, extending logic — all of this is handled cleanly and without unnecessary complexity. That’s why Zustand is often chosen not only for pet projects but also for commercial development.
Part 6. Typing and Testing in Zustand library
When a project stops being small, TypeScript and code quality become almost inevitable concerns. Many developers wonder: will a simple state manager break under the requirements of typing and testing?
The good news is that Zustand works great in TypeScript projects and does not require special tricks for testing.
TypeScript and Zustand: why they work well together
One reason Zustand is so easy to type is its API. A store in Zustand is essentially a plain object with state and methods. This means TypeScript fully understands what’s happening inside.
Typing starts by describing the state interface:
import { create } from 'zustand';
interface CounterState {
count: number;
increment: () => void;
decrement: () => void;
}
const useCounterStore = create<CounterState>((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}));
There’s no complex magic here. TypeScript immediately knows which fields exist in the store and which methods are available. Errors are caught even before running the app.
Why typing doesn’t overload the code
Unlike some other solutions, typing in Zustand does not become a separate layer of logic. You don’t define actions or create extra types just for infrastructure.
Types serve one purpose: to make code safer and clearer. This is especially pleasant in large teams where it’s important for data interfaces to be obvious.
Typing selectors and hooks
When you use selectors, TypeScript automatically infers the return type. This means that in the component you already get strictly typed data without extra effort.
In practice, this feels completely native and requires almost no manual annotations.
Testing the store without React
One of Zustand’s nice bonuses is that the store is not tied to React. This means you can test it without rendering components.
You work with normal functions and state, which greatly simplifies tests.
Example of a simple test:
import { act } from 'react-dom/test-utils';
import { useCounterStore } from './store';
test('counter increments', () => {
act(() => {
useCounterStore.getState().increment();
});
expect(useCounterStore.getState().count).toBe(1);
});
No component mocks, no complex environment setup. You just call the method and check the result.
Testing asynchronous logic
Asynchronous methods in Zustand are tested just as easily as synchronous ones. Since these are ordinary functions, you can use any testing tools — Jest, Vitest, etc.
The key is to control the state before and after the async action. This makes tests readable and reliable.
What is usually tested in Zustand
In practice, the most tested parts are:
- Business logic in the store;
- Correctness of state updates;
- Functionality of asynchronous methods;
- Edge cases and errors.
The UI is tested separately. This fits well with the idea of separation of concerns and makes the testing strategy cleaner.
Zustand and support for large teams
Typing and ease of testing make Zustand convenient for team development. A new developer can quickly understand the store structure, and tests serve as additional documentation.
Over time, this saves a lot of time and reduces bugs related to incorrect state usage.
Zustand scales not only functionally but also in terms of code quality requirements. TypeScript feels natural here, and testing does not require complex infrastructure.
If a project grows and faces serious requirements, Zustand fits comfortably into the process and does not become a bottleneck.
Part 7. Best Practices for Using Zustand library, Limitations, and When Not to Use It
After all the advantages, examples, and comparisons, it’s natural to ask the most mature question:
Is Zustand always the right choice?
The short answer — no. And that’s okay. Below we’ll discuss how to use Zustand properly, where it shines the most, and in which cases it’s worth considering other solutions.
Best Practices for Working with Zustand
Over time, a few informal rules emerge when working with Zustand, which greatly simplify life.
First and perhaps most important — don’t pull the entire store into a component, even if you really want to. Selectors aren’t just a “future optimization”; they are a basic tool. The more precisely a component knows what it needs, the more stable and faster the application will be.
Second point — state decomposition. When a project grows, the temptation to put everything in one store appears almost automatically. In practice, this leads to hard-to-maintain code. It’s much better to separate state by domain: user, data, settings, UI. Zustand fully supports this approach.
Third practice — keep business logic in the store, not in components. Components should display data and respond to events, not decide how exactly to change the state. This approach makes code cleaner and testing easier.
How Not to Turn Zustand into a “New Redux”
Paradoxically, Zustand can be used incorrectly — so that it starts to resemble Redux with all its downsides.
This happens when:
- the store grows into a massive file;
- logic starts to duplicate;
- there are too many indirect dependencies.
To avoid this, remember: Zustand is about simplicity. If at some point you catch yourself thinking that the architecture has become more complicated than the tasks themselves — it’s time to stop and simplify.
Limitations of Zustand
Despite all its advantages, Zustand has its limitations. It doesn’t try to be a universal solution for everything.
For example, Zustand:
- is not designed for complex enterprise-level data flows;
- does not replace server state or API caching;
- requires discipline if the project is large and team-based.
It’s also important to understand that Zustand does not enforce an architecture. For experienced developers, this is a plus, but for teams without a common approach, it can be a downside.
When Zustand library Is an Excellent Choice
In practice, Zustand performs especially well in:
- medium to large SPAs;
- admin panels and dashboards;
- projects where development speed is important;
- teams that value readable code;
- applications with rich client-side logic.
In all these cases, Zustand provides a good balance between simplicity and control.
When to Consider Alternatives
There are situations where it’s better to look at other tools.
If a project:
- requires a strictly formalized architecture;
- operates under rigid corporate standards;
- is heavily tied to the Redux ecosystem;
- needs complex data synchronization scenarios.
In such cases, Redux or specialized solutions may be more suitable.
Conclusion
In summary, Zustand is a tool that fits modern React perfectly. It doesn’t try to replace everything at once, doesn’t complicate life, and doesn’t impose unnecessary rules.
Its main strength is the feeling of comfort. You work with state as if it’s just a normal part of your application, not a separate world with its own laws.
This is exactly why Zustand is increasingly used in real projects — not because of hype, but because it’s simply convenient to work with.