The short answer
Zustand and Redux are not competing on capability; they are competing on where the architecture comes from. Redux Toolkit ships opinions: actions, reducers, a single tree, devtools that narrate everything. Zustand ships almost nothing: a store is a function, a hook, and your good intentions. We picked Zustand, we would pick it again, and the reason is not "less boilerplate." It is that we would rather build exactly the discipline our app needs than pay ceremony tax on the ninety percent of state that never needed it. But that trade has a bill, and it arrives at scale. More below.
What 35 stores look like
The shape that emerged: one store per domain. A feed store, a connections store, an auth store, a notifications store, a subscription store, a screenshot-detection store, on down the list; today the directory holds around 35, on Zustand 5 with the immer middleware, under React Native 0.85 with the new architecture. Components subscribe with selectors so a feed scroll does not rerender the settings screen. Nothing exotic; the interesting parts are the seams between stores, which is where the next two sections live.
One number worth pausing on: 35 stores is not 35 silos. Logout has to tear all of them down. A friend removal has to ripple through half a dozen. A single decrypt result can be awaited by three callers at once. The library does none of this for you, which is the honest headline of the whole comparison.
What Zustand actually buys you
- Stores work outside React. The deciding feature for us, and the one comparisons underweight. A social app is mostly not a screen: WebSocket frames land, push notifications arrive, background tasks run, and all of them need to read and write state with no component mounted and no provider in reach. In Zustand that is a plain function call. In Redux it is dispatching from outside the tree, which works but always feels like visiting.
- No provider tree. React Native apps accumulate wrapper components like barnacles (gesture handlers, safe areas, navigation, theming). Removing state from that stack is one less thing that can order itself wrong.
- Per-store subscription granularity for free. With a store per domain, a rerender storm in one feature physically cannot originate in another store's updates. You can build the same isolation in Redux with careful selectors; here it is the default posture.
- The API fits in your head. `create`, `set`, `get`, subscribe. New code in the app looks like the old code because there is barely any surface to diverge on. With immer for the nested updates, an action reads like the change it makes.
The discipline problem (a war story)
Here is the part the "Zustand is simpler" posts skip. Early on, our auth store's logout handler needed every other store to reset. The obvious move in a library with no opinions: import them and call reset. To dodge circular imports, it dynamically imported them, seventeen stores deep at its worst. It worked, mostly. When it did not (an import that failed to resolve at runtime), logout partially succeeded, silently, which for an app whose whole promise is that signing out means signing out is about the worst sentence in this article.
The fix was not a library. It was accepting that at some scale, cross-cutting behavior needs an owner: we built a small lifecycle coordinator that stores register with (reset handlers, grouped in phases, errors contained per handler), an equivalent registry for reconciling per-friend state when the friend list changes, and a single chokepoint that deduplicates any work that must not run twice. Then we wrote an ESLint rule banning dynamic store-to-store imports so the anti-pattern stays dead.
Notice what happened: we rebuilt, deliberately and minimally, the kind of structure Redux would have imposed from day one. That is the real trade. Zustand does not remove the need for architecture; it defers it until you know which architecture you actually need. For us that deferral was the win: the coordination layer we built is a few hundred lines and fits our app exactly. But if nobody on the team has the instinct for when "just import the store" turns into a landmine, Redux's ceremony is not overhead, it is a guardrail.
Where Redux would have won
Honesty section. Three things we gave up, knowingly:
- The devtools narrative. Redux DevTools' action log and time travel are genuinely excellent, and Zustand's devtools story is thinner, especially across 35 stores. Our substitute is disciplined logging and small stores that are easy to reason about, which is a substitute, not an equal.
- Conventions that scale headcount. Redux patterns are documented, interviewable, and uniform across companies. Our coordination layer is well-documented internally, but it is ours; every new engineer learns it here. At a small team that costs a lunch conversation. At fifty engineers it would cost real money.
- A serializable audit trail. When every mutation is an action object, "what happened" has one answer. With 35 stores mutating directly, reconstructing a sequence of events takes more care. If your domain is the kind where you replay user sessions to debug (editors, canvases, undo-heavy tools), that alone can decide it.
So the honest Redux cases: a large or rotating team, a domain built on undo and replay, or an org that already thinks in Redux. None of those is us; one of them might be you.
Persistence, the part React Native makes interesting
Most comparisons end before the part that dominates mobile: what survives a restart. The stock answer is the persist middleware over MMKV, and for many apps it is the right one. We went explicit instead: each store decides which fields persist and writes them to MMKV itself, and every MMKV instance holding user-scoped data is registered with the session lifecycle so logout wipes it and then verifies the wipe, loudly failing rather than quietly leaking. Blanket persistence is a great default until the first time you hold something that must not outlive the session; then per-field intent stops being pedantry. Even if you never adopt the explicit style, know which of your persisted fields you could defend persisting.
The verdict
Pick Zustand when the team is small enough to hold conventions, the app has real out-of-React behavior, and you would rather grow architecture than inherit it. Pick Redux Toolkit when the team is large, the devtools narrative is load-bearing, or your domain wants an action log by nature. Both scale; what differs is whether the structure comes pre-installed or gets built where it is needed. We are nine months, two app stores, and 35 stores into the second path; the seventeen-import logout is a story we tell instead of a bug we have, and the state layer is one of the boring parts of the codebase now. Boring is the goal.
Common questions
Is Zustand better than Redux? Wrong axis. Zustand defers architecture to you; Redux front-loads it. Team size and domain decide which pricing you want.
Does Zustand scale? To 35 stores in our shipping app, yes, but the scaling agent was our own coordination rules, not the library.
Can Zustand work outside components? Yes, first-class, and for apps with sockets and push it is the deciding feature.
How should React Native apps persist Zustand state? persist middleware over MMKV for the common case; explicit per-field persistence once anything sensitive or session-scoped is involved.
Should you migrate from Redux? A healthy Redux app: no. A small team drowning in ceremony: plausibly. The migration buys ergonomics, not capability.
boopr is a free private app with no ads and no algorithm, built by a very small team that writes up what it learns. It is on the App Store and Google Play.
Back to Blog