Skip to main content
The Search Page displays job listings based on user search criteria, providing advanced filtering, pagination, and real-time results updates.

Route

  • Path: /search
  • Component: SearchPage
  • File: source/src/pages/Search.jsx

Purpose

The search page is the primary job discovery interface, allowing users to:
  • View job listings matching their criteria
  • Filter jobs by technology, location, and experience level
  • Navigate through paginated results
  • Perform text-based searches

URL Parameters

The search page reads and manages multiple URL query parameters:
ParameterDescriptionExample
textFree-text search query?text=react
technologyTechnology filter?technology=javascript
typeLocation/work type filter?type=remoto
levelExperience level filter?level=senior
pageCurrent page number?page=2
Example URL: /search?text=frontend&technology=react&type=remoto&level=mid&page=2

Key Features

1. Advanced Search Form

Provides multiple filter options:
  • Text Search: Free-form search across titles, skills, and companies
  • Technology Filter: Dropdown for specific technologies
  • Location Filter: Remote/city options
  • Experience Level: Junior, Mid, Senior, Lead

2. Dynamic Results

Real-time job listings that:
  • Update automatically when filters change
  • Show loading skeleton during fetch
  • Display total count and current page in title
  • Render job cards with key information

3. Pagination

Navigate through results:
  • 4 jobs per page (configurable via RESULTS_PER_PAGE)
  • Previous/Next navigation
  • Direct page number selection
  • URL synchronized with current page

Components Used

The search page integrates several key components:

SearchFormSection

<SearchFormSection 
  initialTextInput={textToFilter} 
  onSearch={handleSearch} 
  onTextFilter={handleTextFilter} 
/>
Props:
  • initialTextInput: Pre-fills search text from URL
  • onSearch: Callback for filter changes
  • onTextFilter: Callback for text input changes

JobListings

<JobListings jobs={jobs} />
Props:
  • jobs: Array of job objects to display

JobCardSkeleton

<JobCardSkeleton count={5} />
Props:
  • count: Number of skeleton cards to show

Pagination

<Pagination 
  currentPage={currentPage} 
  totalPages={totalPages} 
  onPageChange={handlePageChange} 
/>
Props:
  • currentPage: Active page number
  • totalPages: Total number of pages
  • onPageChange: Callback when page changes

Hooks and State Management

useFilters Hook

The search page relies entirely on the useFilters custom hook for state management:
const {
  jobs,              // Job listings array
  loading,           // Loading state boolean
  total,             // Total number of results
  totalPages,        // Calculated total pages
  currentPage,       // Active page number
  textToFilter,      // Current text search query
  handlePageChange,  // Navigate to different page
  handleSearch,      // Apply filter changes
  handleTextFilter,  // Update text search
} = useFilters()
Key Features:
  • URL Synchronization: Reads and updates URL parameters automatically
  • API Integration: Fetches jobs from backend API with filters
  • Pagination Logic: Calculates offset and limit for API calls
  • State Persistence: Maintains filters across page reloads

Code Example

Complete Search Page Implementation

import { Pagination, SearchFormSection, JobListings, JobCardSkeleton } 
  from '@/components'
import { useFilters } from '@/hooks/useFilters.js'

function SearchPage() {
  const {
    jobs,
    loading,
    total,
    totalPages,
    currentPage,
    textToFilter,
    handlePageChange,
    handleSearch,
    handleTextFilter,
  } = useFilters()

  const title = loading 
    ? 'Cargando...' 
    : `Resultados: ${total} , Pagina: ${currentPage} - DevJobs`

  return (
    <>
      <title>{title}</title>

      <main>
        <SearchFormSection 
          initialTextInput={textToFilter} 
          onSearch={handleSearch} 
          onTextFilter={handleTextFilter} 
        />

        <section>
          <h2 style={{ textAlign: 'center' }}>
            Resultados de búsqueda
          </h2>
          {loading 
            ? <JobCardSkeleton count={5} /> 
            : <JobListings jobs={jobs} />
          }
          <Pagination 
            currentPage={currentPage} 
            totalPages={totalPages} 
            onPageChange={handlePageChange} 
          />
        </section>
      </main>
    </>
  )
}

export default SearchPage

Dynamic Title Generation

const title = loading 
  ? 'Cargando...' 
  : `Resultados: ${total} , Pagina: ${currentPage} - DevJobs`
The page title updates dynamically to show:
  • Loading state while fetching
  • Total results and current page number when loaded

User Flows

Initial Search Flow

  1. User arrives from home page with ?text=react parameter
  2. useFilters hook reads URL parameters
  3. API request is made with search criteria
  4. Loading skeleton is displayed
  5. Job listings render when data arrives

Filter Interaction Flow

  1. User changes filter (e.g., selects “Senior” level)
  2. handleSearch callback is triggered
  3. useFilters updates state and URL parameters
  4. Page resets to 1 (when filters change)
  5. API re-fetches with new criteria
  6. Results update automatically

Pagination Flow

  1. User clicks page number or Next button
  2. handlePageChange callback is triggered
  3. useFilters updates currentPage state
  4. URL updates with ?page={n} parameter
  5. API fetches new page of results
  6. Job listings update with new page data

Text Search Flow

  1. User types in search input
  2. handleTextFilter callback is triggered
  3. useFilters updates textToFilter state
  4. URL updates with ?text={query} parameter
  5. Page resets to 1 (new search)
  6. Results refresh with matching jobs

API Integration

The search page interacts with the backend API through the useFilters hook:

API Endpoint

https://jscamp-api.vercel.app/api/jobs

Query Parameters

const params = new URLSearchParams()
if (textToFilter) params.append('text', textToFilter)
if (filters.technology) params.append('technology', filters.technology)
if (filters.location) params.append('type', filters.location)
if (filters.experienceLevel) params.append('level', filters.experienceLevel)

// Pagination
const offset = (currentPage - 1) * RESULTS_PER_PAGE
params.append('limit', RESULTS_PER_PAGE) // 4 per page
params.append('offset', offset)

Response Format

{
  "data": [
    {
      "id": "1",
      "titulo": "Senior React Developer",
      "empresa": "TechCorp",
      "ubicacion": "Remoto",
      "data": {
        "technology": ["react", "javascript"],
        "modalidad": "remoto",
        "nivel": "senior"
      }
    }
  ],
  "total": 42
}

State Synchronization

The useFilters hook maintains bidirectional synchronization:

URL → State

const [filters, setFilters] = useState(() => {
  return {
    technology: searchParams.get('technology') || '',
    location: searchParams.get('type') || '',
    experienceLevel: searchParams.get('level') || ''
  }
})

const [textToFilter, setTextToFilter] = useState(
  () => searchParams.get('text') || ''
)

const [currentPage, setCurrentPage] = useState(() => {
  const page = searchParams.get('page') || '1'
  const pageNumber = Number(page)
  return isNaN(pageNumber) || pageNumber < 1 ? 1 : pageNumber
})

State → URL

useEffect(() => {
  setSearchParams(() => {
    const params = new URLSearchParams()
    if (textToFilter) params.set('text', textToFilter)
    if (filters.technology) params.set('technology', filters.technology)
    if (filters.location) params.set('type', filters.location)
    if (filters.experienceLevel) params.set('level', filters.experienceLevel)
    if (currentPage > 1) params.set('page', currentPage)
    return params
  })
}, [filters, textToFilter, currentPage, setSearchParams])

Loading States

The page handles three loading states:
  1. Initial Load: Shows skeleton when component first mounts
  2. Filter Change: Shows skeleton when filters update
  3. Pagination: Shows skeleton when changing pages

Skeleton vs. Results

{loading 
  ? <JobCardSkeleton count={5} /> 
  : <JobListings jobs={jobs} />
}

Structure

SearchPage
├── Dynamic Title
├── Main Section
│   ├── SearchFormSection
│   │   ├── Text Input
│   │   ├── Technology Select
│   │   ├── Location Select
│   │   └── Level Select
│   │
│   └── Results Section
│       ├── Heading ("Resultados de búsqueda")
│       ├── JobCardSkeleton (if loading)
│       ├── JobListings (if loaded)
│       │   └── JobCard[] (array of job cards)
│       └── Pagination
│           ├── Previous Button
│           ├── Page Numbers
│           └── Next Button

Integration Points

From Home Page

  • Receives ?text={query} parameter from home search
  • Pre-fills search input with query value
  • Displays filtered results immediately

To Detail Page

  • Each job card links to /jobs/{jobId}
  • Clicking a job navigates to detail view
  • User can return via breadcrumb navigation

URL Sharing

Users can share URLs with filters:
/search?text=frontend&technology=react&type=remoto&level=senior&page=2
Recipient sees exact same filtered results.

Performance Considerations

  • Debouncing: Consider debouncing text input to reduce API calls
  • Skeleton Loading: Provides instant visual feedback during loads
  • URL State: Eliminates need for complex state management libraries
  • Pagination: Limits results to 4 per page for fast loading

Accessibility

  • Semantic HTML: Uses <main> and <section> appropriately
  • Dynamic Title: Updates for screen readers
  • Loading States: Clear indication of loading progress
  • Keyboard Navigation: All controls are keyboard accessible
  • useFilters - Complete filter and pagination logic

Build docs developers (and LLMs) love