En esta clase vamos a implementar correctamente un sistema de paginación que funcione con la API, usando los parámetros limit y offset para cargar datos de forma eficiente. También corregiremos errores comunes que surgen al implementar paginación.

El problema inicial

Aunque ya tenemos los filtros funcionando, hay un problema con la paginación: no se refleja en la URL y no está integrada con la API.

Actualmente:

  • Los filtros se envían correctamente a la API ✅
  • La paginación existe en el frontend ✅
  • Pero la paginación no funciona con la API

Cada vez que cambias de página, no se hace una nueva petición con los datos correctos.

¿Cómo funcionan las APIs de paginación?

La mayoría de APIs no usan un parámetro page. En su lugar, utilizan dos parámetros fundamentales:

limit y offset

ParámetroDescripciónEjemplo
limitCuántos resultados devolver por página10
offsetCuántos resultados saltarse desde el inicio0, 10, 20

Ejemplos de uso

Página 1:

?limit=10&offset=0

Devuelve los primeros 10 resultados (del 0 al 9)

Página 2:

?limit=10&offset=10

Se salta los primeros 10 y devuelve del 10 al 19

Página 3:

?limit=10&offset=20

Se salta los primeros 20 y devuelve del 20 al 29

La fórmula del offset

Para calcular el offset a partir del número de página:

offset = (currentPage - 1) * resultsPerPage

Ejemplos:

Página actualResultados por páginaOffsetCálculo
1100(1-1) × 10 = 0
21010(2-1) × 10 = 10
31020(3-1) × 10 = 20
52080(5-1) × 20 = 80

Implementando limit y offset

Primero, definamos una constante para el número de resultados por página:

const RESULTS_PER_PAGE = 10

Ahora, dentro del useEffect donde hacemos la petición, añadimos el cálculo del offset:

useEffect(() => {
  async function fetchJobs() {
    try {
      setLoading(true)

      const params = new URLSearchParams()

      // Filtros existentes
      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)

      // ✅ Parámetros de paginación
      const offset = (currentPage - 1) * RESULTS_PER_PAGE
      params.append('limit', RESULTS_PER_PAGE)
      params.append('offset', offset)

      const queryParams = params.toString()

      const response = await fetch(`https://jscamp-api.vercel.app/api/jobs?${queryParams}`)
      const json = await response.json()

      setJobs(json.data)
      setTotal(json.total)
    } catch (error) {
      console.error('Error fetching jobs:', error)
    } finally {
      setLoading(false)
    }
  }

  fetchJobs()
}, [textToFilter, filters.technology, filters.location, filters.experienceLevel, currentPage])

Puntos clave

  1. Calculamos el offset antes de hacer la petición
  2. Añadimos limit y offset a los parámetros de la URL
  3. Incluimos currentPage en las dependencias del useEffect para que se ejecute cuando cambie la página

Primer problema: El número de páginas no cuadra

Al probar la paginación, te darás cuenta de que el número total de páginas está mal.

El error común

// ❌ Error: usar jobs.length como total
const totalPages = Math.ceil(jobs.length / RESULTS_PER_PAGE)

¿Por qué está mal?

  • jobs.length es el número de resultados de la página actual (por ejemplo, 10)
  • Si hay 143 empleos totales, jobs.length siempre será 10 (o menos en la última página)
  • Esto da un cálculo incorrecto: Math.ceil(10 / 10) = 1 (solo 1 página)

La solución correcta

Usa el campo total que devuelve la API:

// ✅ Correcto: usar el total real de la API
const totalPages = Math.ceil(total / RESULTS_PER_PAGE)

Ejemplo real:

Si la API devuelve:

{
  "total": 143,
  "limit": 10,
  "offset": 0,
  "data": [
    /* 10 empleos */
  ]
}

El cálculo correcto es:

Math.ceil(143 / 10) = 15 páginas

Implementación completa de la paginación

Veamos el código completo con la paginación funcionando correctamente:

import { useState, useEffect } from 'react'

const RESULTS_PER_PAGE = 10

function JobSearch() {
  const [jobs, setJobs] = useState([])
  const [loading, setLoading] = useState(true)
  const [total, setTotal] = useState(0)
  const [currentPage, setCurrentPage] = useState(1)
  const [textToFilter, setTextToFilter] = useState('')
  const [filters, setFilters] = useState({
    technology: '',
    location: '',
    experienceLevel: '',
  })

  // Calcular número total de páginas
  const totalPages = Math.ceil(total / RESULTS_PER_PAGE)

  useEffect(() => {
    async function fetchJobs() {
      try {
        setLoading(true)

        const params = new URLSearchParams()

        // Añadir filtros si existen
        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)

        // Añadir paginación
        const offset = (currentPage - 1) * RESULTS_PER_PAGE
        params.append('limit', RESULTS_PER_PAGE)
        params.append('offset', offset)

        const queryParams = params.toString()

        const response = await fetch(`https://jscamp-api.vercel.app/api/jobs?${queryParams}`)
        const json = await response.json()

        setJobs(json.data)
        setTotal(json.total)
      } catch (error) {
        console.error('Error fetching jobs:', error)
      } finally {
        setLoading(false)
      }
    }

    fetchJobs()
  }, [textToFilter, filters.technology, filters.location, filters.experienceLevel, currentPage])

  // Funciones para cambiar de página
  const nextPage = () => {
    if (currentPage < totalPages) {
      setCurrentPage(currentPage + 1)
    }
  }

  const prevPage = () => {
    if (currentPage > 1) {
      setCurrentPage(currentPage - 1)
    }
  }

  const goToPage = (page) => {
    setCurrentPage(page)
  }

  return (
    <div>
      {/* Filtros */}
      <SearchForm
        textToFilter={textToFilter}
        setTextToFilter={setTextToFilter}
        filters={filters}
        setFilters={setFilters}
      />

      {/* Resultados */}
      {loading ? (
        <div>Cargando empleos...</div>
      ) : (
        <>
          <JobListing jobs={jobs} />

          {/* Paginación */}
          <Pagination
            currentPage={currentPage}
            totalPages={totalPages}
            onNext={nextPage}
            onPrev={prevPage}
            onGoToPage={goToPage}
          />
        </>
      )}
    </div>
  )
}

Segundo problema: Reiniciar paginación al cambiar filtros

Cuando el usuario cambia un filtro, la paginación debe volver a la página 1.

El problema

Imagina este escenario:

  1. Usuario está en la página 5 de empleos de “JavaScript”
  2. Cambia el filtro a “Python”
  3. Si Python solo tiene 2 páginas de resultados, seguir en la página 5 no tiene sentido

La solución

Cuando cambien los filtros, reinicia la página actual:

// ✅ Reiniciar página cuando cambian los filtros
useEffect(() => {
  setCurrentPage(1)
}, [textToFilter, filters.technology, filters.location, filters.experienceLevel])

Implementación completa

function JobSearch() {
  const [currentPage, setCurrentPage] = useState(1)
  const [textToFilter, setTextToFilter] = useState('')
  const [filters, setFilters] = useState({
    technology: '',
    location: '',
    experienceLevel: '',
  })

  // Reiniciar paginación cuando cambien los filtros
  useEffect(() => {
    setCurrentPage(1)
  }, [textToFilter, filters.technology, filters.location, filters.experienceLevel])

  // Hacer la petición (este useEffect incluye currentPage en las dependencias)
  useEffect(() => {
    async function fetchJobs() {
      // ... código de fetch
    }

    fetchJobs()
  }, [textToFilter, filters.technology, filters.location, filters.experienceLevel, currentPage])

  // ... resto del componente
}

Flujo completo

1. Usuario escribe "JavaScript" en el buscador
   └─> textToFilter cambia
   └─> Primer useEffect ejecuta: setCurrentPage(1)
   └─> currentPage cambia a 1
   └─> Segundo useEffect ejecuta: hace fetch con página 1

2. Usuario navega a página 3
   └─> setCurrentPage(3)
   └─> currentPage cambia a 3
   └─> Segundo useEffect ejecuta: hace fetch con página 3

3. Usuario cambia filtro a "Python"
   └─> filters.technology cambia
   └─> Primer useEffect ejecuta: setCurrentPage(1)
   └─> currentPage cambia a 1
   └─> Segundo useEffect ejecuta: hace fetch con página 1 de Python

Probando la paginación

Para verificar que todo funciona correctamente:

Test 1: Navegación básica

  1. Carga la página
  2. Verifica que muestra “Página 1 de X”
  3. Haz clic en “Siguiente”
  4. Verifica que los empleos cambian
  5. Verifica que la URL en DevTools → Network tiene offset=10

Test 2: Filtros + Paginación

  1. Ve a la página 3
  2. Aplica un filtro (ej: “JavaScript”)
  3. Verifica que vuelve a la página 1
  4. Navega a la página 2
  5. Verifica que mantiene el filtro de JavaScript

Test 3: Total de páginas

  1. Sin filtros, verifica el total de páginas
  2. Aplica un filtro que reduzca resultados
  3. Verifica que el total de páginas se actualiza correctamente

Errores comunes y cómo evitarlos

Error 1: Loop infinito

// ❌ Error: incluir el objeto filters completo
useEffect(() => {
  // ...
}, [filters]) // Esto causa loop porque filters es un objeto nuevo en cada render

Solución: Incluir solo las propiedades que necesitas:

// ✅ Correcto
useEffect(() => {
  // ...
}, [filters.technology, filters.location, filters.experienceLevel])

Error 2: Usar jobs.length en lugar de total

// ❌ Error: jobs.length es solo la página actual
const totalPages = Math.ceil(jobs.length / RESULTS_PER_PAGE)

Solución:

// ✅ Correcto: total viene de la API
const totalPages = Math.ceil(total / RESULTS_PER_PAGE)

Error 3: No reiniciar la paginación al filtrar

// ❌ Problema: el usuario puede quedar en página 10 de algo que solo tiene 2 páginas

Solución:

// ✅ Reiniciar al cambiar filtros
useEffect(() => {
  setCurrentPage(1)
}, [textToFilter, filters.technology, filters.location, filters.experienceLevel])

Error 4: No incluir currentPage en las dependencias

// ❌ Error: el fetch no se ejecuta al cambiar de página
useEffect(() => {
  fetchJobs()
}, [textToFilter, filters.technology]) // Falta currentPage

Solución:

// ✅ Incluir currentPage
useEffect(() => {
  fetchJobs()
}, [textToFilter, filters.technology, filters.location, filters.experienceLevel, currentPage])

Verificando en las DevTools

Para ver que todo funciona correctamente, abre las DevTools:

Network tab

Al cambiar de página deberías ver peticiones como:

Página 1: /api/jobs?limit=10&offset=0
Página 2: /api/jobs?limit=10&offset=10
Página 3: /api/jobs?limit=10&offset=20

Con filtros:

/api/jobs?text=javascript&technology=react&limit=10&offset=20

Response

Verifica que la respuesta incluye:

{
  "total": 143,
  "limit": 10,
  "offset": 20,
  "results": 10,
  "data": [
    /* empleos */
  ]
}

Componente Pagination reutilizable

Puedes crear un componente reutilizable para la paginación:

function Pagination({ currentPage, totalPages, onNext, onPrev, onGoToPage }) {
  return (
    <div className="pagination">
      <button onClick={onPrev} disabled={currentPage === 1}>
        Anterior
      </button>

      <span>
        Página {currentPage} de {totalPages}
      </span>

      <button onClick={onNext} disabled={currentPage === totalPages}>
        Siguiente
      </button>

      {/* Opcional: números de página */}
      <div className="page-numbers">
        {Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
          <button
            key={page}
            onClick={() => onGoToPage(page)}
            className={page === currentPage ? 'active' : ''}
          >
            {page}
          </button>
        ))}
      </div>
    </div>
  )
}

Resumen

En esta clase has aprendido a:

  1. ✅ Entender cómo funcionan limit y offset en las APIs
  2. ✅ Calcular el offset a partir del número de página: (page - 1) × limit
  3. ✅ Corregir el error de usar jobs.length en lugar de total
  4. ✅ Reiniciar la paginación cuando cambian los filtros
  5. ✅ Incluir las dependencias correctas en useEffect
  6. ✅ Probar la paginación en las DevTools
  7. ✅ Evitar errores comunes (loops infinitos, cálculos incorrectos)

Conceptos clave

ConceptoDescripción
limitCuántos resultados devolver por página
offsetCuántos resultados saltarse desde el inicio
Fórmula offset(currentPage - 1) × resultsPerPage
totalTotal de resultados disponibles (de la API)
jobs.lengthSolo resultados de la página actual ⚠️
Reiniciar páginaVolver a página 1 cuando cambian los filtros

Próximos pasos

En las siguientes clases aprenderemos:

  1. Sincronizar paginación con la URL: Para que la página actual se refleje en la URL
  2. Debouncing en búsqueda: Evitar hacer peticiones en cada tecla
  3. Skeleton loading: Mejorar la UX mientras se cargan los datos

Conclusión

La paginación es fundamental en cualquier aplicación que trabaje con listados de datos. Has aprendido a implementarla correctamente usando los estándares de la industria (limit y offset), a evitar errores comunes, y a integrarla perfectamente con el sistema de filtros.

Ahora tu aplicación puede manejar grandes cantidades de datos de forma eficiente, cargando solo lo necesario en cada momento. ¡La paginación está completa y funcional!