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:
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 simplePhase 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 timeYou'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()andget()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 labelsNext Steps
Continue learning:
-
Module 7: Forms & Auth
- User registration/login
- Protected routes
- Form validation
- Auth tokens
-
Module 8: Deployment
- Build for production
- Deploy to Vercel/Netlify
- Environment variables
- Performance optimization
-
Advanced Zustand:
- Middleware (persist, devtools)
- TypeScript integration
- Immer for nested updates
- Multiple stores pattern
-
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! 🚀