As React Native apps grow, managing state becomes increasingly complicated—especially when handling authentication, UI state, API caching, theme switching, and user data. Redux is powerful but can feel heavy. Context is simple but struggles with large-scale apps.
Zustand is the perfect middle-ground: lightweight, fast, scalable, and extremely flexible.
In this tutorial, you’ll learn how to:
This tutorial follows the same clean architecture used in modern production apps.
Zustand offers:
Example of the simplicity:
import { create } from 'zustand';
const useStore = create(set => ({ count: 0, increment: () => set(state => ({ count: state.count + 1 })) }));
npm install zustand zustand/middleware
You’ll also need AsyncStorage for persistence:
npx expo install @react-native-async-storage/async-storage
Create a folder structure:
src/
store/
auth/
auth.store.js
ui/
ui.store.js
user/
user.store.js
index.js
Each store is modular and isolated.
src/store/auth/auth.store.js:
import { create } from 'zustand'; import { persist } from 'zustand/middleware'; import AsyncStorage from '@react-native-async-storage/async-storage';export const useAuthStore = create( persist( (set, get) => ({ token: null, user: null, isLoggedIn: false,
login: (token, user) => { set({ token, user, isLoggedIn: true }); }, logout: () => { set({ token: null, user: null, isLoggedIn: false }); }, }), { name: 'auth-storage', // AsyncStorage key storage: AsyncStorage, }
) );
This persists the user session even after app restart.
Useful for theme, modals, drawer state, etc.
src/store/ui/ui.store.js:
import { create } from 'zustand';export const useUIStore = create(set => ({ theme: 'light', loading: false,
setTheme: theme => set({ theme }), setLoading: value => set({ loading: value }), }));
Works beautifully with React Query.
src/store/user/user.store.js:
import { create } from 'zustand';export const useUserStore = create(set => ({ profile: null,
setProfile: profile => set({ profile }), }));
Then in your React Query service:
const { setProfile } = useUserStore.getState();
useQuery(['profile'], getProfile, { onSuccess: data => setProfile(data), });
This avoids prop drilling.
src/store/index.js:
export * from './auth/auth.store';
export * from './ui/ui.store';
export * from './user/user.store';
Now you can import from one place.
Zustand’s secret weapon: selectors.
const username = useAuthStore(state => state.user?.name);
This re-renders only when the selected field changes.
const { login } = useAuthStore.getState();
login(token, user);
This does not trigger React re-renders — super powerful for services.
Zustand stores global/cacheable state. React Query handles server state.
Example pattern:
const { setProfile } = useUserStore.getState();
const { data, isLoading } = useQuery(['profile'], getProfile, { onSuccess: setProfile, });
Best of both worlds.
const HomeScreen = () => { const user = useAuthStore(state => state.user); const theme = useUIStore(state => state.theme);
return ( <View> <Text>Welcome, {user?.name}</Text> <Text>Current theme: {theme}</Text> </View> ); };
Large apps benefit from splitting state + actions:
store/auth/
state.js
actions.js
index.js
Zustand is one of the cleanest and most scalable global state solutions for React Native. With modular slices, selectors, persistence, and React Query integration, you get a powerful system that stays maintainable as your app grows.
If you'd like, I can also generate tutorials for: