L8: Create Navbar Component
Build a navigation bar with favorites count badge
Let's build a navbar with navigation links and a favorites counter! 🧭
What We're Building
A Navbar component that:
- Displays site logo/name
- Links to HomePage and FavoritesPage
- Shows favorites count in a badge
- Updates count in real-time
- Highlights active page
Step 1: Create the Component
import { Link, useLocation } from 'react-router-dom';
import useListingsStore from '@/state/useListingsStore';
function Navbar() {
// Get favorites count
const favorites = useListingsStore((state) => state.favorites);
const favoritesCount = favorites.length;
// Get current location for active link
const location = useLocation();
return (
<nav className="navbar">
<div className="navbar-container">
{/* Logo/Brand */}
<Link to="/" className="navbar-brand">
<span className="logo">🏖️</span>
<span className="brand-name">Holidaze</span>
</Link>
{/* Navigation Links */}
<div className="navbar-links">
<Link
to="/"
className={`nav-link ${location.pathname === '/' ? 'active' : ''}`}
>
<span className="nav-icon">🏠</span>
<span className="nav-text">Home</span>
</Link>
<Link
to="/favorites"
className={`nav-link ${location.pathname === '/favorites' ? 'active' : ''}`}
>
<span className="nav-icon">❤️</span>
<span className="nav-text">Favorites</span>
{favoritesCount > 0 && (
<span className="badge">{favoritesCount}</span>
)}
</Link>
</div>
</div>
</nav>
);
}
export default Navbar;Understanding the Component
Step 2: Add Styling
/* Navbar */
.navbar {
background: white;
border-bottom: 1px solid #e5e7eb;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
position: sticky;
top: 0;
z-index: 100;
}
.navbar-container {
max-width: 1200px;
margin: 0 auto;
padding: 1rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}
/* Brand/Logo */
.navbar-brand {
display: flex;
align-items: center;
gap: 0.75rem;
text-decoration: none;
color: #0369a1;
font-weight: 700;
font-size: 1.5rem;
transition: transform 0.2s;
}
.navbar-brand:hover {
transform: scale(1.05);
}
.logo {
font-size: 2rem;
}
.brand-name {
font-size: 1.5rem;
}
/* Navigation Links */
.navbar-links {
display: flex;
gap: 1rem;
}
.nav-link {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1.25rem;
text-decoration: none;
color: #6b7280;
border-radius: 8px;
font-weight: 500;
transition: all 0.2s;
position: relative;
}
.nav-link:hover {
background: #f3f4f6;
color: #0369a1;
}
.nav-link.active {
background: #dbeafe;
color: #0369a1;
}
.nav-icon {
font-size: 1.25rem;
}
.nav-text {
font-size: 1rem;
}
/* Badge */
.badge {
background: #dc2626;
color: white;
font-size: 0.75rem;
font-weight: 700;
padding: 0.125rem 0.5rem;
border-radius: 12px;
min-width: 20px;
text-align: center;
position: absolute;
top: 0.5rem;
right: 0.5rem;
animation: badge-appear 0.3s ease;
}
@keyframes badge-appear {
from {
opacity: 0;
transform: scale(0);
}
to {
opacity: 1;
transform: scale(1);
}
}
/* Responsive */
@media (max-width: 640px) {
.navbar-container {
padding: 1rem;
}
.brand-name {
display: none;
}
.nav-text {
display: none;
}
.nav-link {
padding: 0.75rem;
}
}Real-Time Updates
The best part about using Zustand: The count updates automatically!
The flow:
1. User is on HomePage
Navbar shows "Favorites [3]"
2. User clicks favorite button on listing
↓
3. toggleFavorite() is called
↓
4. Store updates: favorites = [1, 5, 9, 12] (added 12)
↓
5. Navbar re-renders (it selects favorites.length)
↓
6. Badge updates: "Favorites [4]"No props, no context, no manual updates! Just works. ✨
Test sequence:
- Open app, go to HomePage
- Look at navbar: "Favorites" (no badge)
- Click favorite on a listing
- Watch navbar: "Favorites [1]" appears!
- Click favorite on 2 more listings
- Watch navbar: "Favorites [3]"
- Go to FavoritesPage
- Remove a favorite
- Watch navbar: "Favorites [2]" updates!
It works from any page! 🎉
How efficient is this?
// Navbar selects favorites.length
const count = useListingsStore((state) => state.favorites.length);
// When user changes search filter:
// - HomePage re-renders (uses searchQuery)
// - Navbar does NOT re-render (doesn't use searchQuery) ✅
// When user toggles favorite:
// - HomePage re-renders (uses favorites for button)
// - Navbar re-renders (uses favorites.length for badge) ✅
// - Both necessary!
// When user changes price filter:
// - HomePage re-renders (uses maxPrice)
// - Navbar does NOT re-render (doesn't use maxPrice) ✅Zustand only re-renders components that use the changed state! 🚀
Improved Version with Memoization
For maximum performance, we can optimize:
import { Link, useLocation } from 'react-router-dom';
import useListingsStore from '@/state/useListingsStore';
function Navbar() {
// Select only the count, not the entire array
const favoritesCount = useListingsStore((state) => state.favorites.length);
const location = useLocation();
return (
<nav className="navbar">
<div className="navbar-container">
<Link to="/" className="navbar-brand">
<span className="logo">🏖️</span>
<span className="brand-name">Holidaze</span>
</Link>
<div className="navbar-links">
<Link
to="/"
className={`nav-link ${location.pathname === '/' ? 'active' : ''}`}
>
<span className="nav-icon">🏠</span>
<span className="nav-text">Home</span>
</Link>
<Link
to="/favorites"
className={`nav-link ${location.pathname === '/favorites' ? 'active' : ''}`}
>
<span className="nav-icon">❤️</span>
<span className="nav-text">Favorites</span>
{favoritesCount > 0 && (
<span className="badge">{favoritesCount}</span>
)}
</Link>
</div>
</div>
</nav>
);
}
export default Navbar;Difference:
// Before: Selects array, calculates length
const favorites = useListingsStore((state) => state.favorites);
const count = favorites.length;
// After: Selects length directly
const count = useListingsStore((state) => state.favorites.length);Why better?
// Scenario: favorites array is [1, 2, 3]
// User toggles favorite 1 OFF, then favorite 1 ON
// Before:
// 1. favorites = [2, 3] → re-render (array changed)
// 2. favorites = [1, 2, 3] → re-render (array changed)
// Total: 2 re-renders
// After:
// 1. favorites.length = 2 → re-render (length changed)
// 2. favorites.length = 3 → re-render (length changed)
// Total: 2 re-renders
// Actually same! But if reordering:
// favorites = [1, 2, 3] → [3, 2, 1] (reordered)
// Before: Re-renders (array reference changed)
// After: Doesn't re-render (length still 3) ✅More stable!
Comparison with Redux
Simple and direct:
import useListingsStore from '@/state/useListingsStore';
function Navbar() {
// Just select what you need
const count = useListingsStore((state) => state.favorites.length);
return (
<nav>
Favorites {count > 0 && <span>{count}</span>}
</nav>
);
}No Provider, no hooks setup, just works! ✨
More setup required:
import { useSelector } from 'react-redux';
function Navbar() {
// Need to know nested path
const count = useSelector((state) => state.listings.favorites.length);
return (
<nav>
Favorites {count > 0 && <span>{count}</span>}
</nav>
);
}
// Also need Provider in root:
// <Provider store={store}>
// <App />
// </Provider>More imports, nested paths, Provider needed.
Add Navbar to Layout
We'll add the navbar to our app layout in the next lesson, but here's a preview:
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Navbar from '@/components/Navbar';
import HomePage from '@/pages/HomePage';
import FavoritesPage from '@/pages/FavoritesPage';
function App() {
return (
<BrowserRouter>
<div className="app">
<Navbar /> {/* Add navbar here! */}
<main className="main-content">
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/favorites" element={<FavoritesPage />} />
</Routes>
</main>
</div>
</BrowserRouter>
);
}
export default App;Understanding Zustand's Power
What's Next?
Perfect! Navbar is ready. In the next lesson:
- Add Navbar to App - Integrate into layout
- Update Router - Add favorites route
- Test navigation - Ensure all links work
- Add layout styling - Polish the design
✅ Lesson Complete! Navbar displays favorites count with real-time updates!
Key Takeaways
- ✅ Real-time count - Badge updates automatically when favorites change
- ✅ Active link highlighting - Shows current page with
useLocation() - ✅ Conditional badge - Only shows when count > 0
- ✅ No prop drilling - Component reads directly from store
- ✅ Optimized selector - Selects only
favorites.length, not entire array - ✅ Responsive design - Adapts to mobile screens
- ✅ Simpler than Redux - No Provider, no nested paths, just hooks
- ✅ Automatic sync - All components update together