Let's explore how Zustand, the lightweight state management library, can transform how you handle state in your React Native applications. If you're tired of complex state management solutions or looking for a more efficient approach, you're in for a treat.
Think about the typical challenges in mobile state management: handling authentication, managing shopping carts, dealing with app settings, and synchronizing data across screens. Zustand addresses these with an elegantly simple approach that feels right at home in React Native.
Here's a simple example that demonstrates Zustand's elegance:
1import create from 'zustand'
2
3interface AuthStore {
4 user: User | null
5 isLoading: boolean
6 login: (username: string, password: string) => Promise<void>
7 logout: () => void
8}
9
10const useAuthStore = create<AuthStore>((set) => ({
11 user: null,
12 isLoading: false,
13 login: async (username, password) => {
14 set({ isLoading: true })
15 try {
16 const user = await api.login(username, password)
17 set({ user, isLoading: false })
18 } catch (error) {
19 set({ isLoading: false })
20 throw error
21 }
22 },
23 logout: () => set({ user: null })
24}))
1import create from 'zustand'
2
3interface AuthStore {
4 user: User | null
5 isLoading: boolean
6 login: (username: string, password: string) => Promise<void>
7 logout: () => void
8}
9
10const useAuthStore = create<AuthStore>((set) => ({
11 user: null,
12 isLoading: false,
13 login: async (username, password) => {
14 set({ isLoading: true })
15 try {
16 const user = await api.login(username, password)
17 set({ user, isLoading: false })
18 } catch (error) {
19 set({ isLoading: false })
20 throw error
21 }
22 },
23 logout: () => set({ user: null })
24}))
Let's explore some common patterns that demonstrate Zustand's power in React Native:
1import create from 'zustand'
2import { persist, createJSONStorage } from 'zustand/middleware'
3import AsyncStorage from '@react-native-async-storage/async-storage'
4
5interface SettingsStore {
6 theme: 'light' | 'dark'
7 language: string
8 toggleTheme: () => void
9 setLanguage: (lang: string) => void
10}
11
12const useSettingsStore = create(
13 persist<SettingsStore>(
14 (set) => ({
15 theme: 'light',
16 language: 'en',
17 toggleTheme: () =>
18 set((state) => ({
19 theme: state.theme === 'light' ? 'dark' : 'light'
20 })),
21 setLanguage: (language) => set({ language })
22 }),
23 {
24 name: 'settings-storage',
25 storage: createJSONStorage(() => AsyncStorage)
26 }
27 )
28)
1import create from 'zustand'
2import { persist, createJSONStorage } from 'zustand/middleware'
3import AsyncStorage from '@react-native-async-storage/async-storage'
4
5interface SettingsStore {
6 theme: 'light' | 'dark'
7 language: string
8 toggleTheme: () => void
9 setLanguage: (lang: string) => void
10}
11
12const useSettingsStore = create(
13 persist<SettingsStore>(
14 (set) => ({
15 theme: 'light',
16 language: 'en',
17 toggleTheme: () =>
18 set((state) => ({
19 theme: state.theme === 'light' ? 'dark' : 'light'
20 })),
21 setLanguage: (language) => set({ language })
22 }),
23 {
24 name: 'settings-storage',
25 storage: createJSONStorage(() => AsyncStorage)
26 }
27 )
28)
1interface CartItem {
2 id: string
3 name: string
4 price: number
5 quantity: number
6}
7
8interface CartStore {
9 items: CartItem[]
10 total: number
11 addItem: (item: CartItem) => void
12 removeItem: (itemId: string) => void
13 updateQuantity: (itemId: string, quantity: number) => void
14 clearCart: () => void
15}
16
17const useCartStore = create<CartStore>((set) => ({
18 items: [],
19 total: 0,
20 addItem: (item) => set((state) => {
21 const existingItem = state.items.find(i => i.id === item.id)
22
23 if (existingItem) {
24 return {
25 items: state.items.map(i =>
26 i.id === item.id
27 ? { ...i, quantity: i.quantity + 1 }
28 : i
29 ),
30 total: state.total + item.price
31 }
32 }
33
34 return {
35 items: [...state.items, { ...item, quantity: 1 }],
36 total: state.total + item.price
37 }
38 }),
39 removeItem: (itemId) => set((state) => {
40 const item = state.items.find(i => i.id === itemId)
41 return {
42 items: state.items.filter(i => i.id !== itemId),
43 total: state.total - (item ? item.price * item.quantity : 0)
44 }
45 }),
46 updateQuantity: (itemId, quantity) => set((state) => {
47 const item = state.items.find(i => i.id === itemId)
48 if (!item) return state
49
50 const quantityDiff = quantity - item.quantity
51 return {
52 items: state.items.map(i =>
53 i.id === itemId ? { ...i, quantity } : i
54 ),
55 total: state.total + (item.price * quantityDiff)
56 }
57 }),
58 clearCart: () => set({ items: [], total: 0 })
59}))
1interface CartItem {
2 id: string
3 name: string
4 price: number
5 quantity: number
6}
7
8interface CartStore {
9 items: CartItem[]
10 total: number
11 addItem: (item: CartItem) => void
12 removeItem: (itemId: string) => void
13 updateQuantity: (itemId: string, quantity: number) => void
14 clearCart: () => void
15}
16
17const useCartStore = create<CartStore>((set) => ({
18 items: [],
19 total: 0,
20 addItem: (item) => set((state) => {
21 const existingItem = state.items.find(i => i.id === item.id)
22
23 if (existingItem) {
24 return {
25 items: state.items.map(i =>
26 i.id === item.id
27 ? { ...i, quantity: i.quantity + 1 }
28 : i
29 ),
30 total: state.total + item.price
31 }
32 }
33
34 return {
35 items: [...state.items, { ...item, quantity: 1 }],
36 total: state.total + item.price
37 }
38 }),
39 removeItem: (itemId) => set((state) => {
40 const item = state.items.find(i => i.id === itemId)
41 return {
42 items: state.items.filter(i => i.id !== itemId),
43 total: state.total - (item ? item.price * item.quantity : 0)
44 }
45 }),
46 updateQuantity: (itemId, quantity) => set((state) => {
47 const item = state.items.find(i => i.id === itemId)
48 if (!item) return state
49
50 const quantityDiff = quantity - item.quantity
51 return {
52 items: state.items.map(i =>
53 i.id === itemId ? { ...i, quantity } : i
54 ),
55 total: state.total + (item.price * quantityDiff)
56 }
57 }),
58 clearCart: () => set({ items: [], total: 0 })
59}))
After using Zustand in React Native production apps, here are key practices that will serve you well:
1interface LoadingState {
2 [key: string]: boolean
3}
4
5interface Store {
6 loading: LoadingState
7 setLoading: (key: string, value: boolean) => void
8}
9
10const useStore = create<Store>((set) => ({
11 loading: {},
12 setLoading: (key, value) =>
13 set((state) => ({
14 loading: { ...state.loading, [key]: value }
15 }))
16}))
1interface LoadingState {
2 [key: string]: boolean
3}
4
5interface Store {
6 loading: LoadingState
7 setLoading: (key: string, value: boolean) => void
8}
9
10const useStore = create<Store>((set) => ({
11 loading: {},
12 setLoading: (key, value) =>
13 set((state) => ({
14 loading: { ...state.loading, [key]: value }
15 }))
16}))
1interface ErrorState {
2 message: string | null
3 setError: (message: string | null) => void
4}
5
6const useErrorStore = create<ErrorState>((set) => ({
7 message: null,
8 setError: (message) => set({ message })
9}))
1interface ErrorState {
2 message: string | null
3 setError: (message: string | null) => void
4}
5
6const useErrorStore = create<ErrorState>((set) => ({
7 message: null,
8 setError: (message) => set({ message })
9}))
1function App() {
2 const user = useAuthStore(state => state.user)
3
4 return (
5 <NavigationContainer>
6 <Stack.Navigator>
7 {user ? (
8 // Authenticated stack
9 <>
10 <Stack.Screen name="Home" component={HomeScreen} />
11 <Stack.Screen name="Profile" component={ProfileScreen} />
12 </>
13 ) : (
14 // Auth stack
15 <>
16 <Stack.Screen name="Login" component={LoginScreen} />
17 <Stack.Screen name="Register" component={RegisterScreen} />
18 </>
19 )}
20 </Stack.Navigator>
21 </NavigationContainer>
22 )
23}
1function App() {
2 const user = useAuthStore(state => state.user)
3
4 return (
5 <NavigationContainer>
6 <Stack.Navigator>
7 {user ? (
8 // Authenticated stack
9 <>
10 <Stack.Screen name="Home" component={HomeScreen} />
11 <Stack.Screen name="Profile" component={ProfileScreen} />
12 </>
13 ) : (
14 // Auth stack
15 <>
16 <Stack.Screen name="Login" component={LoginScreen} />
17 <Stack.Screen name="Register" component={RegisterScreen} />
18 </>
19 )}
20 </Stack.Navigator>
21 </NavigationContainer>
22 )
23}
- Use Shallow Equality
1const { width, height } = useStore(
2 state => ({
3 width: state.dimensions.width,
4 height: state.dimensions.height
5 }),
6 shallow
7)
1const { width, height } = useStore(
2 state => ({
3 width: state.dimensions.width,
4 height: state.dimensions.height
5 }),
6 shallow
7)
- Batch Updates
1const useStore = create((set) => ({
2 batchUpdate: (updates) => set((state) => ({
3 ...state,
4 ...updates
5 }))
6}))
1const useStore = create((set) => ({
2 batchUpdate: (updates) => set((state) => ({
3 ...state,
4 ...updates
5 }))
6}))
Zustand proves to be an excellent choice for React Native applications due to its:
- Simplicity and ease of use
- Small bundle size
- Great performance characteristics
- Easy integration with AsyncStorage
- TypeScript support
When building mobile applications, the lightweight nature of Zustand combined with its powerful features makes it a compelling choice for state management. Its straightforward API and minimal boilerplate allow developers to focus on building features rather than managing complex state architectures.
Remember: The best state management solution is the one that helps your team build and maintain your application effectively. Zustand's simplicity and power make it an excellent choice for most React Native applications.