Zustand vs Redux in React Native: Why I Use 7 Zustand Stores in Production
March 2026
Zustand wins for React Native. After building multiple production apps with both Redux and Zustand, I switched to Zustand full-time and never looked back. It is simpler, faster to set up, and does not need middleware boilerplate to do things that should be easy. Ship React Native uses 7 Zustand stores in production -- Auth, Premium, Settings, Credits, Onboarding, UI, and AI -- and the total state management code is a fraction of what it would be with Redux.
This post is a direct comparison based on real production experience. I will show you the actual store patterns I use, explain where Redux still makes sense, and give you enough to decide which one fits your project.
Table of Contents
Why Zustand Won Me Over
I used Redux for about three years across several React Native projects. It works. It is battle-tested. But every time I started a new project, I dreaded the setup. Create a store. Create slices. Create action types. Create action creators. Create reducers. Wire up middleware for async logic. Configure the provider. Set up Redux DevTools. Before I wrote a single line of business logic, I had 8-10 files and a couple hundred lines of boilerplate.
Zustand replaced all of that with a single function call. The first time I set up a Zustand store, I thought I was missing something. I wrote about 15 lines of code, and the store was working -- including async actions, TypeScript types, and selectors. No provider wrapping the app. No dispatching actions. No connect or useSelector boilerplate.
The performance characteristics also work better for React Native. Zustand re-renders only the components that subscribe to the specific slice of state they use. You do not need React.memo or reselect to avoid unnecessary re-renders. For mobile apps where every frame counts, this matters.
Zustand vs Redux: Full Comparison
Here is a side-by-side comparison based on real usage in React Native projects:
| Category | Zustand | Redux (with RTK) |
|---|---|---|
| Bundle size | ~1.1 kB gzipped | ~11 kB gzipped (RTK + React-Redux) |
| Boilerplate | Minimal. One file per store, no actions/reducers split | Moderate even with RTK. Slices help but you still need a store config, provider, and typed hooks |
| Middleware | Built-in: persist, devtools, immer, subscribeWithSelector. Compose with simple function wrapping | redux-thunk included with RTK. Anything else (saga, observable) requires extra packages and setup |
| DevTools | React Native Debugger support via devtools middleware. Less polished than Redux DevTools but functional | Excellent. Redux DevTools is the gold standard for state inspection, time travel, and action replay |
| Learning curve | Very low. If you understand useState, you can use Zustand in 10 minutes | Moderate to high. Actions, reducers, dispatch, selectors, middleware concepts all need to be learned |
| TypeScript support | First-class. Store types are inferred from the create function. Minimal generics needed | Good with RTK, but you need to manually type RootState, AppDispatch, and typed hook wrappers |
| Async actions | Just write async functions inside the store. No middleware required | Requires createAsyncThunk or middleware like redux-saga. Extra abstraction layer |
| Persist / Storage | zustand/middleware persist works with AsyncStorage out of the box. One line to add | redux-persist is a separate package with its own configuration, transforms, and migration system |
| React Native specific | No provider needed. Works with any navigation library without context issues. MMKV adapter available | Provider must wrap the entire app. Can cause issues with navigation stacks if not configured correctly |
| Ecosystem | Smaller but growing. Covers 95% of common use cases | Massive. RTK Query, redux-saga, redux-observable, thousands of middleware packages |
The bundle size difference matters more than people think on mobile. Every kilobyte affects startup time, especially on lower-end Android devices. Zustand being roughly 10x smaller than the Redux stack is a real advantage when you are trying to keep your app under performance budgets.
Real Store Examples from Ship React Native
These are simplified versions of actual stores running in production. I have removed some business-specific logic but kept the structure and patterns intact.
Auth Store
The auth store manages the current user session, login state, and user profile data:
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';
interface User {
id: string;
email: string;
displayName: string;
avatarUrl: string | null;
}
interface AuthState {
user: User | null;
isAuthenticated: boolean;
isLoading: boolean;
hasCompletedOnboarding: boolean;
setUser: (user: User | null) => void;
setLoading: (loading: boolean) => void;
setOnboardingComplete: () => void;
logout: () => void;
}
export const useAuthStore = create<AuthState>()(
persist(
(set) => ({
user: null,
isAuthenticated: false,
isLoading: true,
hasCompletedOnboarding: false,
setUser: (user) =>
set({ user, isAuthenticated: !!user, isLoading: false }),
setLoading: (isLoading) => set({ isLoading }),
setOnboardingComplete: () => set({ hasCompletedOnboarding: true }),
logout: () =>
set({
user: null,
isAuthenticated: false,
hasCompletedOnboarding: false,
}),
}),
{
name: 'auth-storage',
storage: createJSONStorage(() => AsyncStorage),
partialize: (state) => ({
user: state.user,
isAuthenticated: state.isAuthenticated,
hasCompletedOnboarding: state.hasCompletedOnboarding,
}),
}
)
);
A few things to notice. The store is a single file. Actions and state live together. The partialize function controls exactly what gets persisted -- I do not persist isLoading because that is a transient UI state. Using the store in a component is one line:
const { user, isAuthenticated } = useAuthStore();
No useSelector. No useDispatch. No mapStateToProps. Just destructure what you need.
Premium / Subscription Store
This store tracks the user's subscription status. It works alongside RevenueCat for subscription management:
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';
type PlanType = 'free' | 'starter' | 'pro' | 'lifetime';
interface PremiumState {
isPremium: boolean;
plan: PlanType;
expirationDate: string | null;
isTrialActive: boolean;
setPremiumStatus: (isPremium: boolean, plan: PlanType) => void;
setExpiration: (date: string | null) => void;
setTrialActive: (active: boolean) => void;
reset: () => void;
}
export const usePremiumStore = create<PremiumState>()(
persist(
(set) => ({
isPremium: false,
plan: 'free',
expirationDate: null,
isTrialActive: false,
setPremiumStatus: (isPremium, plan) => set({ isPremium, plan }),
setExpiration: (expirationDate) => set({ expirationDate }),
setTrialActive: (isTrialActive) => set({ isTrialActive }),
reset: () =>
set({
isPremium: false,
plan: 'free',
expirationDate: null,
isTrialActive: false,
}),
}),
{
name: 'premium-storage',
storage: createJSONStorage(() => AsyncStorage),
}
)
);
In my components, checking premium status is trivial:
const { isPremium, plan } = usePremiumStore();
if (!isPremium) {
return <PaywallScreen />;
}
Compare this to Redux, where you would need a selector, possibly a memoized selector if you are combining multiple fields, and you would dispatch an action through a thunk to update subscription status. Zustand collapses all of that into direct function calls.
Settings Store
The settings store handles user preferences like theme, notifications, and language:
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';
type Theme = 'light' | 'dark' | 'system';
interface SettingsState {
theme: Theme;
notificationsEnabled: boolean;
hapticFeedback: boolean;
language: string;
setTheme: (theme: Theme) => void;
toggleNotifications: () => void;
toggleHapticFeedback: () => void;
setLanguage: (language: string) => void;
}
export const useSettingsStore = create<SettingsState>()(
persist(
(set) => ({
theme: 'system',
notificationsEnabled: true,
hapticFeedback: true,
language: 'en',
setTheme: (theme) => set({ theme }),
toggleNotifications: () =>
set((state) => ({
notificationsEnabled: !state.notificationsEnabled,
})),
toggleHapticFeedback: () =>
set((state) => ({ hapticFeedback: !state.hapticFeedback })),
setLanguage: (language) => set({ language }),
}),
{
name: 'settings-storage',
storage: createJSONStorage(() => AsyncStorage),
}
)
);
All three stores follow the same pattern. Once you understand one, you understand all of them. That consistency is one of Zustand's biggest practical advantages -- every developer on your team writes stores the same way because there is really only one way to write them.
AsyncStorage Persistence Pattern
Persistence is where Zustand really shines for React Native. With Redux, you install redux-persist, configure a persistor, wrap your app in a PersistGate, set up transforms if you need to migrate state, and handle rehydration events. It works but it is a lot of ceremony.
With Zustand, the persist middleware handles everything. Here is the pattern I use across all stores:
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';
export const useAnyStore = create<YourStateType>()(
persist(
(set, get) => ({
// your state and actions
}),
{
name: 'unique-storage-key',
storage: createJSONStorage(() => AsyncStorage),
partialize: (state) => ({
// only persist what you need
}),
version: 1,
migrate: (persistedState, version) => {
// handle state migrations between versions
return persistedState as YourStateType;
},
}
)
);
Key things I have learned about persistence in production:
Always use partialize. Do not persist everything. Loading states, error messages, temporary UI flags -- keep those out of storage. Persisting too much causes stale state bugs that are painful to debug.
Use version numbers from day one. When you need to change the shape of your persisted state (and you will), the version and migrate options let you handle that cleanly. Without versioning, users who update your app will get crashes from state shape mismatches.
Consider MMKV for performance-critical stores. AsyncStorage is fine for most cases, but if you are persisting state that reads on every app launch, MMKV is significantly faster. Zustand works with MMKV through a simple adapter:
import { MMKV } from 'react-native-mmkv';
const mmkvStorage = new MMKV();
const zustandMMKVStorage = {
getItem: (name: string) => {
const value = mmkvStorage.getString(name);
return value ?? null;
},
setItem: (name: string, value: string) => {
mmkvStorage.set(name, value);
},
removeItem: (name: string) => {
mmkvStorage.delete(name);
},
};
// Then use it in your store:
// storage: createJSONStorage(() => zustandMMKVStorage),
That is the entire MMKV integration. No extra packages. No configuration files. Just swap the storage adapter.
When Redux Is Still the Better Choice
I am not here to tell you Redux is bad. It is not. There are specific situations where Redux is the better tool:
Very large teams (15+ developers). Redux's strict patterns -- actions, reducers, immutable updates -- create a forced consistency that helps when many developers touch the same state. Zustand gives you more freedom, which is great for small teams but can lead to inconsistent patterns at scale.
Complex middleware chains. If your app needs saga-style orchestration of side effects, or you have complex event-driven workflows where one action triggers a cascade of other actions across different state slices, Redux middleware is more mature and better documented for those patterns.
Existing Redux codebase. If you have a working app built on Redux, do not rewrite it. The migration cost is almost never worth it. Use Zustand for new features if you want, but ripping out working Redux code to replace it with Zustand is a waste of time.
RTK Query dependency. If you rely heavily on RTK Query for server state management with caching, invalidation, and polling, you are already locked into the Redux ecosystem. RTK Query is excellent and there is no direct Zustand equivalent -- though I use Convex for server state and Zustand for client state, which is a different approach entirely.
Compliance or audit requirements. Some industries require complete audit trails of every state change. Redux's action log is purpose-built for this. Every state mutation goes through a dispatched action that gets logged. Zustand mutations are direct function calls that are harder to trace without additional tooling.
This Is Not for You If...
Zustand is not the right choice for every project. Skip it and use something else if:
- You need a full-featured server state cache with automatic invalidation. Use TanStack Query or Convex instead. Zustand is for client state.
- Your team has deep Redux expertise and a large existing Redux codebase. Switching tools for the sake of switching is not engineering, it is fashion.
- You want opinionated structure enforced by the library. Zustand intentionally gives you freedom. If you want the framework to prevent junior developers from making bad state management decisions, Redux's stricter patterns help with that.
- You are building a web-only app with very complex state interactions. Redux's ecosystem is larger, and server-side rendering with Redux is more battle-tested than with Zustand (though this does not apply to React Native).
For most React Native projects -- especially ones built by solo developers or small teams shipping fast -- Zustand is the better choice. It lets you focus on your app instead of your state management infrastructure.
Ship React Native and Zustand
Ship React Native ships with all 7 Zustand stores pre-configured and ready to use. The Auth store integrates with the authentication flow. The Premium store connects to RevenueCat for subscription management. The Settings store persists user preferences. The Credits store tracks usage-based features. The Onboarding store manages first-launch flows. The UI store handles modals, toasts, and sheet states. The AI store manages conversation state and streaming responses.
Every store uses the persistence pattern described above, with proper TypeScript types, version numbers for migrations, and partialize to keep persisted state clean. You do not have to figure out the patterns yourself -- they are already working in production.
If you are starting a new React Native app and want state management that works without fighting the framework, take a look at shipreactnative.com. The Zustand setup alone saves you a few days of boilerplate and pattern decisions.
Related posts:
- Convex for React Native -- server state that complements Zustand's client state
- Expo Router in 2026 -- navigation that works alongside Zustand stores
- Best React Native Starter Kits -- templates with state management built in