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 at Pristren

Mahmudul Haque Qudrati

CEO & ML Engineer

April 30, 2026
4 min read
Zustand 5: The Minimal React State Manager That Scales

Why Zustand?

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

typescript
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:

tsx
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.

Start free

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:

typescript
// 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:

typescript
// 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:

typescript
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:

typescript
// 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

typescript
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

typescript
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

ScenarioRecommended
Simple global state (1-3 stores)Zustand
Atomic state with many fine-grained piecesJotai
Complex state machines / time-travel debuggingRedux 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 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.

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.

#zustand#state-management#react#typescript#v5

// discussion

Comments

0/4000
Mahmudul Haque Qudrati — CEO & ML Engineer at Pristren

Mahmudul Haque Qudrati

CEO & ML Engineer

Visionary technologist, software engineer, and machine learning specialist. Founder and CEO of Pristren, directing engineering teams that ship production-grade AI/ML pipelines, mission-critical full-stack applications, and developer tooling. Creator of Zlyqor, the unified team workspace platform. Author of 540+ technical guides and benchmark research reports on large language models, agentic workflows, Model Context Protocol (MCP), and modern web stacks.

PristrenZlyqor