Kurs React Native · Moduł 4: Stan i dane z API

Zustand - Lekki Store

3 min czytania ·
W tej lekcji6

Zustand to minimalistyczna biblioteka do zarządzania stanem - prosta jak useState, ale globalna jak Redux!

Instalacja

npx expo install zustand

Podstawowy store

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

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

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

  // Akcje
  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) => ({
  // Stan początkowy
  treasures: [],
  isLoading: false,
  error: null,

  // Akcje
  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 }),
}));

Użycie w komponentach

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

function TreasureList() {
  // Subskrypcja do całego stanu
  const { treasures, isLoading, removeTreasure } = useTreasureStore();

  // Lub selektywna subskrypcja (lepsza wydajność)
  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>
      )}
    />
  );
}

Akcje asynchroniczne

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.atlantyda.app/treasures');
      const data = await response.json();
      set({ treasures: data, isLoading: false });
    } catch (error) {
      set({ error: 'Nie udało się pobrać skarbów', isLoading: false });
    }
  },

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

    try {
      await fetch(`https://api.atlantyda.app/treasures/${id}/sell`, {
        method: 'POST',
      });
      // Usuń po udanej sprzedaży
      set((state) => ({
        treasures: state.treasures.filter((t) => t.id !== id),
      }));
    } catch (error) {
      set({ error: 'Nie udało się sprzedać skarbu' });
    }
  },
}));

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() }],
        })),
      // ... reszta akcji
    }),
    {
      name: 'treasure-storage', // klucz w storage
      storage: createJSONStorage(() => AsyncStorage),
      partialize: (state) => ({ treasures: state.treasures }), // co zapisywać
    }
  )
);

Selektory i wydajność

// Złe - komponent przerenderuje się przy każdej zmianie stanu
function BadComponent() {
  const store = useTreasureStore();
  return <Text>{store.treasures.length}</Text>;
}

// Dobre - komponent przerenderuje się tylko gdy zmieni się treasures.length
function GoodComponent() {
  const count = useTreasureStore((state) => state.treasures.length);
  return <Text>{count}</Text>;
}

// Zaawansowane selektory
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 // porównanie płytkie zamiast referencji
  );

  return (
    <View>
      <Text>Wszystkie: {total}</Text>
      <Text>Legendarne: {legendary}</Text>
    </View>
  );
}

Zustand to doskonały wybór dla większości aplikacji - prosty, wydajny i bez boilerplate'u!

Kod do tej lekcji: 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) => ({
  // Stan początkowy
  treasures: [],

  // TODO: dodaj skarb do tablicy z unikalnym id (Date.now().toString())
  addTreasure: (name, value) =>
    set((state) => ({
      treasures: [...state.treasures, { id: Date.now().toString(), name, value }],
    })),

  // TODO: usuń skarb o podanym id za pomocą filter
  removeTreasure: (id) =>
    set((state) => ({
      treasures: state.treasures.filter((t) => t.id !== id),
    })),

  // TODO: wyczyść całą tablicę skarbów
  reset: () => set({ treasures: [] }),
}));

Hasła z dokumentacji w tej lekcji

Widzisz błąd w tej lekcji?

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Jak utworzyć store w Zustand?

  2. 2. Dlaczego warto używać selektorów w Zustand?

To 2 z 3 pytań do tej lekcji. Pozostałe rozwiążesz w grze.

Zadania praktyczne w grze

  • Klikanie w kolejności

    Kliknij elementy aktualizacji stanu w Zustand:

  • Edytor kodu

    Uzupełnij kod store z akcjami CRUD.

Przydatne artykuły