Simplifying State Management in React Native with Zustand

Ajmal Hasan - Aug 30 - - Dev Community

Image description

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
Enter fullscreen mode Exit fullscreen mode

or

yarn add zustand
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
  }
};
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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),
};
Enter fullscreen mode Exit fullscreen mode

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
    }
  )
);
Enter fullscreen mode Exit fullscreen mode

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 and useCallback 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.

FULL CODE 🧑‍💻

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .