Zustand 5: The Minimal React State Manager That Scales
Zustand 5 tightens TypeScript types, removes the act() auto-wrap, and adds stricter equality — here is how to use it effectively from simple stores to large sliced architectures.
Mahmudul Haque Qudrati
CEO & ML Engineer
One AI engineering post, weekly
LLM benchmarks, prompt techniques, and token-cost breakdowns — not another AI news roundup.
Redux Toolkit is powerful but verbose. Jotai is atomic but unfamiliar. Zustand sits in the middle: a single function call creates a store, components subscribe with a selector, and re-renders only happen when the selected value changes. The entire library is under 2KB gzipped.
Basic Store
import { create } from "zustand";
interface TaskStore {
tasks: Task[];
isLoading: boolean;
fetchTasks: (projectId: string) => Promise<void>;
addTask: (task: Task) => void;
removeTask: (id: string) => void;
}
export const useTaskStore = create<TaskStore>()((set) => ({
tasks: [],
isLoading: false,
fetchTasks: async (projectId) => {
set({ isLoading: true });
const tasks = await api.tasks.list(projectId);
set({ tasks, isLoading: false });
},
addTask: (task) => set((state) => ({ tasks: [...state.tasks, task] })),
removeTask: (id) =>
set((state) => ({ tasks: state.tasks.filter((t) => t.id !== id) })),
}));
Using it in a component — only re-renders when tasks changes, not on isLoading changes:
function TaskList() {
const tasks = useTaskStore((state) => state.tasks);
const fetchTasks = useTaskStore((state) => state.fetchTasks);
useEffect(() => { fetchTasks(projectId); }, [projectId, fetchTasks]);
return <ul>{tasks.map(t => <li key={t.id}>{t.title}</li>)}</ul>;
}
Team workspace
Ship faster with chat, meetings, and projects in one place — Zlyqor.
Zustand v5 Breaking Changes
No more auto-wrapping in act(): In v4, React Testing Library's act() was applied automatically during tests. In v5, you manage this explicitly:
// v5 — explicit act() in tests
import { act } from "@testing-library/react";
await act(async () => {
useTaskStore.getState().addTask(newTask);
});
Stricter TypeScript: The create<T>() function now requires the state type argument. The old implicit inference sometimes produced incorrect types:
// v4 — sometimes worked incorrectly
const useStore = create((set) => ({ count: 0 }));
// v5 — explicit type required
const useStore = create<{ count: number }>()((set) => ({ count: 0 }));
Stricter equality: In v5, the default equality function is Object.is instead of shallow comparison. If you rely on shallow equality for objects, you need to pass a custom equality function:
import { shallow } from "zustand/shallow";
const { tasks, isLoading } = useTaskStore(
(state) => ({ tasks: state.tasks, isLoading: state.isLoading }),
shallow
);
Slices Pattern for Large Stores
For large apps, split the store into slices:
// stores/slices/taskSlice.ts
import type { StateCreator } from "zustand";
export interface TaskSlice {
tasks: Task[];
addTask: (task: Task) => void;
}
export const createTaskSlice: StateCreator<TaskSlice> = (set) => ({
tasks: [],
addTask: (task) => set((state) => ({ tasks: [...state.tasks, task] })),
});
// stores/slices/uiSlice.ts
export interface UISlice {
sidebarOpen: boolean;
toggleSidebar: () => void;
}
export const createUISlice: StateCreator<UISlice> = (set) => ({
sidebarOpen: true,
toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })),
});
// stores/root.ts
type RootStore = TaskSlice & UISlice;
export const useStore = create<RootStore>()((...a) => ({
...createTaskSlice(...a),
...createUISlice(...a),
}));
Persist Middleware
import { create } from "zustand";
import { persist } from "zustand/middleware";
const useSettingsStore = create<SettingsStore>()(
persist(
(set) => ({
theme: "light",
setTheme: (theme) => set({ theme }),
}),
{
name: "app-settings",
// defaults to localStorage — use sessionStorage if needed:
storage: createJSONStorage(() => sessionStorage),
partialize: (state) => ({ theme: state.theme }), // only persist theme
}
)
);
Immer Middleware
import { create } from "zustand";
import { immer } from "zustand/middleware/immer";
const useStore = create<Store>()(
immer((set) => ({
users: [] as User[],
updateUser: (id: string, name: string) =>
set((state) => {
const user = state.users.find(u => u.id === id);
if (user) user.name = name; // mutate directly — Immer handles immutability
}),
}))
);
When to Choose What
| Scenario | Recommended |
|---|---|
| Simple global state (1-3 stores) | Zustand |
| Atomic state with many fine-grained pieces | Jotai |
| Complex state machines / time-travel debugging | Redux Toolkit |
| Server state (fetching/caching) | TanStack Query |
References: Zustand GitHub · docs · v5 migration
Frequently Asked Questions
What is Zustand 5?
Zustand 5 is the latest major version of the minimal React state management library. It introduces stricter TypeScript types, removes the automatic act() wrapping in tests, and changes the default equality function to Object.is for better predictability. The core API remains the same, making migration straightforward.
How does Zustand 5 work?
Zustand 5 works by creating a store with the create function, which returns a hook. Components subscribe to specific slices of state using selectors. When state changes, only components that selected the changed value re-render. The store can also be accessed outside React via getState() and setState().
What are the best practices for Zustand 5?
- Always provide an explicit type argument to
create<T>(). - Use selectors to minimize re-renders.
- For large stores, use the slices pattern to keep code organized.
- Use middleware like
persistandimmerfor common needs. - In tests, wrap state updates in
act()explicitly.
How much does Zustand 5 cost?
Zustand is completely free and open-source under the MIT license. There are no paid tiers or enterprise licenses. You can use it in personal and commercial projects without any cost.
Is Zustand 5 worth it in 2026?
Yes, Zustand 5 remains a top choice for React state management in 2026. Its minimal API, excellent TypeScript support, and small bundle size make it ideal for projects that need simple yet scalable state management. The v5 improvements further solidify its position as a reliable, modern solution.
Frequently Asked Questions
What is Zustand 5?
Zustand 5 is the latest major version of the minimal React state management library. It introduces stricter TypeScript types, removes the automatic act() wrapping in tests, and changes the default equality function to Object.is for better predictability. The core API remains the same, making migration straightforward.
How does Zustand 5 work?
Zustand 5 works by creating a store with the create function, which returns a hook. Components subscribe to specific slices of state using selectors. When state changes, only components that selected the changed value re-render. The store can also be accessed outside React via getState() and setState().
What are the best practices for Zustand 5?
Always provide an explicit type argument to create<T>(). Use selectors to minimize re-renders. For large stores, use the slices pattern to keep code organized. Use middleware like persist and immer for common needs. In tests, wrap state updates in act() explicitly.
How much does Zustand 5 cost?
Zustand is completely free and open-source under the MIT license. There are no paid tiers or enterprise licenses. You can use it in personal and commercial projects without any cost.
Is Zustand 5 worth it in 2026?
Yes, Zustand 5 remains a top choice for React state management in 2026. Its minimal API, excellent TypeScript support, and small bundle size make it ideal for projects that need simple yet scalable state management. The v5 improvements further solidify its position as a reliable, modern solution.
Mahmudul Haque Qudrati
CEO & ML Engineer
Visionary leader with extensive experience in machine learning and software development. Drives strategic innovation and business growth.
More from Mahmudul
Related Articles
How to Build an MCP Server in TypeScript (Step-by-Step)
Build your first MCP server in TypeScript with the official SDK — tools, stdio transport, and a Claude Code test loop in under an hour.
Next.js App Router Patterns in 2026: What to Use and What to Avoid
App Router is the default in Next.js. Here are the patterns that work well in production and the ones that create more problems than they solve.
React Server Components: What They Are and When to Use Them
React Server Components run only on the server and never ship to the browser. Here is what that means in practice and when it actually helps.
// discussion
Comments