Zustand in React Native: A Practical Guide

Learn how to effectively implement Zustand for state management in your React Native and Expo applications.

Mobile Apps
5 min read

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.

Why Zustand for React Native?#

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}))

Real-World Patterns#

Let's explore some common patterns that demonstrate Zustand's power in React Native:

1. Persistent Storage#

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)

2. Complex State Management#

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}))

Best Practices for Success#

After using Zustand in React Native production apps, here are key practices that will serve you well:

1. Handle Loading States#

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}))

2. Error Management#

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}))

3. Navigation Integration#

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}

Performance Tips#

  1. 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)
  1. 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}))

The Bottom Line#

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.