Navigation and Links in React Router

Mastering Navigation Patterns in React Applications

Welcome to Navigation and Links!

Think of navigation in a React app like a smart GPS system. It not only helps users move between destinations but also remembers where they've been, suggests the best routes, and even adapts to their preferences. Today, we'll explore how to create sophisticated navigation systems that enhance user experience.

graph LR A[Link Components] --> B{Navigation System} B --> C[URL Update] B --> D[History Management] B --> E[Component Rendering] C --> F[Browser Address Bar] D --> G[Back/Forward Buttons] E --> H[New Content] style A fill:#ff9999 style B fill:#99ff99 style F fill:#9999ff

Programmatic Navigation

1. useNavigate Hook

import { useNavigate } from 'react-router-dom';

function NavigationExamples() {
  const navigate = useNavigate();
  
  // Basic navigation
  const goToHome = () => {
    navigate('/');
  };
  
  // Navigation with replace
  const goToLogin = () => {
    navigate('/login', { replace: true });
  };
  
  // Navigation with state
  const goToProfile = () => {
    navigate('/profile', { 
      state: { 
        from: 'dashboard',
        timestamp: Date.now()
      } 
    });
  };
  
  // Relative navigation
  const goBack = () => {
    navigate(-1);
  };
  
  const goForward = () => {
    navigate(1);
  };
  
  // Navigate to specific history entry
  const goToHistoryIndex = (index) => {
    navigate(index);
  };
  
  // Conditional navigation
  const conditionalNavigate = (condition) => {
    if (condition) {
      navigate('/success');
    } else {
      navigate('/error');
    }
  };
  
  return (
    <div>
      <button onClick={goToHome}>Home</button>
      <button onClick={goToLogin}>Login</button>
      <button onClick={goToProfile}>Profile</button>
      <button onClick={goBack}>Back</button>
      <button onClick={goForward}>Forward</button>
    </div>
  );
}

2. Advanced Navigation Patterns

// Navigation with confirmation
function ConfirmNavigation() {
  const navigate = useNavigate();
  const [showConfirm, setShowConfirm] = useState(false);
  const [pendingNavigation, setPendingNavigation] = useState(null);
  
  const navigateWithConfirmation = (to) => {
    if (hasUnsavedChanges()) {
      setPendingNavigation(to);
      setShowConfirm(true);
    } else {
      navigate(to);
    }
  };
  
  const handleConfirm = () => {
    setShowConfirm(false);
    if (pendingNavigation) {
      navigate(pendingNavigation);
      setPendingNavigation(null);
    }
  };
  
  return (
    <>
      <button onClick={() => navigateWithConfirmation('/dashboard')}>
        Go to Dashboard
      </button>
      
      {showConfirm && (
        <Modal>
          <h2>Unsaved Changes</h2>
          <p>You have unsaved changes. Are you sure you want to leave?</p>
          <button onClick={handleConfirm}>Leave</button>
          <button onClick={() => setShowConfirm(false)}>Stay</button>
        </Modal>
      )}
    </>
  );
}

// Navigation with loading states
function NavigationWithLoading() {
  const navigate = useNavigate();
  const [isNavigating, setIsNavigating] = useState(false);
  
  const navigateWithLoading = async (to) => {
    setIsNavigating(true);
    
    try {
      // Perform any async operations before navigation
      await prepareNavigation(to);
      navigate(to);
    } catch (error) {
      console.error('Navigation failed:', error);
      // Handle error appropriately
    } finally {
      setIsNavigating(false);
    }
  };
  
  return (
    <button 
      onClick={() => navigateWithLoading('/dashboard')}
      disabled={isNavigating}
    >
      {isNavigating ? 'Loading...' : 'Go to Dashboard'}
    </button>
  );
}

// Navigation with animation
function AnimatedNavigation() {
  const navigate = useNavigate();
  const [isAnimating, setIsAnimating] = useState(false);
  
  const animatedNavigate = async (to) => {
    setIsAnimating(true);
    
    // Start exit animation
    await new Promise(resolve => setTimeout(resolve, 300));
    
    // Navigate
    navigate(to);
    
    // Wait for enter animation
    await new Promise(resolve => setTimeout(resolve, 300));
    
    setIsAnimating(false);
  };
  
  return (
    <div className={`transition-opacity ${isAnimating ? 'opacity-0' : 'opacity-100'}`}>
      <button onClick={() => animatedNavigate('/about')}>
        About Us
      </button>
    </div>
  );
}

Advanced Navigation Patterns

1. Breadcrumb Navigation

function Breadcrumbs() {
  const location = useLocation();
  const matches = useMatches();
  
  const breadcrumbs = matches
    .filter(match => match.handle?.breadcrumb)
    .map(match => ({
      title: match.handle.breadcrumb(match.data),
      path: match.pathname
    }));
  
  return (
    <nav aria-label="Breadcrumb">
      <ol className="breadcrumb">
        {breadcrumbs.map((crumb, index) => (
          <li key={crumb.path} className="breadcrumb-item">
            {index === breadcrumbs.length - 1 ? (
              <span>{crumb.title}</span>
            ) : (
              <Link to={crumb.path}>{crumb.title}</Link>
            )}
          </li>
        ))}
      </ol>
    </nav>
  );
}

2. Wizard Navigation

function WizardNavigation({ steps, currentStep }) {
  const navigate = useNavigate();
  
  const goToStep = (stepIndex) => {
    if (canNavigateToStep(stepIndex)) {
      navigate(`/wizard/step/${stepIndex}`);
    }
  };
  
  const nextStep = () => {
    if (currentStep < steps.length - 1) {
      goToStep(currentStep + 1);
    }
  };
  
  const previousStep = () => {
    if (currentStep > 0) {
      goToStep(currentStep - 1);
    }
  };
  
  return (
    <div className="wizard-nav">
      <div className="steps">
        {steps.map((step, index) => (
          <button
            key={step.id}
            className={`step ${index === currentStep ? 'active' : ''} 
              ${index < currentStep ? 'completed' : ''}`}
            onClick={() => goToStep(index)}
            disabled={!canNavigateToStep(index)}
          >
            {step.title}
          </button>
        ))}
      </div>
      
      <div className="controls">
        <button 
          onClick={previousStep} 
          disabled={currentStep === 0}
        >
          Previous
        </button>
        <button 
          onClick={nextStep}
          disabled={currentStep === steps.length - 1}
        >
          {currentStep === steps.length - 1 ? 'Finish' : 'Next'}
        </button>
      </div>
    </div>
  );
}

3. Tab Navigation

function TabNavigation() {
  const location = useLocation();
  const navigate = useNavigate();
  
  const tabs = [
    { id: 'overview', label: 'Overview', path: '/dashboard/overview' },
    { id: 'analytics', label: 'Analytics', path: '/dashboard/analytics' },
    { id: 'reports', label: 'Reports', path: '/dashboard/reports' },
    { id: 'settings', label: 'Settings', path: '/dashboard/settings' }
  ];
  
  const activeTab = tabs.find(tab => tab.path === location.pathname);
  
  return (
    <div className="tab-navigation">
      <div className="tabs" role="tablist">
        {tabs.map(tab => (
          <button
            key={tab.id}
            role="tab"
            aria-selected={activeTab?.id === tab.id}
            aria-controls={`panel-${tab.id}`}
            onClick={() => navigate(tab.path)}
            className={`tab ${activeTab?.id === tab.id ? 'active' : ''}`}
          >
            {tab.label}
          </button>
        ))}
      </div>
      
      <div className="tab-indicator" style={{
        transform: `translateX(${tabs.findIndex(t => t.id === activeTab?.id) * 100}%)`
      }} />
    </div>
  );
}

Accessible Navigation

// Accessible navigation component
function AccessibleNav() {
  const location = useLocation();
  
  // Announce page changes to screen readers
  useEffect(() => {
    const pageTitle = document.title;
    const announcement = `Navigated to ${pageTitle}`;
    
    // Create live region for announcements
    const liveRegion = document.getElementById('nav-announcer') || 
      document.createElement('div');
    liveRegion.id = 'nav-announcer';
    liveRegion.setAttribute('aria-live', 'polite');
    liveRegion.setAttribute('aria-atomic', 'true');
    liveRegion.className = 'sr-only';
    document.body.appendChild(liveRegion);
    
    // Announce navigation
    liveRegion.textContent = announcement;
  }, [location]);
  
  return (
    <nav aria-label="Main navigation">
      <ul role="list">
        <li>
          <NavLink 
            to="/" 
            aria-current={location.pathname === '/' ? 'page' : undefined}
          >
            Home
          </NavLink>
        </li>
        <li>
          <NavLink 
            to="/about"
            aria-current={location.pathname === '/about' ? 'page' : undefined}
          >
            About
          </NavLink>
        </li>
        <li>
          <NavLink 
            to="/contact"
            aria-current={location.pathname === '/contact' ? 'page' : undefined}
          >
            Contact
          </NavLink>
        </li>
      </ul>
    </nav>
  );
}

// Skip navigation link
function SkipLink() {
  return (
    <a 
      href="#main-content" 
      className="skip-link"
      onFocus={(e) => e.target.classList.add('visible')}
      onBlur={(e) => e.target.classList.remove('visible')}
    >
      Skip to main content
    </a>
  );
}

Testing Navigation

// Testing navigation components
import { render, screen, fireEvent } from '@testing-library/react';
import { MemoryRouter, Route, Routes } from 'react-router-dom';

describe('Navigation', () => {
  test('navigates to correct route when link is clicked', () => {
    render(
      <MemoryRouter initialEntries={['/']}>
        <Routes>
          <Route path="/" element={<Navigation />} />
          <Route path="/about" element={<div>About Page</div>} />
        </Routes>
      </MemoryRouter>
    );
    
    fireEvent.click(screen.getByText('About'));
    expect(screen.getByText('About Page')).toBeInTheDocument();
  });
  
  test('applies active class to current route', () => {
    render(
      <MemoryRouter initialEntries={['/about']}>
        <Navigation />
      </MemoryRouter>
    );
    
    const aboutLink = screen.getByText('About');
    expect(aboutLink).toHaveClass('active');
  });
  
  test('programmatic navigation works correctly', async () => {
    const TestComponent = () => {
      const navigate = useNavigate();
      
      return (
        <button onClick={() => navigate('/dashboard')}>
          Go to Dashboard
        </button>
      );
    };
    
    render(
      <MemoryRouter initialEntries={['/']}>
        <Routes>
          <Route path="/" element={<TestComponent />} />
          <Route path="/dashboard" element={<div>Dashboard</div>} />
        </Routes>
      </MemoryRouter>
    );
    
    fireEvent.click(screen.getByText('Go to Dashboard'));
    expect(screen.getByText('Dashboard')).toBeInTheDocument();
  });
});

// Testing navigation guards
describe('Navigation Guards', () => {
  test('prevents navigation when form is dirty', () => {
    const mockConfirm = jest.spyOn(window, 'confirm');
    mockConfirm.mockImplementation(() => false);
    
    render(
      <MemoryRouter>
        <EditForm />
      </MemoryRouter>
    );
    
    // Make form dirty
    fireEvent.change(screen.getByRole('textbox'), {
      target: { value: 'test' }
    });
    
    // Try to navigate away
    fireEvent.click(screen.getByText('Cancel'));
    
    expect(mockConfirm).toHaveBeenCalled();
    mockConfirm.mockRestore();
  });
});

Practice Exercises

Exercise 1: Build a Multi-level Navigation

Create a navigation system with:

  • Main navigation bar
  • Dropdown submenus
  • Active state indicators
  • Mobile-responsive behavior
  • Keyboard navigation support

Exercise 2: Create a History Manager

Build a component that:

  • Tracks navigation history
  • Provides "Recently Visited" list
  • Implements "Back to Previous Section"
  • Handles history persistence

Exercise 3: Implement Smart Navigation

Create a navigation system that:

  • Preloads routes on hover
  • Shows loading states during navigation
  • Handles navigation errors gracefully
  • Implements navigation analytics

Navigation Best Practices

1. Use Semantic HTML

<nav aria-label="Main navigation">
  <ul role="list">
    <li>
      <NavLink to="/" aria-current="page">Home</NavLink>
    </li>
  </ul>
</nav>

2. Handle Loading States

function NavigationWithLoading() {
  const navigation = useNavigation();
  
  return (
    <nav>
      {navigation.state === 'loading' && <LoadingIndicator />}
      {/* Navigation links */}
    </nav>
  );
}

3. Implement Error Boundaries

function NavigationErrorBoundary({ children }) {
  return (
    <ErrorBoundary
      fallback={<div>Navigation error occurred</div>}
    >
      {children}
    </ErrorBoundary>
  );
}

Key Takeaways