Code To Learn logo

Code To Learn

M6: State ManagementZustand Path

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.js

Step 2: Define Initial State

Let's start with the state structure:

src/state/useListingsStore.js
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 9

Why IDs instead of full objects?

status: Loading state indicator

status: 'idle'      // Initial state
status: 'loading'   // Fetching data
status: 'succeeded' // Fetch successful
status: 'failed'    // Fetch failed

Usage 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 error

Usage:

{status === 'failed' && (
  <div className="error">
    Error: {error}
  </div>
)}

Step 3: Add Actions

Now let's add actions to manipulate state:

src/state/useListingsStore.js
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:

src/state/useListingsStore.js
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:

Test Component (temporary)
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:

  1. Initial: []
  2. Click "Toggle Listing 1": [1]
  3. Click "Toggle Listing 5": [1, 5]
  4. 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:

FieldPurposeRedux Equivalent
itemsAll listingsstate.listings.items
favoritesFavorited IDsstate.listings.favorites
statusLoading statestate.listings.status
errorError messagestate.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:

  1. Use the store in components
  2. Select state with selectors
  3. Call actions to update state
  4. 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