Code To Learn logo

Code To Learn

M6: State ManagementZustand Path

L13: Module Review

Complete recap of the Zustand state management path

Zustand State Management

Let's review everything we built! 🎉

What We Built

Complete Holidaze application with:

  • ✅ Global state management (Zustand)
  • ✅ Data fetching from API
  • ✅ Filtering system (search, price, guests)
  • ✅ Favorites feature (add/remove)
  • ✅ Navigation with React Router
  • ✅ Responsive design
  • ✅ Real-time UI updates

Lesson-by-Lesson Recap

Complete Store Structure

Final useListingsStore:

src/store/listings.js
import { create } from 'zustand';

const useListingsStore = create((set, get) => ({
  // State
  items: [],
  favorites: [],
  status: 'idle',  // 'idle' | 'loading' | 'succeeded' | 'failed'
  error: null,
  searchQuery: '',
  maxPrice: null,
  maxGuests: null,
  
  // Sync 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]
  })),
  
  setSearchQuery: (query) => set({ searchQuery: query }),
  setMaxPrice: (price) => set({ maxPrice: price }),
  setMaxGuests: (guests) => set({ maxGuests: guests }),
  clearFilters: () => set({ searchQuery: '', maxPrice: null, maxGuests: null }),
  
  // Async Actions
  fetchListings: async () => {
    set({ status: 'loading', error: null });
    try {
      const response = await fetch('https://v2.api.noroff.dev/holidaze/venues');
      if (!response.ok) throw new Error('Failed to fetch');
      const result = await response.json();
      set({ items: result.data, status: 'succeeded' });
    } catch (error) {
      set({ error: error.message, status: 'failed' });
    }
  },
  
  // Computed Selectors
  getFilteredItems: () => {
    const { items, searchQuery, maxPrice, maxGuests } = get();
    return items.filter((item) => {
      if (searchQuery && !item.name.toLowerCase().includes(searchQuery.toLowerCase())) {
        return false;
      }
      if (maxPrice && item.price > maxPrice) {
        return false;
      }
      if (maxGuests && item.maxGuests < maxGuests) {
        return false;
      }
      return true;
    });
  },
  
  getFavoritedItems: () => {
    const { items, favorites } = get();
    return items.filter((item) => favorites.includes(item.id));
  },
}));

export default useListingsStore;

~150 lines of code for complete state management!

Complete Component List

Components we built:

src/
├── components/
│   ├── FavoriteButton.jsx       (Reusable favorite button)
│   ├── PropertyCard.jsx          (Display listing)
│   ├── Navbar.jsx               (Navigation with badge)
│   └── NotFoundPage.jsx         (404 error)
├── pages/
│   ├── HomePage.jsx             (Main listings page)
│   └── FavoritesPage.jsx        (Favorited listings)
├── store/
│   └── listings.js              (Zustand store)
└── App.jsx                      (Root component)

8 files, ~800 lines of code total! 🚀

Zustand vs Redux Comparison

Zustand (Simple):

import { create } from 'zustand';

const useStore = create((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
}));

// That's it! Ready to use.

Redux (Complex):

// 1. Install multiple packages
npm install @reduxjs/toolkit react-redux

// 2. Create slice
import { createSlice } from '@reduxjs/toolkit';

const counterSlice = createSlice({
  name: 'counter',
  initialState: { count: 0 },
  reducers: {
    increment: (state) => { state.count += 1; },
  },
});

// 3. Create store
import { configureStore } from '@reduxjs/toolkit';

const store = configureStore({
  reducer: { counter: counterSlice.reducer },
});

// 4. Wrap app with Provider
import { Provider } from 'react-redux';

<Provider store={store}>
  <App />
</Provider>

Zustand wins on simplicity!

Zustand:

// Direct state updates
const useStore = create((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
  reset: () => set({ count: 0 }),
}));

// Use anywhere
const increment = useStore((state) => state.increment);
increment();  // Done!

Redux:

// Create actions
const { increment, decrement, reset } = counterSlice.actions;

// Dispatch actions
import { useDispatch } from 'react-redux';

function Component() {
  const dispatch = useDispatch();
  
  const handleClick = () => {
    dispatch(increment());  // Extra boilerplate
  };
}

Zustand is more direct!

Zustand (Simple):

const useStore = create((set) => ({
  items: [],
  status: 'idle',
  
  fetchItems: async () => {
    set({ status: 'loading' });
    try {
      const response = await fetch('/api/items');
      const data = await response.json();
      set({ items: data, status: 'succeeded' });
    } catch (error) {
      set({ status: 'failed' });
    }
  },
}));

// Use it
const fetchItems = useStore((state) => state.fetchItems);
fetchItems();  // That's it!

Redux (Complex):

import { createAsyncThunk } from '@reduxjs/toolkit';

// Create thunk
const fetchItems = createAsyncThunk(
  'items/fetch',
  async () => {
    const response = await fetch('/api/items');
    return response.json();
  }
);

// Add to slice
const itemsSlice = createSlice({
  name: 'items',
  initialState: { items: [], status: 'idle' },
  extraReducers: (builder) => {
    builder
      .addCase(fetchItems.pending, (state) => {
        state.status = 'loading';
      })
      .addCase(fetchItems.fulfilled, (state, action) => {
        state.items = action.payload;
        state.status = 'succeeded';
      })
      .addCase(fetchItems.rejected, (state) => {
        state.status = 'failed';
      });
  },
});

// Dispatch
const dispatch = useDispatch();
dispatch(fetchItems());

Zustand: 15 lines. Redux: 30+ lines! 🎯

Zustand:

import useListingsStore from '@/store/listings';

function HomePage() {
  const items = useListingsStore((state) => state.items);
  const fetchListings = useListingsStore((state) => state.fetchListings);
  
  useEffect(() => {
    fetchListings();
  }, [fetchListings]);
  
  return <div>{items.map(...)}</div>;
}

Redux:

import { useSelector, useDispatch } from 'react-redux';
import { fetchListings } from '@/store/listingsSlice';

function HomePage() {
  const items = useSelector((state) => state.listings.items);
  const dispatch = useDispatch();
  
  useEffect(() => {
    dispatch(fetchListings());
  }, [dispatch]);
  
  return <div>{items.map(...)}</div>;
}

Very similar! But Zustand setup was easier.

When to Use Zustand vs Redux

Zustand is perfect for:

Small to medium apps

  • Under 50 components
  • Simple state shape
  • Few developers

Rapid prototyping

  • Need to move fast
  • Don't want boilerplate
  • Quick experiments

Learning state management

  • First time with global state
  • Want simple concepts
  • Prefer minimal setup

Modern React patterns

  • Hook-based components
  • Functional patterns
  • TypeScript support

Small bundle size matters

  • 1KB vs 13KB (Redux Toolkit)
  • Mobile-first apps
  • Performance-critical

Example projects:

  • Personal projects
  • Startups/MVPs
  • Portfolio sites
  • Small business apps

Redux is better for:

Large enterprise apps

  • 100+ components
  • Complex state shape
  • Multiple teams

Time-travel debugging

  • Need to replay actions
  • Complex debugging needs
  • DevTools integration essential

Predictable state changes

  • Strict patterns required
  • Audit trail needed
  • Compliance requirements

Middleware ecosystem

  • Redux Saga for complex async
  • Redux Persist for storage
  • Many third-party tools

Team familiarity

  • Team knows Redux well
  • Existing Redux codebase
  • Enterprise standards

Example projects:

  • Banking apps
  • E-commerce platforms
  • Social media apps
  • Enterprise SaaS

Can start with Zustand, migrate to Redux later:

Phase 1: Zustand (Weeks 1-8)

// Quick setup, rapid development
const useStore = create((set) => ({
  users: [],
  posts: [],
  // ... simple state
}));

Phase 2: Growth (Months 3-6)

// App grows, still manageable
const useUsersStore = create(...);
const usePostsStore = create(...);
const useAuthStore = create(...);
// Multiple stores, still simple

Phase 3: Redux Migration (Month 7+)

// When complexity demands it:
// - 50+ components
// - 10+ developers
// - Need DevTools
// - Require middleware

// Migrate gradually:
// 1. Keep Zustand for simple state
// 2. Add Redux for complex state
// 3. Slowly migrate over time

You're not locked in! Both can coexist. 🔄

Key Patterns We Learned

Best Practices Recap

State Management:

  • ✅ Keep store flat and simple
  • ✅ Use computed selectors for derived data
  • ✅ Store IDs for relationships
  • ✅ Make updates immutable
  • ✅ Choose local vs store state wisely

Components:

  • ✅ Extract reusable components (FavoriteButton)
  • ✅ Use selective selectors (only what you need)
  • ✅ Handle loading/error states
  • ✅ Add proper event handling (preventDefault, stopPropagation)
  • ✅ Include accessibility features (aria-label, keyboard nav)

Performance:

  • ✅ Memoize expensive calculations (useMemo)
  • ✅ Lazy load images (loading="lazy")
  • ✅ Code split routes (React.lazy)
  • ✅ Optimize selectors (select primitives when possible)

Styling:

  • ✅ Responsive design (mobile-first)
  • ✅ Smooth animations (transitions, transforms)
  • ✅ Consistent spacing (rem units)
  • ✅ Accessible colors (WCAG contrast ratios)

What You've Learned

After completing this module, you can:

Zustand:

  • ✅ Install and set up Zustand
  • ✅ Create stores with create()
  • ✅ Use set() and get() for state updates
  • ✅ Write async actions
  • ✅ Build computed selectors
  • ✅ Optimize re-renders with selective subscriptions

React Patterns:

  • ✅ Custom hooks for state management
  • ✅ Component composition
  • ✅ Local vs global state decisions
  • ✅ Event handling best practices
  • ✅ Accessibility implementation

Application Architecture:

  • ✅ Organize store structure
  • ✅ Separate concerns (components, store, pages)
  • ✅ Create reusable components
  • ✅ Handle async data flow
  • ✅ Manage navigation with React Router

Project Stats

Final application:

📦 Bundle Size: ~50KB (with React, Router, Zustand)
  ├── React: ~40KB
  ├── React Router: ~9KB
  └── Zustand: ~1KB ✨

📁 Files: 8 files
  ├── 3 pages (HomePage, FavoritesPage, NotFoundPage)
  ├── 3 components (Navbar, PropertyCard, FavoriteButton)
  ├── 1 store (listings)
  └── 1 root (App)

📝 Lines of Code: ~800 total
  ├── Components: ~400 lines
  ├── Store: ~150 lines
  ├── Styling: ~250 lines
  └── Config: minimal

⚡ Performance: Excellent
  ✅ Selective re-renders
  ✅ Lazy image loading
  ✅ Memoized computations
  ✅ Small bundle size

♿ Accessibility: AAA
  ✅ Keyboard navigation
  ✅ Screen reader support
  ✅ Color contrast
  ✅ ARIA labels

Next Steps

Continue learning:

  1. Module 7: Forms & Auth

    • User registration/login
    • Protected routes
    • Form validation
    • Auth tokens
  2. Module 8: Deployment

    • Build for production
    • Deploy to Vercel/Netlify
    • Environment variables
    • Performance optimization
  3. Advanced Zustand:

    • Middleware (persist, devtools)
    • TypeScript integration
    • Immer for nested updates
    • Multiple stores pattern
  4. Testing:

    • Unit tests with Vitest
    • Component tests with Testing Library
    • E2E tests with Playwright
    • Store testing strategies

Congratulations! 🎉

You've completed the Zustand path for Module 6!

What you built:

  • ✅ Complete state management system
  • ✅ Real-time UI synchronization
  • ✅ Favorites feature
  • ✅ Filtering system
  • ✅ Navigation with Router
  • ✅ Responsive, accessible UI

Skills gained:

  • ✅ Zustand state management
  • ✅ Component architecture
  • ✅ Performance optimization
  • ✅ Accessibility best practices
  • ✅ Modern React patterns

You're now ready to:

  • Build production-ready React apps
  • Choose between Zustand and Redux
  • Implement complex state management
  • Create scalable application architecture

🎓 Module 6 Complete! You're a Zustand expert! Ready for Module 7?

Resources

Zustand:

React Router:

Accessibility:

Performance:

Keep building amazing things! 🚀