Code To Learn logo

Code To Learn

M6: State ManagementZustand Path

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

src/components/Navbar.jsx
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

src/app/global.css
/* 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:

  1. Open app, go to HomePage
  2. Look at navbar: "Favorites" (no badge)
  3. Click favorite on a listing
  4. Watch navbar: "Favorites [1]" appears!
  5. Click favorite on 2 more listings
  6. Watch navbar: "Favorites [3]"
  7. Go to FavoritesPage
  8. Remove a favorite
  9. 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:

src/components/Navbar.jsx (Optimized)
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:

src/App.jsx (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:

  1. Add Navbar to App - Integrate into layout
  2. Update Router - Add favorites route
  3. Test navigation - Ensure all links work
  4. 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