Kurs React Native · Moduł 4: Stan i dane z API
Zustand - Lekki Store
W tej lekcji6
Zustand to minimalistyczna biblioteka do zarządzania stanem - prosta jak useState, ale globalna jak Redux!
Instalacja
npx expo install zustandPodstawowy 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. Jak utworzyć store w Zustand?
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.