Mastering Zustand V5 for React State
Zustand V5 is the latest evolution of a lightweight state management library that prioritizes simplicity, predictable updates, and strong ergonomics. This updated version embraces React 18’s useSyncExternalStore, offering more consistent rendering behavior and significantly improved performance. Many developers choose Zustand because it avoids the verbosity of Redux and the reactivity overhead of MobX while still delivering predictable and scalable state flows.
This article explains how Zustand V5 works, why it differs from earlier versions, its internal mechanisms, and how to build real‑world stores using clean patterns. All examples use rewritten code and original variable names to avoid overlap with common tutorials.
Why Zustand V5 Matters
Zustand is built around several principles:
- Minimal boilerplate.
- Direct, synchronous state updates.
- Component‑level subscriptions to avoid unnecessary re‑renders.
- Extensible middleware for structured logic.
Version 5 continues to optimize these foundations by integrating tightly with React’s rendering model.
Key improvements include:
-
useSyncExternalStoreintegration for consistent subscription behavior. - Better compatibility with concurrent rendering.
- More predictable state propagation.
- Reduced package size through internal refactoring.
Creating Your First Store
Zustand uses a simple factory function, making it easy to define modular and testable stores.
import { createStore } from 'zustand/vanilla';
import { create } from 'zustand';
type CounterState = {
value: number;
increase: () => void;
reset: () => void;
};
const useCounter = create<CounterState>()((set) => ({
value: 0,
increase: () =>
set((draft) => ({
value: draft.value + 1,
})),
reset: () =>
set(() => ({
value: 0,
})),
}));This store remains small, readable, and predictable. There are no reducers, no action types, and no ceremony.
Using the Store in Components
Components subscribe to specific slices of state, not the entire store.
import React from 'react';
export function CounterPanel() {
const count = useCounter((s) => s.value);
const increase = useCounter((s) => s.increase);
return (
<section>
<p>Counter: {count}</p>
<button onClick={increase}>Add One</button>
</section>
);
}Zustand ensures that CounterPanel only re-renders when value changes.
Selective Subscriptions for Performance
One of Zustand’s strengths is selective subscriptions. With Redux, updating one part of the store typically re-renders all connected components. Zustand avoids this by subscribing to only the selected state.
Example: Selecting Multiple Fields With Stability
import { shallow } from 'zustand/shallow';
export function StatsPanel() {
const { clicks, views } = useCounter(
(s) => ({ clicks: s.value, views: s.value * 2 }),
shallow
);
return (
<div>
<p>Clicks: {clicks}</p>
<p>Views: {views}</p>
</div>
);
}The shallow comparator prevents re-renders when the selected object shape remains identical.
Using Middleware for Advanced Scenarios
Zustand’s middleware architecture makes it possible to extend behavior without changing your store logic.
Persistence Example
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
type Preferences = {
theme: 'light' | 'dark';
switchTheme: () => void;
};
const usePreferences = create<Preferences>()(
persist(
(set) => ({
theme: 'light',
switchTheme: () =>
set((prev) => ({
theme: prev.theme === 'light' ? 'dark' : 'light',
})),
}),
{ name: 'ui-preferences' }
)
);Now your theme selection survives reloads.
Logging Example (Custom Middleware)
const withLogger =
(core) =>
(set, get, api) =>
core(
(args) => {
console.log('State change:', args);
set(args);
},
get,
api
);
const useLoggedCounter = create(
withLogger((set) => ({
count: 0,
inc: () => set((s) => ({ count: s.count + 1 })),
}))
);This pattern enables custom observability systems without overcomplicating the store.
Derived State and Computed Fields
Zustand doesn’t require the store to contain everything. Derived values can be computed inside selectors.
const price = useShop((s) => s.total);
const tax = useShop((s) => s.total * 0.2);
const totalWithTax = useShop((s) => s.total * 1.2);Since each subscription is isolated, only affected components update.
Zustand V5 Internal Mechanisms
Zustand V5 relies on useSyncExternalStore, making the state system consistent across concurrent rendering environments.
The process works as follows:
- Each store exposes a subscribe method.
- Components register a subscription for a selected slice.
- When the store updates, only those subscriptions are notified.
- React re-renders only the affected components.
The result is deterministic behavior, even when components suspend, stream, or render in parallel.
Why This Matters
- Prevents tearing in concurrent mode.
- Ensures consistent reads during render.
- Reduces unnecessary renders.
Scaling Zustand to Larger Applications
Although Zustand is often associated with small apps, it scales well. Recommended patterns include:
- Modular stores (splitting logic into independent slices).
- Co-locating store logic near features.
- Avoiding monolithic global stores when unnecessary.
- Combining Zustand with React Query for server data.
Slice Pattern Example
const createCounterSlice = (set) => ({
count: 0,
add: () => set((s) => ({ count: s.count + 1 })),
});
const createUserSlice = (set) => ({
user: null,
login: (name) => set(() => ({ user: name })),
});
const useAppStore = create((set) => ({
...createCounterSlice(set),
...createUserSlice(set),
}));This approach organizes your domain logic cleanly.
When to Use Zustand vs Alternatives
Zustand is ideal when:
- You need client-side state only.
- You want minimal API surface.
- You prefer explicit state updates over reducers.
- You want to avoid context performance pitfalls.
Use React Query instead of Zustand for:
- Server data caching
- Automatic refetching
- Background updates
- Stale‑while‑revalidate flows
Both tools complement each other.
Conclusion
Zustand V5 remains one of the most practical and minimalistic state management libraries for React. Its strong performance guarantees, simple architecture, and robust middleware system make it suited for both small and enterprise-grade applications. The integration with useSyncExternalStore aligns it perfectly with modern React patterns, ensuring predictable state flow in all rendering modes.
Whether you’re building a large dashboard, a real-time UI, or a small personal project, Zustand V5 offers a clean and effective model for managing state without the noise found in other libraries.