React Native course ยท Module 4: State and API data
Zustand - Lightweight Store
In this lesson6
Zustand is a minimalist state management library - as simple as useState, but global like Redux!
Installation
npx expo install zustandBasic 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. How do you create a store in Zustand?
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.