Skip to main content

useFilters

The useFilters hook manages job filtering, pagination, and API data fetching. It synchronizes filter state with URL parameters and handles loading states.

Location

/src/hooks/useFilters.js

Function Signature

export const useFilters = () => {
  // Returns object with filter state and handlers
}

Parameters

This hook takes no parameters.

Return Value

Returns an object with the following properties:
PropertyTypeDescription
jobsArrayArray of job objects from the API
loadingbooleanLoading state for API calls
totalnumberTotal number of jobs matching filters
totalPagesnumberTotal pages based on results per page (4)
currentPagenumberCurrent active page number (1-indexed)
textToFilterstringCurrent text search query
handlePageChangefunctionCallback to change current page
handleSearchfunctionCallback to update filters
handleTextFilterfunctionCallback to update text search

Usage Example

From src/pages/Search.jsx:
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 (
    <main>
      <SearchFormSection 
        initialTextInput={textToFilter} 
        onSearch={handleSearch} 
        onTextFilter={handleTextFilter} 
      />

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

Handler Functions

handleSearch(filters)

Updates filter state and resets pagination to page 1. Parameters:
filters = {
  technology: string,      // e.g., 'react', 'python'
  location: string,        // e.g., 'remoto', 'cdmx'
  experienceLevel: string  // e.g., 'junior', 'senior'
}
Example:
const filters = {
  technology: 'react',
  location: 'remoto',
  experienceLevel: 'senior'
}

handleSearch(filters)
// Updates URL to: ?technology=react&type=remoto&level=senior

handleTextFilter(text)

Updates the text search query and resets to page 1. Parameters:
  • text (string): Search query text
Example:
handleTextFilter('frontend developer')
// Updates URL to: ?text=frontend+developer

handlePageChange(page)

Changes the current page number. Parameters:
  • page (number): Target page number (1-indexed)
Example:
handlePageChange(3)
// Updates URL to: ?page=3

Internal State Management

The hook manages several pieces of state:

Filter State

const [filters, setFilters] = useState(() => ({
  technology: searchParams.get('technology') || '',
  location: searchParams.get('type') || '',
  experienceLevel: searchParams.get('level') || ''
}))
Initialized from URL parameters on mount.

Text Filter State

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

Pagination State

const [currentPage, setCurrentPage] = useState(() => {
  const page = searchParams.get('page') || '1'
  const pageNumber = Number(page)
  return isNaN(pageNumber) || pageNumber < 1 ? 1 : pageNumber
})
Validates page number from URL to ensure it’s a positive integer.

API Data State

const [jobs, setJobs] = useState([])
const [total, setTotal] = useState(0)
const [loading, setLoading] = useState(true)

API Integration

The hook fetches data from the JSCamp API:
useEffect(() => {
  async function fetchJobs() {
    try {
      setLoading(true)

      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)
      
      const offset = (currentPage - 1) * RESULTS_PER_PAGE
      params.append('limit', RESULTS_PER_PAGE)
      params.append('offset', offset)

      const response = await fetch(
        `https://jscamp-api.vercel.app/api/jobs?${params.toString()}`
      )
      const json = await response.json()
      setJobs(json.data)
      setTotal(json.total)
    } catch (error) {
      console.error('Error fetching jobs:', error)
    } finally {
      setLoading(false)
    }
  }
  fetchJobs()
}, [filters, textToFilter, currentPage])
API Parameters:
  • text: Search query
  • technology: Technology filter
  • type: Location filter
  • level: Experience level filter
  • limit: Results per page (fixed at 4)
  • offset: Pagination offset

URL Synchronization

The hook keeps URL parameters in sync with state:
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])
URL Example:
/search?text=react&technology=react&type=remoto&level=senior&page=2

Pagination Calculation

Total pages is calculated based on total results:
const RESULTS_PER_PAGE = 4
const totalPages = Math.ceil(total / RESULTS_PER_PAGE)

Best Practices

1. Reset Pagination on Filter Change

Both handleSearch and handleTextFilter reset to page 1:
const handleSearch = (filters) => {
  setFilters(filters)
  setCurrentPage(1)  // Reset to first page
}
This prevents showing page 5 when there are only 2 pages of results.

2. URL as Single Source of Truth

Initial state comes from URL parameters:
const [filters, setFilters] = useState(() => ({
  technology: searchParams.get('technology') || '',
  // ...
}))
This enables:
  • Shareable search URLs
  • Browser back/forward navigation
  • Page refresh without losing state

3. Loading States

Always show loading state during API calls:
{loading ? (
  <JobCardSkeleton count={5} />
) : (
  <JobListings jobs={jobs} />
)}

4. Error Handling

Log errors but don’t crash:
try {
  // API call
} catch (error) {
  console.error('Error fetching jobs:', error)
} finally {
  setLoading(false)  // Always stop loading
}

Common Patterns

Combining with useSearchForm

Pass handlers to form components:
const { handleSearch, handleTextFilter, textToFilter } = useFilters()

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

Displaying Results Metadata

const { total, currentPage, loading } = useFilters()

const title = loading 
  ? 'Cargando...' 
  : `Resultados: ${total}, Página: ${currentPage}`

Dependencies

  • react: useState, useEffect
  • react-router-dom: useSearchParams

Build docs developers (and LLMs) love