zustand-mobile-patterns
Zustand state management for React Native. Use when implementing client-side state.
When & Why to Use This Skill
This Claude skill provides comprehensive patterns and best practices for implementing Zustand state management in React Native applications. It streamlines mobile development by offering boilerplate and architectural guidance for global state, authentication flows, and persistent storage, ensuring lightweight and high-performance React Native apps.
Use Cases
- Implementing secure authentication flows using Zustand integrated with Expo SecureStore for sensitive data handling.
- Managing complex shopping cart logic in e-commerce apps with real-time calculations and item management.
- Creating persistent user preference systems (theme, language, notifications) using Zustand middleware and AsyncStorage.
- Optimizing mobile app performance by utilizing precise state selectors to minimize unnecessary component re-renders.
- Structuring scalable client-side state architecture that works seamlessly with server-state libraries like React Query.
| name | zustand-mobile-patterns |
|---|---|
| description | Zustand state management for React Native. Use when implementing client-side state. |
Zustand Mobile Patterns Skill
This skill covers Zustand state management for React Native apps.
When to Use
Use this skill when:
- Managing global client state
- Authentication state
- User preferences/settings
- Shopping cart
- Theme management
Core Principle
SIMPLE AND MINIMAL - Zustand is lightweight. Keep stores focused.
Installation
npm install zustand
Basic Store
import { create } from 'zustand';
interface CounterState {
count: number;
increment: () => void;
decrement: () => void;
reset: () => void;
}
export const useCounterStore = create<CounterState>((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
}));
// Usage
function Counter(): React.ReactElement {
const count = useCounterStore((state) => state.count);
const increment = useCounterStore((state) => state.increment);
return (
<TouchableOpacity onPress={increment}>
<Text>Count: {count}</Text>
</TouchableOpacity>
);
}
Authentication Store
import { create } from 'zustand';
import * as SecureStore from 'expo-secure-store';
interface User {
id: string;
email: string;
name: string;
}
interface AuthState {
user: User | null;
token: string | null;
isAuthenticated: boolean;
isLoading: boolean;
login: (email: string, password: string) => Promise<void>;
logout: () => Promise<void>;
loadStoredAuth: () => Promise<void>;
}
export const useAuthStore = create<AuthState>((set, get) => ({
user: null,
token: null,
isAuthenticated: false,
isLoading: true,
login: async (email, password) => {
set({ isLoading: true });
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
if (!response.ok) throw new Error('Login failed');
const { user, token } = await response.json();
await SecureStore.setItemAsync('authToken', token);
await SecureStore.setItemAsync('user', JSON.stringify(user));
set({ user, token, isAuthenticated: true, isLoading: false });
} catch (error) {
set({ isLoading: false });
throw error;
}
},
logout: async () => {
await SecureStore.deleteItemAsync('authToken');
await SecureStore.deleteItemAsync('user');
set({ user: null, token: null, isAuthenticated: false });
},
loadStoredAuth: async () => {
try {
const token = await SecureStore.getItemAsync('authToken');
const userStr = await SecureStore.getItemAsync('user');
if (token && userStr) {
const user = JSON.parse(userStr);
set({ user, token, isAuthenticated: true, isLoading: false });
} else {
set({ isLoading: false });
}
} catch {
set({ isLoading: false });
}
},
}));
Settings Store with Persistence
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';
interface SettingsState {
theme: 'light' | 'dark' | 'system';
notifications: boolean;
language: string;
setTheme: (theme: 'light' | 'dark' | 'system') => void;
setNotifications: (enabled: boolean) => void;
setLanguage: (language: string) => void;
}
export const useSettingsStore = create<SettingsState>()(
persist(
(set) => ({
theme: 'system',
notifications: true,
language: 'en',
setTheme: (theme) => set({ theme }),
setNotifications: (notifications) => set({ notifications }),
setLanguage: (language) => set({ language }),
}),
{
name: 'settings-storage',
storage: createJSONStorage(() => AsyncStorage),
}
)
);
Cart Store
interface CartItem {
id: string;
name: string;
price: number;
quantity: number;
}
interface CartState {
items: CartItem[];
addItem: (item: Omit<CartItem, 'quantity'>) => void;
removeItem: (id: string) => void;
updateQuantity: (id: string, quantity: number) => void;
clearCart: () => void;
total: () => number;
}
export const useCartStore = create<CartState>((set, get) => ({
items: [],
addItem: (item) =>
set((state) => {
const existing = state.items.find((i) => i.id === item.id);
if (existing) {
return {
items: state.items.map((i) =>
i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
),
};
}
return { items: [...state.items, { ...item, quantity: 1 }] };
}),
removeItem: (id) =>
set((state) => ({
items: state.items.filter((item) => item.id !== id),
})),
updateQuantity: (id, quantity) =>
set((state) => ({
items:
quantity <= 0
? state.items.filter((item) => item.id !== id)
: state.items.map((item) =>
item.id === id ? { ...item, quantity } : item
),
})),
clearCart: () => set({ items: [] }),
total: () => {
const state = get();
return state.items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
},
}));
Selectors for Performance
// ❌ Bad: Subscribes to entire store
function Component(): React.ReactElement {
const store = useCartStore();
return <Text>{store.items.length}</Text>;
}
// ✅ Good: Only subscribes to items.length
function Component(): React.ReactElement {
const itemCount = useCartStore((state) => state.items.length);
return <Text>{itemCount}</Text>;
}
// ✅ Good: Multiple selectors
function CartSummary(): React.ReactElement {
const itemCount = useCartStore((state) => state.items.length);
const total = useCartStore((state) => state.total());
return (
<View>
<Text>Items: {itemCount}</Text>
<Text>Total: ${total.toFixed(2)}</Text>
</View>
);
}
Combining with React Query
// Zustand for client state
const useAuthStore = create<AuthState>(...);
// React Query for server state
function useUser() {
const token = useAuthStore((state) => state.token);
return useQuery({
queryKey: ['user'],
queryFn: () => fetchUser(token),
enabled: !!token,
});
}
DevTools (Development)
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
export const useStore = create<StoreState>()(
devtools(
(set) => ({
// state and actions
}),
{ name: 'MyStore' }
)
);
Testing Stores
import { useAuthStore } from '../authStore';
describe('authStore', () => {
beforeEach(() => {
// Reset store before each test
useAuthStore.setState({
user: null,
token: null,
isAuthenticated: false,
isLoading: false,
});
});
it('logs out correctly', () => {
useAuthStore.setState({
user: { id: '1', email: 'test@test.com', name: 'Test' },
isAuthenticated: true,
});
useAuthStore.getState().logout();
expect(useAuthStore.getState().user).toBeNull();
expect(useAuthStore.getState().isAuthenticated).toBe(false);
});
});
Notes
- Use selectors to prevent unnecessary re-renders
- Persist only necessary state
- Use SecureStore for sensitive data
- Keep stores focused on single concerns
- Zustand works outside React components
- Compatible with React 18 concurrent features