Open-source project
pmndrs/zustand avatar
pmndrs/zustand

zustand's set merges by default, and the replace flag takes your actions with it

Bear necessities for state management in React. If you want to construct a single object with multiple state-picks inside, similar to redux's mapStateToProps, you can use useShallow to prevent unnecessary rerenders when the selector output does not change according to shallow equal.

58,747 stars2,193 forksTypeScriptMIT

At a glance

What is it?
A MIT-licensed React state library where the store is a hook, so no provider wraps your application and components select from it directly. The API is short enough to learn in a minute, which is also why its one destructive flag is easy to miss.
Who is it for?
Adopt zustand when you want shared React state without a provider tree and you are disciplined about selectors, because the whole performance story lives in the selector you pass and the default no-selector form re-renders on every change.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 7 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 25, 2026, and from our analysis. They are not legal advice.

Editorial analysis

set merges by default, and the replace flag takes the actions with it

The shape of a zustand store follows from one design choice: the store is a hook, and it holds whatever you put into it, which can be primitives, objects and functions together. Actions living in the same object as the data is what makes the API this short, and it is also where the sharpest edge lives. The set function takes a second argument that is false by default, meaning set merges into existing state. Pass true and it replaces the state model instead. The worked example makes the consequence explicit in a comment on one line: it clears the entire store, actions included. So the failure is total rather than partial. Your counters do not survive, and the store your components call no longer carries the functions, so nothing throws at the call site. This is the first thing to check when a store stops responding.

jsx
const useFishStore = create((set) => ({
  salmon: 1,
  tuna: 2,
  deleteEverything: () => set({}, true), // clears the entire store, actions included
  deleteTuna: () => set(({ tuna, ...rest }) => rest, true),
}))

The merge default exists for exactly this reason, which is why a passing second argument is the dangerous form rather than the safe one.

The shortest line in the documentation is also the slowest one

Calling the hook with no argument is the most natural thing to write, and the documentation warns against it directly. The bare form subscribes the component to the entire store, so the component re-renders on every state change regardless of what the change concerned. The stated fix is to select, and the default comparison for a selection is strict equality, old === new, which the documentation describes as efficient for atomic state picks. So the render behaviour of your whole application is determined by the selectors you write, and the failure mode is silent: a component that re-renders too often reports no error, it simply spends frames it should not. The usual cause is a selector that builds a fresh object or array on each call, because a new reference never equals the old one. Closing that gap is what the shallow helper is for.

Shallow comparison fixes object picks, and a custom comparator changes the store

Once you select more than one value, strict equality stops helping, because a selector that assembles an object or an array returns a new reference on every call and every render registers as a change. useShallow is the answer, and it covers three shapes: an object pick, an array pick, and a mapped pick, each re-rendering only when one of the picked values actually changes. Beyond shallow, the documentation is direct about the cost. A custom equality function is available, but the example that uses one requires createWithEqualityFn rather than the ordinary create. That makes a deep or domain-specific comparison not a per-call option you can add to a store that already exists, but a change to how the store is constructed. Consequence for a codebase: plan the creator at the point you define the store, because retrofitting it means replacing the import in every file that touches the store, not editing one call site.

TypeScript below 4.5 is rejected with a file that exists to fail

Most libraries degrade quietly when a compiler is too old. This one does not. The package manifest routes non-ESM type lookups to a single stub file whose name states its purpose, ts_version_4.5_and_above_is_required.d.ts, so a project on an older TypeScript receives a deliberate error on import rather than silently incorrect types. The typesVersions map splits on 4.5: at or above it, the esm entry points resolve to the real declarations, and below it, everything resolves to the stub. Two consequences follow. The compiler floor is stated in the type system rather than in a sentence of documentation, which is the more reliable place for it. And the manifest is marked private even though it configures main, types, a three-condition exports map and a files list, which is the shape of a package meant for publication, so the release cannot be a plain publish from this file and is worth confirming before you assume otherwise.

Reading and writing outside components is flagged for React Server Components

Beyond the hook there is a non-reactive escape hatch, because sometimes you need state without subscribing a component to it. The created hook carries utility functions on its prototype: getState for fresh state outside the render path, subscribe for a listener that fires synchronously on every change, and setState for writing, which triggers those listeners. It works, and it is easy to reach for. The documentation attaches a warning to it, though, scoped specifically to React Server Components, which it notes typically means Next.js 13 and above, saying the technique is not recommended there and can lead to unexpected bugs and privacy issues for your users, with a discussion linked for detail. The consequence is narrower than a general prohibition: module-scope getState and setState is fine in a client-rendered application and the wrong shape inside a server component, and the type system will not catch the difference for you.

Two lockfiles, and the shipped package is a matrix of build targets

The repository carries package-lock.json and pnpm-lock.yaml side by side, and every build script routes through pnpm. The published artifact is not one bundle either. The scripts list separate rollup builds for a base target, a vanilla target with no React, a react target, a middleware target, a middleware_immer target, and shallow variants of each, so the package ships several entry points rather than one. That structure is visible in the exports map, which carries three conditions, react-native, import and default, each pointing at its own types and runtime file, plus a wildcard subpath export for everything else. The manifest also sets sideEffects to false, which lets a bundler discard the pieces you never import. The consequence for a consumer is that most of what you install is not what you use, and the react-native condition means the same import specifier resolves to a different file on a phone than it does in a browser.

Main sits about six weeks past the last tag, and all three releases are patches

The three most recent releases are v5.0.13 on 2026-05-05, v5.0.14 on 2026-05-28 and v5.0.15 on 2026-08-13, while the last push to main was on 2026-09-22. Two things follow from those four dates. The distance between the last tag and the last commit is roughly six weeks, so main is not what the registry hands you and the two will differ in ways no version number describes. And every one of the three is a patch release inside the same major, which is the more useful signal for a library: the store API you are writing against has not moved in version terms, so a 5.0.x pin is a statement about the shape of the API rather than a promise of any particular feature. The repository also declares itself written for JavaScript users in its own warning, while being a TypeScript project, so type-level detail lives in a separate section.

Editorial conclusion

Adopt zustand when you want shared React state without a provider tree and you are disciplined about selectors, because the whole performance story lives in the selector you pass and the default no-selector form re-renders on every change. Do not adopt it expecting a full Redux replacement or a deep-comparison escape hatch: actions and data share one object, so the replace flag clears both, and a custom equality function requires a different store creator rather than a per-call option. Two things to check before you start. Decide on the store creator up front, since swapping to createWithEqualityFn later means touching every file that imports the store. And pin a 5.0.x version rather than tracking main, because the branch sits about six weeks ahead of the last tag.

Frequently asked questions

What is Zustand used for?

It is a small React state management library built on simplified flux principles with a hooks-based API. A store is itself a hook you can put anything into, primitives, objects and functions, and components select from it directly with no provider wrapping the application.

Is Zustand better than Redux?

The project lists its reasons as being simple and un-opinionated, making hooks the primary way of consuming state, not wrapping the app in context providers, and being able to inform components transiently without causing a render. Against plain context it claims less boilerplate, rendering components only on changes, and centralised action-based state management.

How do you use Zustand in Next.js?

Install the package, create a store, and select from it in components. The documentation specifically warns against adding state from outside components in React Server Components, which it notes typically means Next.js 13 and above, because the technique can lead to unexpected bugs and privacy issues for your users.

How do you use Zustand outside a component?

The created store hook carries utility functions on its prototype: getState for non-reactive fresh state, setState to write, and subscribe for a listener that fires synchronously on every change. To subscribe with a selector, add the subscribeWithSelector middleware, which extends subscribe to take a selector and a callback.

Official sources

  1. Official documentation
  2. Official README
  3. Project repository
  4. Release notes
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/pmndrs-zustand.svg)](https://hysenlabs.com/projects/pmndrs-zustand)