React Native course ยท Module 4: State and API data

Zustand - Lightweight Store

3 min read ยท
In this lesson6

Zustand is a minimalist state management library - as simple as useState, but global like Redux!

Installation

npx expo install zustand

Basic Store

// stores/treasureStore.ts
import { create } from 'zustand';

interface Treasure {
  id: string;
  name: string;
  value: number;
}

interface TreasureStore {
  // State
  treasures: Treasure[];
  isLoading: boolean;
  error: string | null;

  // Actions
  addTreasure: (treasure: Omit<Treasure, 'id'>) => void;
  removeTreasure: (id: string) => void;
  setLoading: (loading: boolean) => void;
  setError: (error: string | null) => void;
  reset: () => void;
}

export const useTreasureStore = create<TreasureStore>((set) => ({
  // Initial state
  treasures: [],
  isLoading: false,
  error: null,

  // Actions
  addTreasure: (treasure) =>
    set((state) => ({
      treasures: [...state.treasures, { ...treasure, id: Date.now().toString() }],
    })),

  removeTreasure: (id) =>
    set((state) => ({
      treasures: state.treasures.filter((t) => t.id !== id),
    })),

  setLoading: (isLoading) => set({ isLoading }),

  setError: (error) => set({ error, isLoading: false }),

  reset: () => set({ treasures: [], isLoading: false, error: null }),
}));

Usage in Components

import { useTreasureStore } from '@/stores/treasureStore';

function TreasureList() {
  // Subscribe to the entire state
  const { treasures, isLoading, removeTreasure } = useTreasureStore();

  // Or selective subscription (better performance)
  const treasures = useTreasureStore((state) => state.treasures);
  const removeTreasure = useTreasureStore((state) => state.removeTreasure);

  if (isLoading) return <ActivityIndicator />;

  return (
    <FlatList
      data={treasures}
      keyExtractor={(item) => item.id}
      renderItem={({ item }) => (
        <Pressable onPress={() => removeTreasure(item.id)}>
          <Text>{item.name}</Text>
        </Pressable>
      )}
    />
  );
}

Async Actions

import { create } from 'zustand';

interface TreasureStore {
  treasures: Treasure[];
  isLoading: boolean;
  error: string | null;
  fetchTreasures: () => Promise<void>;
  sellTreasure: (id: string) => Promise<void>;
}

export const useTreasureStore = create<TreasureStore>((set, get) => ({
  treasures: [],
  isLoading: false,
  error: null,

  fetchTreasures: async () => {
    set({ isLoading: true, error: null });
    try {
      const response = await fetch('https://api.atlantis.app/treasures');
      const data = await response.json();
      set({ treasures: data, isLoading: false });
    } catch (error) {
      set({ error: 'Failed to fetch treasures', isLoading: false });
    }
  },

  sellTreasure: async (id: string) => {
    const treasure = get().treasures.find((t) => t.id === id);
    if (!treasure) return;

    try {
      await fetch(`https://api.atlantis.app/treasures/${id}/sell`, {
        method: 'POST',
      });
      // Remove after successful sale
      set((state) => ({
        treasures: state.treasures.filter((t) => t.id !== id),
      }));
    } catch (error) {
      set({ error: 'Failed to sell treasure' });
    }
  },
}));

Middleware - Persist

import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';

export const useTreasureStore = create<TreasureStore>()(
  persist(
    (set) => ({
      treasures: [],
      isLoading: false,
      error: null,
      addTreasure: (treasure) =>
        set((state) => ({
          treasures: [...state.treasures, { ...treasure, id: Date.now().toString() }],
        })),
      // ... rest of actions
    }),
    {
      name: 'treasure-storage', // key in storage
      storage: createJSONStorage(() => AsyncStorage),
      partialize: (state) => ({ treasures: state.treasures }), // what to save
    }
  )
);

Selectors and Performance

// Bad - component re-renders on every state change
function BadComponent() {
  const store = useTreasureStore();
  return <Text>{store.treasures.length}</Text>;
}

// Good - component re-renders only when treasures.length changes
function GoodComponent() {
  const count = useTreasureStore((state) => state.treasures.length);
  return <Text>{count}</Text>;
}

// Advanced selectors
import { shallow } from 'zustand/shallow';

function TreasureSummary() {
  const { total, legendary } = useTreasureStore(
    (state) => ({
      total: state.treasures.length,
      legendary: state.treasures.filter((t) => t.rarity === 'legendary').length,
    }),
    shallow // shallow comparison instead of reference
  );

  return (
    <View>
      <Text>All: {total}</Text>
      <Text>Legendary: {legendary}</Text>
    </View>
  );
}

Zustand is an excellent choice for most applications - simple, performant, and boilerplate-free!

Code for this lesson: store.ts
import { create } from 'zustand';

export interface Treasure {
  id: string;
  name: string;
  value: number;
}

interface TreasureStore {
  treasures: Treasure[];
  addTreasure: (name: string, value: number) => void;
  removeTreasure: (id: string) => void;
  reset: () => void;
}

export const useTreasureStore = create<TreasureStore>((set) => ({
  // Initial state
  treasures: [],

  // TODO: add a treasure to the array with a unique id (Date.now().toString())
  addTreasure: (name, value) =>
    set((state) => ({
      treasures: [...state.treasures, { id: Date.now().toString(), name, value }],
    })),

  // TODO: remove the treasure with the given id using filter
  removeTreasure: (id) =>
    set((state) => ({
      treasures: state.treasures.filter((t) => t.id !== id),
    })),

  // TODO: clear the entire treasures array
  reset: () => set({ treasures: [] }),
}));

Docs entries used in this lesson

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. How do you create a store in Zustand?

  2. 2. Why is it worth using selectors in Zustand?

These are 2 of 3 questions for this lesson. Solve the rest in the game.

Hands-on tasks in the game

  • Click in order

    Click the elements for updating state in Zustand:

  • Code editor

    Complete the store code with CRUD actions.

Useful articles