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ámetro | Descripción | Ejemplo |
|---|---|---|
limit | Cuántos resultados devolver por página | 10 |
offset | Cuántos resultados saltarse desde el inicio | 0, 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 actual | Resultados por página | Offset | Cálculo |
|---|---|---|---|
| 1 | 10 | 0 | (1-1) × 10 = 0 |
| 2 | 10 | 10 | (2-1) × 10 = 10 |
| 3 | 10 | 20 | (3-1) × 10 = 20 |
| 5 | 20 | 80 | (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
- Calculamos el offset antes de hacer la petición
- Añadimos
limityoffseta los parámetros de la URL - Incluimos
currentPageen las dependencias deluseEffectpara 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.lengthes el número de resultados de la página actual (por ejemplo, 10)- Si hay 143 empleos totales,
jobs.lengthsiempre 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:
- Usuario está en la página 5 de empleos de “JavaScript”
- Cambia el filtro a “Python”
- 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
- Carga la página
- Verifica que muestra “Página 1 de X”
- Haz clic en “Siguiente”
- Verifica que los empleos cambian
- Verifica que la URL en DevTools → Network tiene
offset=10
Test 2: Filtros + Paginación
- Ve a la página 3
- Aplica un filtro (ej: “JavaScript”)
- Verifica que vuelve a la página 1
- Navega a la página 2
- Verifica que mantiene el filtro de JavaScript
Test 3: Total de páginas
- Sin filtros, verifica el total de páginas
- Aplica un filtro que reduzca resultados
- 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:
- ✅ Entender cómo funcionan
limityoffseten las APIs - ✅ Calcular el offset a partir del número de página:
(page - 1) × limit - ✅ Corregir el error de usar
jobs.lengthen lugar detotal - ✅ Reiniciar la paginación cuando cambian los filtros
- ✅ Incluir las dependencias correctas en
useEffect - ✅ Probar la paginación en las DevTools
- ✅ Evitar errores comunes (loops infinitos, cálculos incorrectos)
Conceptos clave
| Concepto | Descripción |
|---|---|
| limit | Cuántos resultados devolver por página |
| offset | Cuántos resultados saltarse desde el inicio |
| Fórmula offset | (currentPage - 1) × resultsPerPage |
| total | Total de resultados disponibles (de la API) |
| jobs.length | Solo resultados de la página actual ⚠️ |
| Reiniciar página | Volver a página 1 cuando cambian los filtros |
Próximos pasos
En las siguientes clases aprenderemos:
- Sincronizar paginación con la URL: Para que la página actual se refleje en la URL
- Debouncing en búsqueda: Evitar hacer peticiones en cada tecla
- 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!