State management is a crucial aspect of modern application development. In React Native, managing state effectively can significantly enhance your app’s performance and maintainability. Zustand, a minimalistic state management library for React, provides an elegant and simple solution for handling state in React Native applications. In this blog, we'll explore Zustand, how it works, and why it might be the right choice for your React Native projects.
Here’s a quick comparison between Redux and Zustand for state management in React Native:
Redux:
- Popularity & Ecosystem: Redux is a widely used, mature solution with a large ecosystem of middleware (e.g., Redux Thunk, Redux Saga).
- Boilerplate: Known for requiring more boilerplate code (reducers, actions, etc.), but this can be simplified with modern tools like Redux Toolkit.
- Global State Management: Handles complex and large-scale applications well, especially when multiple slices of state need to interact.
- DevTools: Excellent developer experience with powerful debugging and time-travel capabilities via Redux DevTools.
-
Performance: Requires careful attention to avoid unnecessary re-renders and to optimize for performance (e.g.,
memoization
).
Zustand:
- Minimal & Lightweight: Much simpler and lighter than Redux with little to no boilerplate. State is managed via hooks directly.
- Ease of Use: Very easy to set up, making it perfect for small to medium projects or situations where simplicity is key.
- Less Opinionated: Offers more flexibility in how state is managed and updated, allowing for direct mutation of state.
- Performance: Optimized by default with automatic state splitting, reducing the need for memoization or other optimization techniques.
- React Native Friendly: Zustand works well with React Native and has a smaller bundle size compared to Redux.
When to Use Redux:
- Large, complex applications with a lot of state shared between components.
- Projects where middleware (like for async actions) or extensive DevTools support is needed.
When to Use Zustand:
- Small to medium applications where simplicity and performance are key.
- Projects that need quick setup without the boilerplate of Redux.
Both are good choices, but Redux shines in large applications, while Zustand is ideal for simpler, fast-moving projects.
What is Zustand?
Zustand is a small, fast, and scalable state management solution for React applications. Its name, derived from the German word for "state," reflects its primary function: managing state efficiently. Zustand stands out due to its simplicity and ease of use, allowing you to create state stores with minimal boilerplate code.
Key Features of Zustand:
- Minimal API: Zustand offers a simple API that makes managing state intuitive and straightforward.
- No Provider Component: Unlike other state management libraries, Zustand does not require a provider component, simplifying your component tree.
- Reactivity: Zustand integrates seamlessly with React’s built-in hooks, making it reactive and efficient.
- Middleware Support: Zustand supports middleware for enhanced functionality, such as persistence and logging.
Getting Started with Zustand
1. Installation
First, install Zustand in your React Native project. Open your terminal and run:
npm install zustand
or
yarn add zustand
2. Creating a Store
Zustand uses a store to manage state. A store is a JavaScript object that holds the state and provides methods to update it.
-
set
: Used to update the state. -
get
: Used to read the current state.
Here’s a simple example of creating and using a Zustand store:
useBasicStore.jsx
import { create } from 'zustand';
import axios from 'axios';
const BASE_URL = 'https://api.example.com'; // Replace with your API URL
// Create the store
const useBasicStore = create((set, get) => ({
items: [], // Initial state
// Action to fetch items from an API
fetchItems: async () => {
try {
const response = await axios.get(`${BASE_URL}/items`); // Fetching items
const data = response.data;
set({ items: data });
} catch (error) {
console.error('Failed to fetch items:', error);
}
},
// Action to add an item
addItem: (item) =>
set((state) => ({
items: [...state.items, item],
})),
// Action to remove an item
removeItem: (id) =>
set((state) => ({
items: state.items.filter((item) => item.id !== id),
})),
// Action to get the count of items
getItemCount: () => get().items.length,
// Optional: Clear all items
clearItems: () => set({ items: [] }),
// Optional: Update an item by id
updateItem: (id, updatedItem) =>
set((state) => ({
items: state.items.map((item) => (item.id === id ? updatedItem : item)),
})),
}));
export default useBasicStore;
Alternative API Call Method Using a Service:
itemStoreService.js
import axios from 'axios';
import useBasicStore from '../store/useBasicStore';
const BASE_URL = 'https://jsonplaceholder.typicode.com'; // Example URL
export const fetchItemsService = async (id) => {
try {
// Fetching items using a sample placeholder API (fetching a post as an example)
const response = await axios.post(`${BASE_URL}/posts`, { userId: id });
// Mock token and items data, adapt based on your actual response structure
const { data: items } = response;
// Update store with fetched items
const { addItem } = useBasicStore.getState();
addItem({ id: items?.id, name: Date.now() }); // Assuming `items` is an array, adapt if needed.
// Return the response data
return response.data;
} catch (error) {
console.error('Error fetching items:', error);
throw error;
}
};
Usage:
App.jsx
import React, { useEffect } from 'react';
import { View, Text, Button, FlatList, StyleSheet } from 'react-native';
import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';
import { fetchItemsService } from './services/itemStoreService';
import useBasicStore from './store/useBasicStore';
import { usePersistanceStore } from './store/usePersistanceStore';
const App = () => {
// Zustand store for basic state management
const { items, addItem, removeItem, getItemCount } = useBasicStore();
// Zustand persistent store for persisted data
const { fishes, addAFish, removeAFish } = usePersistanceStore();
useEffect(() => {
// Fetch data and update both stores
const fetchData = async () => {
await fetchItemsService();
};
fetchData();
}, []);
const handleAddItem = () => {
const newItem = { id: Date.now(), name: 'New Item' };
addItem(newItem); // Update basic store
addAFish(); // Update persistent store
};
const handleRemoveItem = (id) => {
removeItem(id); // Remove from basic store
removeAFish(); // Remove from persistent store
};
return (
<SafeAreaProvider>
<SafeAreaView style={styles.container}>
<Text style={styles.header}>Item List ({getItemCount()})</Text>
<Button title="Add Item and Fish" onPress={handleAddItem} />
<Text style={styles.subheader}>Fishes: {fishes}</Text>
<FlatList
data={items}
keyExtractor={(item, index) => index.toString()}
renderItem={({ item }) => (
<View style={styles.item}>
<Text>{item.name}</Text>
<Button title="Remove" onPress={() => handleRemoveItem(item.id)} />
</View>
)}
/>
</SafeAreaView>
</SafeAreaProvider>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 16,
backgroundColor: 'white',
},
header: {
fontSize: 24,
marginBottom: 16,
},
subheader: {
fontSize: 18,
marginVertical: 16,
},
item: {
flexDirection: 'row',
justifyContent: 'space-between',
padding: 16,
borderBottomWidth: 1,
borderBottomColor: '#ccc',
},
});
export default App;
Advanced Usage: Middleware
Zustand with MMKV/AsyncStorage
storage.js
(optional if MMKV not used)
import { MMKV } from 'react-native-mmkv';
export const storage = new MMKV({
id: 'my-app-storage',
encryptionKey: 'some_encryption_key',
});
export const mmkvStorage = {
setItem: (key, value) => storage.set(key, value),
getItem: (key) => storage.getString(key) ?? null,
removeItem: (key) => storage.delete(key),
};
usePersistanceStore.jsx
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
// import AsyncStorage from '@react-native-async-storage/async-storage';
import { mmkvStorage } from '../storage/storage'; // Using mmkvStorage for React Native
// Create the store
export const usePersistanceStore = create()(
persist(
(set, get) => ({
fishes: 0, // Initial state for fishes
// Action to add a fish
addAFish: () => set({ fishes: get().fishes + 1 }),
// Action to remove a fish
removeAFish: () => {
const currentFishes = get().fishes;
if (currentFishes > 0) {
set({ fishes: currentFishes - 1 });
}
},
// Action to reset fishes to 0
resetFishes: () => set({ fishes: 0 }),
}),
{
name: 'food-storage', // The key used for storage
storage: createJSONStorage(() => mmkvStorage), // You can switch between AsyncStorage or mmkvStorage
}
)
);
Best Practices
- Keep Stores Small: Maintain simplicity by keeping Zustand stores modular and focused on a single piece of state.
- Use Middleware Wisely: Only apply middleware when necessary to avoid unnecessary complexity and overhead.
-
Leverage React Native Hooks: Utilize
useEffect
anduseCallback
to manage side effects and optimize performance.
Conclusion
Zustand provides a minimalist and efficient approach to state management in React Native applications. Its simple API, reactivity, and middleware support make it an excellent choice for managing state in both small and large projects.
Top comments (0)