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:
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:
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:
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
Import with lazy()
Use React’s lazy() function to dynamically import componentsconst SearchPage = lazy(() => import('./pages/Search.jsx'))
Wrap with Suspense
Provide a loading fallback while the component loads<Suspense fallback={<PageSkeleton />}>
<Routes>...</Routes>
</Suspense>
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.
Navigation Patterns
DevJobs uses three navigation approaches:
1. Declarative Links
For standard navigation, use the custom Link component:
import { Link } from '@/components'
<Link href="/" style={{ textDecoration: 'none' }}>
<h1>DevJobs</h1>
</Link>
Custom Link Wrapper
The Link component wraps React Router’s Link for consistency:
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.
2. Active Links
Use NavLink for navigation with active state:
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:
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:
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:
<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