Skip to main content

Overview

DevJobs uses React Router v6 for client-side routing with:
  • Lazy-loaded route components for optimal performance
  • Suspense boundaries for loading states
  • Error boundaries for graceful error handling
  • Custom navigation hook for programmatic routing

React Router Setup

The routing infrastructure is initialized in two main files:

Entry Point (main.jsx)

The application is wrapped with BrowserRouter at the entry point:
main.jsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import App from './App.jsx'

createRoot(document.getElementById('root')).render(
  <BrowserRouter>
    <StrictMode>
      <App />
    </StrictMode>
  </BrowserRouter>
)
BrowserRouter uses the HTML5 History API to keep the UI in sync with the URL.

Route Configuration (App.jsx)

All routes are defined in the main App component:
App.jsx
import { lazy, Suspense } from 'react'
import { Routes, Route } from 'react-router-dom'
import { Footer, Header, PageSkeleton, ErrorBoundary } from '@/components'

// Lazy load all page components
const HomePage = lazy(() => import('./pages/Home.jsx'))
const SearchPage = lazy(() => import('./pages/Search.jsx'))
const JobDetailPage = lazy(() => import('./pages/Detail.jsx'))
const NotFoundPage = lazy(() => import('./pages/404.jsx'))

function App() {
  return (
    <ErrorBoundary>
      <Header />
      
      <Suspense fallback={<PageSkeleton />}>
        <Routes>
          <Route path="/" element={<HomePage />} />
          <Route path="/home" element={<HomePage />} />
          <Route path="/search" element={<SearchPage />} />
          <Route path="/jobs/:jobId" element={<JobDetailPage />} />
          <Route path="*" element={<NotFoundPage />} />
        </Routes>
      </Suspense>
      
      <Footer />
    </ErrorBoundary>
  )
}

Route Definitions

The application has five main routes:

Home Page

Path: / or /homeLanding page with hero section and quick search

Search Page

Path: /searchJob listings with filters and pagination

Job Detail

Path: /jobs/:jobIdIndividual job details with dynamic route parameter

404 Page

Path: * (catch-all)Not found page for invalid routes

Route Parameters

The job detail route uses a dynamic parameter:
<Route path="/jobs/:jobId" element={<JobDetailPage />} />
Access the parameter using the useParams hook:
Detail.jsx
import { useParams } from 'react-router-dom'

function JobDetailPage() {
  const { jobId } = useParams()
  // Use jobId to fetch job details
}

Lazy Loading Implementation

All page components are lazy-loaded to optimize initial bundle size.

How It Works

1

Import with lazy()

Use React’s lazy() function to dynamically import components
const SearchPage = lazy(() => import('./pages/Search.jsx'))
2

Wrap with Suspense

Provide a loading fallback while the component loads
<Suspense fallback={<PageSkeleton />}>
  <Routes>...</Routes>
</Suspense>
3

Automatic Code Splitting

Build tools automatically split lazy components into separate chunks

Benefits

Users only download code for the page they’re visiting, not the entire application.
Smaller initial bundle means faster time-to-interactive, especially on slower connections.
Additional features load as users navigate, distributing the load over time.

Loading States

The PageSkeleton component provides a consistent loading experience:
<Suspense fallback={<PageSkeleton />}>
  <Routes>
    {/* Routes render here after loading */}
  </Routes>
</Suspense>
Skeleton screens create the perception of faster loading compared to spinners or blank screens.
DevJobs uses three navigation approaches: For standard navigation, use the custom Link component:
Header.jsx
import { Link } from '@/components'

<Link href="/" style={{ textDecoration: 'none' }}>
  <h1>DevJobs</h1>
</Link>
The Link component wraps React Router’s Link for consistency:
Link.jsx
import { Link as NavLink } from 'react-router-dom'

export const Link = ({ href, children, ...props }) => {
  return (
    <NavLink to={href} {...props}>
      {children}
    </NavLink>
  )
}
The wrapper uses href prop instead of to for a more familiar API.
Use NavLink for navigation with active state:
Header.jsx
import { NavLink } from 'react-router-dom'

<NavLink
  className={({ isActive }) => isActive ? 'nav-link-active' : ''}
  to="/search"
>
  Empleos
</NavLink>
The isActive prop allows conditional styling based on current route.

3. Programmatic Navigation

For navigation triggered by user actions, use the custom useRouter hook:
Home.jsx
import useRouter from '@/hooks/useRouter'

export function HomePage() {
  const { navigateTo } = useRouter()
  
  const handleSearch = (event) => {
    event.preventDefault()
    const formData = new FormData(event.currentTarget)
    const searchText = formData.get('search')
    const url = searchText 
      ? `/search?text=${encodeURIComponent(searchText)}` 
      : '/search'
    navigateTo(url)
  }
  
  return (
    <form onSubmit={handleSearch}>
      {/* Form fields */}
    </form>
  )
}

useRouter Hook

The custom useRouter hook provides a simplified navigation API:
useRouter.js
import { useNavigate, useLocation } from 'react-router-dom'

export function useRouter() {
  const navigate = useNavigate()
  const location = useLocation()
  
  function navigateTo(path) {
    navigate(path)
  }
  
  return {
    currentPath: location.pathname,
    navigateTo
  }
}

Usage

const { currentPath, navigateTo } = useRouter()

// Get current path
console.log(currentPath) // e.g., '/search'

// Navigate programmatically
navigateTo('/jobs/123')
useRouter wraps React Router hooks to provide a consistent API across the application.

Error Handling

Routes are wrapped in an ErrorBoundary component:
App.jsx
<ErrorBoundary>
  <Header />
  <Suspense fallback={<PageSkeleton />}>
    <Routes>...</Routes>
  </Suspense>
  <Footer />
</ErrorBoundary>
This catches rendering errors and displays a friendly error page instead of crashing the app.

Search Parameters

Many routes use URL search parameters for state:
// Navigate with search params
navigateTo('/search?technology=react&page=2')

// Read search params
const [searchParams] = useSearchParams()
const technology = searchParams.get('technology') // 'react'
const page = searchParams.get('page') // '2'
Search parameters make URLs shareable and enable browser back/forward navigation.
See State Management for details on URL synchronization patterns.

Route Structure Best Practices

Consistent Paths

Use clear, consistent URL patterns
/search          - Listings
/jobs/:id        - Detail pages

Lazy Load Pages

Split code at route boundaries for optimal performance

Handle 404s

Always include a catch-all route for invalid URLs
<Route path="*" element={<NotFoundPage />} />

Error Boundaries

Wrap routes in error boundaries to prevent crashes

Example: Complete Navigation Flow

Here’s a complete example showing navigation from search to detail:
// 1. User searches on home page
const { navigateTo } = useRouter()
navigateTo('/search?text=react')

// 2. SearchPage loads with filters from URL
function SearchPage() {
  const { jobs, loading } = useFilters() // Reads URL params
  return (
    <JobListings jobs={jobs} />
  )
}

// 3. User clicks a job card
<Link href={`/jobs/${job.id}`}>
  <JobCard job={job} />
</Link>

// 4. DetailPage loads with job ID from route
function JobDetailPage() {
  const { jobId } = useParams()
  // Fetch and display job details
}

Next Steps

Architecture Overview

Understand the overall application architecture

State Management

Learn about custom hooks and URL synchronization

Build docs developers (and LLMs) love