L2: Create Listings Store
Build the listings store with state and actions for our app
Let's create the main store for our application! 🏠
What We're Building
A useListingsStore that manages:
- items - All listings from the API
- favorites - Array of favorited listing IDs
- status - Loading state ('idle', 'loading', 'succeeded', 'failed')
- error - Error message if fetch fails
Plus actions to update this state!
Step 1: Create the Store File
Create a new file for the listings store:
mkdir -p src/state
touch src/state/useListingsStore.jsStep 2: Define Initial State
Let's start with the state structure:
import { create } from 'zustand';
const useListingsStore = create((set) => ({
// State
items: [],
favorites: [],
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
error: null,
// Actions will go here...
}));
export default useListingsStore;Understanding the State Shape
items: Array of all listings
items: [
{
id: 1,
title: 'Beach House',
description: 'Beautiful beach house...',
price: 250,
images: ['url1', 'url2'],
maxGuests: 6,
// ... more fields
},
{
id: 2,
title: 'Mountain Cabin',
// ...
}
]Purpose: Store all fetched listings for display
favorites: Array of listing IDs
favorites: [1, 5, 9] // User favorited listings 1, 5, and 9Why IDs instead of full objects?
status: Loading state indicator
status: 'idle' // Initial state
status: 'loading' // Fetching data
status: 'succeeded' // Fetch successful
status: 'failed' // Fetch failedUsage in components:
if (status === 'loading') return <Spinner />;
if (status === 'failed') return <Error message={error} />;
if (status === 'succeeded') return <ListingList listings={items} />;Why not just isLoading?
// With boolean (limited)
isLoading: true/false
// Can't distinguish between:
// - Initial state (not started)
// - Currently loading
// - Successfully loaded
// - Failed to load
// With status string (clear)
status: 'idle' | 'loading' | 'succeeded' | 'failed'
// Know exactly what state we're in!error: Error message when fetch fails
error: null // No error
error: 'Network request failed' // Network error
error: 'Listings not found' // API errorUsage:
{status === 'failed' && (
<div className="error">
Error: {error}
</div>
)}Step 3: Add Actions
Now let's add actions to manipulate state:
import { create } from 'zustand';
const useListingsStore = create((set) => ({
// State
items: [],
favorites: [],
status: 'idle',
error: null,
// Actions
setItems: (items) => set({ items }),
setStatus: (status) => set({ status }),
setError: (error) => set({ error }),
toggleFavorite: (id) => set((state) => ({
favorites: state.favorites.includes(id)
? state.favorites.filter(favId => favId !== id) // Remove
: [...state.favorites, id] // Add
})),
}));
export default useListingsStore;Understanding the Actions
Complete Store Code
Here's our complete listings store:
import { create } from 'zustand';
const useListingsStore = create((set) => ({
// State
items: [],
favorites: [],
status: 'idle',
error: null,
// Actions
setItems: (items) => set({ items }),
setStatus: (status) => set({ status }),
setError: (error) => set({ error }),
toggleFavorite: (id) => set((state) => ({
favorites: state.favorites.includes(id)
? state.favorites.filter(favId => favId !== id)
: [...state.favorites, id]
})),
}));
export default useListingsStore;That's the entire store! ~20 lines total. 🎉
Comparison with Redux
Let's see how this compares to Redux Toolkit:
1 file, ~20 lines:
import { create } from 'zustand';
const useListingsStore = create((set) => ({
items: [],
favorites: [],
status: 'idle',
error: null,
setItems: (items) => set({ items }),
setStatus: (status) => set({ status }),
setError: (error) => set({ error }),
toggleFavorite: (id) => set((state) => ({
favorites: state.favorites.includes(id)
? state.favorites.filter(favId => favId !== id)
: [...state.favorites, id]
})),
}));
export default useListingsStore;Done! ✨
3 files, ~60 lines:
File 1: store.js
import { configureStore } from '@reduxjs/toolkit';
import listingsReducer from './slices/listingsSlice';
export const store = configureStore({
reducer: {
listings: listingsReducer
}
});File 2: listingsSlice.js
import { createSlice } from '@reduxjs/toolkit';
const listingsSlice = createSlice({
name: 'listings',
initialState: {
items: [],
favorites: [],
status: 'idle',
error: null
},
reducers: {
setItems: (state, action) => {
state.items = action.payload;
},
setStatus: (state, action) => {
state.status = action.payload;
},
setError: (state, action) => {
state.error = action.payload;
},
toggleFavorite: (state, action) => {
const id = action.payload;
if (state.favorites.includes(id)) {
state.favorites = state.favorites.filter(fav => fav !== id);
} else {
state.favorites.push(id);
}
}
}
});
export const { setItems, setStatus, setError, toggleFavorite } = listingsSlice.actions;
export default listingsSlice.reducer;File 3: App.jsx
import { Provider } from 'react-redux';
import { store } from './state/store';
<Provider store={store}>
<App />
</Provider>3x more code!
Testing the Store
Let's test our store works correctly. We can test toggleFavorite in a component:
import useListingsStore from '@/state/useListingsStore';
function TestStore() {
const favorites = useListingsStore((state) => state.favorites);
const toggleFavorite = useListingsStore((state) => state.toggleFavorite);
return (
<div>
<h2>Favorites: {JSON.stringify(favorites)}</h2>
<button onClick={() => toggleFavorite(1)}>Toggle Listing 1</button>
<button onClick={() => toggleFavorite(5)}>Toggle Listing 5</button>
<button onClick={() => toggleFavorite(9)}>Toggle Listing 9</button>
</div>
);
}Test sequence:
- Initial:
[] - Click "Toggle Listing 1":
[1] - Click "Toggle Listing 5":
[1, 5] - Click "Toggle Listing 1" again:
[5]
Perfect! The toggle works. ✅
Understanding set() Behavior
Zustand's set() function has smart behavior:
Why This Structure?
Our store structure mirrors what Redux Toolkit would create:
| Field | Purpose | Redux Equivalent |
|---|---|---|
| items | All listings | state.listings.items |
| favorites | Favorited IDs | state.listings.favorites |
| status | Loading state | state.listings.status |
| error | Error message | state.listings.error |
Benefit: If you know Redux, Zustand feels familiar!
Difference: Zustand is 90% simpler to set up and use.
What's Next?
Perfect! Our store is ready. In the next lesson, we'll:
- Use the store in components
- Select state with selectors
- Call actions to update state
- Understand re-render optimization
✅ Lesson Complete! You've created a complete Zustand store for listings!
Key Takeaways
- ✅ Store structure - State + actions in one place
- ✅ State shape - items, favorites, status, error
- ✅ Actions are functions - No dispatch, just call them
- ✅ toggleFavorite - Add/remove IDs from favorites array
- ✅ set() merges - Only specify what changes
- ✅ Functional updates - Use
(state) =>when reading previous state - ✅ 90% simpler than Redux Toolkit setup