Terminando la paginación
En las clases anteriores creamos el componente Pagination y aprendimos a pasar funciones como props y manejar el estado en el componente padre. Ahora vamos a conectar todo para que la paginación funcione completamente: calcularemos el número total de páginas y mostraremos solo los trabajos de la página actual.
Moviendo la importación de datos a App.jsx
Antes de continuar, necesitamos hacer un cambio importante en nuestra arquitectura. Hasta ahora, JobListings importaba directamente los datos del archivo jobs.json:
// src/components/JobListings.jsx (antes)
import jobsData from './data/jobs.json'
export function JobListings() {
return (
<div className="jobs-grid">
{jobsData.map((job) => (
<JobCard key={job.id} job={job} />
))}
</div>
)
}
Problema: Si los datos están dentro de JobListings, no podemos filtrarlos, paginarlos ni manipularlos desde App.jsx.
Solución: Mover la importación de los datos a App.jsx y pasarlos como prop a JobListings:
// src/App.jsx (ahora)
import jobsData from './data/jobs.json'
function App() {
// Ahora podemos filtrar, paginar o manipular los datos aquí
return (
<>
<Header />
<main>
<SearchForm />
<JobListings jobs={jobsData} />
<Pagination />
</main>
<Footer />
</>
)
}
// src/components/JobListings.jsx (ahora)
export function JobListings({ jobs }) {
// Recibe los datos desde arriba
return (
<div className="jobs-grid">
{jobs.map((job) => (
<JobCard key={job.id} job={job} />
))}
</div>
)
}
¿Por qué es mejor así?
Antes (datos en JobListings):
JobListings (tiene los datos)
↓
Renderiza JobCard
- ❌ No podemos filtrar los datos desde App
- ❌ No podemos paginar los datos desde App
- ❌ No podemos compartir los datos con otros componentes
Ahora (datos en App):
App (tiene los datos)
↓
├─→ Puede filtrar
├─→ Puede paginar
├─→ Puede calcular totales
↓
JobListings (solo renderiza)
↓
JobCard
- ✅ Tenemos control total sobre los datos
- ✅ Podemos aplicar paginación
- ✅ Podemos aplicar filtros
- ✅ Podemos pasar los datos a múltiples componentes
Principio de React: Los datos deben estar en el componente padre que necesita manipularlos. Los componentes hijos solo deben mostrar lo que reciben.
El código completo en App.jsx
Vamos a ver cómo queda el componente App con la paginación funcionando:
// src/App.jsx
import { useState } from 'react'
import { Header } from './components/Header'
import { Footer } from './components/Footer'
import { SearchForm } from './components/SearchForm'
import { JobListings } from './components/JobListings'
import Pagination from './components/Pagination'
import jobsData from './data/jobs.json'
const RESULTS_PER_PAGE = 5
function App() {
const [currentPage, setCurrentPage] = useState(1)
const totalPages = Math.ceil(jobsData.length / RESULTS_PER_PAGE)
const pagedResults = jobsData.slice(
(currentPage - 1) * RESULTS_PER_PAGE,
currentPage * RESULTS_PER_PAGE
)
const handlePageChange = (page) => {
setCurrentPage(page)
}
return (
<>
<Header />
<main>
<SearchForm />
<JobListings jobs={pagedResults} />
<Pagination
currentPage={currentPage}
totalPages={totalPages}
onPageChange={handlePageChange}
/>
</main>
<Footer />
</>
)
}
export default App
Vamos a desglosar cada parte para entenderla mejor.
Constante RESULTS_PER_PAGE
const RESULTS_PER_PAGE = 5
Definimos una constante con el número de resultados que queremos mostrar por página. Usar una constante nos permite:
- ✅ Cambiar fácilmente - Solo modificamos un valor
- ✅ Legibilidad - El código es más claro
- ✅ Evitar valores mágicos - No usamos
5directamente en varias partes
Convención: Las constantes suelen escribirse en MAYÚSCULAS_CON_GUIONES_BAJOS.
Calculando el total de páginas con Math.ceil()
const totalPages = Math.ceil(jobsData.length / RESULTS_PER_PAGE)
Necesitamos saber cuántas páginas totales tendremos. Para eso dividimos el total de trabajos entre los resultados por página.
Entendiendo Math.ceil()
Math.ceil() redondea hacia arriba al número entero más cercano:
Math.ceil(4.1) // 5
Math.ceil(4.5) // 5
Math.ceil(4.9) // 5
Math.ceil(5.0) // 5
¿Por qué usamos ceil()?
Porque si tenemos 23 trabajos y mostramos 5 por página:
23 / 5 = 4.6 páginas
No podemos tener 4.6 páginas, necesitamos 5 páginas completas:
- Página 1: 5 trabajos (1-5)
- Página 2: 5 trabajos (6-10)
- Página 3: 5 trabajos (11-15)
- Página 4: 5 trabajos (16-20)
- Página 5: 3 trabajos (21-23) ← Página incompleta pero necesaria
// ✅ Correcto: Redondeamos hacia arriba
const totalPages = Math.ceil(23 / 5) // 5
// ❌ Incorrecto: Perderíamos los últimos 3 trabajos
const totalPages = Math.floor(23 / 5) // 4
// ❌ Incorrecto: No tiene sentido 4.6 páginas
const totalPages = 23 / 5 // 4.6
Comparando Math.floor(), Math.ceil() y Math.round()
JavaScript tiene tres funciones para redondear números:
1. Math.floor() - Redondea hacia abajo
Siempre redondea al entero menor más cercano:
Math.floor(4.1) // 4
Math.floor(4.5) // 4
Math.floor(4.9) // 4
Math.floor(5.0) // 5
Math.floor(-4.9) // -5 (hacia abajo en la recta numérica)
Ejemplo de uso:
// Calcular edad a partir de años decimales
const edadExacta = 25.8
const edad = Math.floor(edadExacta) // 25 años completos
2. Math.ceil() - Redondea hacia arriba
Siempre redondea al entero mayor más cercano:
Math.ceil(4.1) // 5
Math.ceil(4.5) // 5
Math.ceil(4.9) // 5
Math.ceil(5.0) // 5
Math.ceil(-4.1) // -4 (hacia arriba en la recta numérica)
Ejemplo de uso:
// Calcular número de cajas necesarias
const productos = 23
const productosPorCaja = 10
const cajasNecesarias = Math.ceil(productos / productosPorCaja) // 3 cajas
3. Math.round() - Redondea al más cercano
Redondea al entero más cercano. Si está exactamente en el medio (.5), redondea hacia arriba:
Math.round(4.1) // 4
Math.round(4.4) // 4
Math.round(4.5) // 5 ← En el medio, redondea hacia arriba
Math.round(4.9) // 5
Math.round(5.0) // 5
Math.round(-4.5) // -4
Ejemplo de uso:
// Redondear precio al euro más cercano
const precioExacto = 19.67
const precioRedondeado = Math.round(precioExacto) // 20€
Tabla comparativa
| Número | floor() | round() | ceil() |
|---|---|---|---|
| 4.1 | 4 | 4 | 5 |
| 4.4 | 4 | 4 | 5 |
| 4.5 | 4 | 5 | 5 |
| 4.6 | 4 | 5 | 5 |
| 4.9 | 4 | 5 | 5 |
| 5.0 | 5 | 5 | 5 |
Ayuda visual:
floor() round() ceil()
↓ ↓ ↓
4 ←-------- 4.3 -----------→ 5 --------→ 5
4 ←-------- 4.5 ---------------→ 5 ----→ 5
4 ←-------- 4.7 -------------------→ 5 → 5
¿Cuál usar para la paginación?
Para calcular páginas totales, siempre usa Math.ceil():
// 23 trabajos, 5 por página
Math.floor(23 / 5) // ❌ 4 - Perdemos los últimos 3 trabajos
Math.round(23 / 5) // ❌ 5 - Funciona, pero no es semánticamente correcto
Math.ceil(23 / 5) // ✅ 5 - Correcto: necesitamos 5 páginas
Cortando los datos con Array.slice()
Ahora necesitamos obtener solo los trabajos de la página actual. Para eso usamos slice():
const pagedResults = jobsData.slice(
(currentPage - 1) * RESULTS_PER_PAGE, // inicio
currentPage * RESULTS_PER_PAGE // fin (no incluido)
)
¿Qué hace Array.slice()?
slice() extrae una porción del array sin modificar el array original:
array.slice(inicio, fin)
inicio: Índice donde empezar (incluido)fin: Índice donde terminar (no incluido)- Retorna un nuevo array con los elementos entre
inicioyfin
Ejemplos básicos:
const numeros = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
// Obtener los primeros 5 elementos
numeros.slice(0, 5) // [0, 1, 2, 3, 4]
// Obtener los elementos del índice 5 al 10
numeros.slice(5, 10) // [5, 6, 7, 8, 9]
// Obtener los últimos 3 elementos
numeros.slice(-3) // [7, 8, 9]
// Obtener todos menos el primero y el último
numeros.slice(1, -1) // [1, 2, 3, 4, 5, 6, 7, 8]
Importante: slice() no muta el array original:
const original = [1, 2, 3, 4, 5]
const copia = original.slice(1, 3) // [2, 3]
console.log(original) // [1, 2, 3, 4, 5] ← No cambió
console.log(copia) // [2, 3]
Calculando los índices para cada página
Veamos cómo calcular los índices de inicio y fin para cada página:
Con 23 trabajos y 5 por página:
const RESULTS_PER_PAGE = 5
Página 1
currentPage = 1
inicio = (1 - 1) * 5 = 0
fin = 1 * 5 = 5
jobsData.slice(0, 5) // Índices 0, 1, 2, 3, 4 (5 elementos)
Página 2
currentPage = 2
inicio = (2 - 1) * 5 = 5
fin = 2 * 5 = 10
jobsData.slice(5, 10) // Índices 5, 6, 7, 8, 9 (5 elementos)
Página 3
currentPage = 3
inicio = (3 - 1) * 5 = 10
fin = 3 * 5 = 15
jobsData.slice(10, 15) // Índices 10, 11, 12, 13, 14 (5 elementos)
Página 5 (última, incompleta)
currentPage = 5
inicio = (5 - 1) * 5 = 20
fin = 5 * 5 = 25
jobsData.slice(20, 25) // Índices 20, 21, 22 (3 elementos)
// No hay problema si `fin` es mayor que el tamaño del array
Tabla visual de la paginación
| Página | currentPage | inicio | fin | slice() | Trabajos mostrados |
|---|---|---|---|---|---|
| 1 | 1 | 0 | 5 | slice(0, 5) | 0-4 (5 trabajos) |
| 2 | 2 | 5 | 10 | slice(5, 10) | 5-9 (5 trabajos) |
| 3 | 3 | 10 | 15 | slice(10, 15) | 10-14 (5 trabajos) |
| 4 | 4 | 15 | 20 | slice(15, 20) | 15-19 (5 trabajos) |
| 5 | 5 | 20 | 25 | slice(20, 25) | 20-22 (3 trabajos) |
Ejemplo con datos reales
const jobsData = [
{ id: 1, title: 'Job 1' },
{ id: 2, title: 'Job 2' },
{ id: 3, title: 'Job 3' },
{ id: 4, title: 'Job 4' },
{ id: 5, title: 'Job 5' },
{ id: 6, title: 'Job 6' },
{ id: 7, title: 'Job 7' },
{ id: 8, title: 'Job 8' },
{ id: 9, title: 'Job 9' },
{ id: 10, title: 'Job 10' },
{ id: 11, title: 'Job 11' },
{ id: 12, title: 'Job 12' },
{ id: 13, title: 'Job 13' },
]
const RESULTS_PER_PAGE = 5
const currentPage = 2
const pagedResults = jobsData.slice(
(currentPage - 1) * RESULTS_PER_PAGE, // (2-1)*5 = 5
currentPage * RESULTS_PER_PAGE // 2*5 = 10
)
console.log(pagedResults)
// [
// { id: 6, title: 'Job 6' },
// { id: 7, title: 'Job 7' },
// { id: 8, title: 'Job 8' },
// { id: 9, title: 'Job 9' },
// { id: 10, title: 'Job 10' }
// ]
El flujo completo de la paginación
Vamos a ver cómo funciona todo junto:
1. Estado inicial
const [currentPage, setCurrentPage] = useState(1)
// currentPage = 1
2. Cálculo de páginas totales
const totalPages = Math.ceil(jobsData.length / RESULTS_PER_PAGE)
// totalPages = Math.ceil(23 / 5) = 5
3. Obtención de trabajos de la página actual
const pagedResults = jobsData.slice(
(currentPage - 1) * RESULTS_PER_PAGE, // (1-1)*5 = 0
currentPage * RESULTS_PER_PAGE // 1*5 = 5
)
// pagedResults = [trabajo1, trabajo2, trabajo3, trabajo4, trabajo5]
4. Renderizado
<JobListings jobs={pagedResults} />
// Muestra solo los 5 primeros trabajos
<Pagination
currentPage={1}
totalPages={5}
onPageChange={handlePageChange}
/>
// Muestra: [<] 1 2 3 4 5 [>]
5. Usuario hace clic en página 3
// Usuario hace clic en el botón "3"
// Se ejecuta:
handlePageChange(3)
// Que llama a:
setCurrentPage(3)
6. React re-renderiza
// currentPage ahora es 3
const pagedResults = jobsData.slice(
(3 - 1) * 5, // 10
3 * 5 // 15
)
// pagedResults = [trabajo11, trabajo12, trabajo13, trabajo14, trabajo15]
// Se renderiza de nuevo con los nuevos datos
<JobListings jobs={pagedResults} />
// Muestra los trabajos 11-15
<Pagination currentPage={3} totalPages={5} onPageChange={handlePageChange} />
// Muestra: [<] 1 2 *3* 4 5 [>]
// ↑
// página activa
Diagrama del flujo de datos
╔════════════════════════════════════╗
║ jobsData (23 items) ║
╚════════════════════════════════════╝
↓
╔════════════════════════════════════╗
║ totalPages = ceil(23/5) = 5 ║
╚════════════════════════════════════╝
↓
╔════════════════════════════════════╗
║ currentPage = 1 (estado) ║
╚════════════════════════════════════╝
↓
╔════════════════════════════════════╗
║ pagedResults = slice(0, 5) ║
║ [trabajo1, trabajo2, ... trabajo5]║
╚════════════════════════════════════╝
↓
┌─────────────────┐
│ JobListings │
│ (5 trabajos) │
└─────────────────┘
↓
┌─────────────────┐
│ Pagination │
│ [<] 1 2 3 4 5 [>] │
└─────────────────┘
↓
Usuario hace clic en "3"
↓
onPageChange(3)
↓
setCurrentPage(3)
↓
RE-RENDER
↓
pagedResults = slice(10, 15)
↓
JobListings muestra trabajos 11-15
Ventajas de esta implementación
✅ Separación de responsabilidades
// App.jsx - Maneja la lógica de datos
const pagedResults = jobsData.slice(...)
// JobListings.jsx - Solo renderiza lo que recibe
<JobListings jobs={pagedResults} />
// Pagination.jsx - Solo maneja la navegación
<Pagination currentPage={...} onPageChange={...} />
✅ Reutilizable
El componente Pagination puede usarse con cualquier tipo de datos:
// Para trabajos
<Pagination currentPage={1} totalPages={5} onPageChange={handlePageChange} />
// Para productos
<Pagination
currentPage={productPage}
totalPages={productTotalPages}
onPageChange={handleProductPageChange}
/>
// Para artículos
<Pagination
currentPage={articlePage}
totalPages={articleTotalPages}
onPageChange={handleArticlePageChange}
/>
✅ Eficiente
- Solo renderizamos los trabajos necesarios
slice()no muta el array original- No cargamos todos los trabajos de golpe
Mejoras posibles
1. Scroll al inicio al cambiar de página
const handlePageChange = (page) => {
setCurrentPage(page)
window.scrollTo({ top: 0, behavior: 'smooth' })
}
2. Persistir página en la URL
// Usando React Router
const [searchParams, setSearchParams] = useSearchParams()
const currentPage = Number(searchParams.get('page') || '1')
const handlePageChange = (page) => {
setSearchParams({ page })
}
// URL: /jobs?page=3
3. Desactivar botones en los extremos
<Pagination
currentPage={currentPage}
totalPages={totalPages}
onPageChange={handlePageChange}
disablePrevious={currentPage === 1}
disableNext={currentPage === totalPages}
/>
4. Mostrar rango de resultados
<p>
Mostrando {(currentPage - 1) * RESULTS_PER_PAGE + 1} -{' '}
{Math.min(currentPage * RESULTS_PER_PAGE, jobsData.length)} de {jobsData.length} trabajos
</p>
// Página 2: "Mostrando 6 - 10 de 23 trabajos"
Errores comunes
❌ Olvidar restar 1 en el cálculo de inicio
// ❌ Incorrecto
const pagedResults = jobsData.slice(
currentPage * RESULTS_PER_PAGE, // Página 1 empezaría en índice 5
(currentPage + 1) * RESULTS_PER_PAGE
)
// ✅ Correcto
const pagedResults = jobsData.slice(
(currentPage - 1) * RESULTS_PER_PAGE, // Página 1 empieza en índice 0
currentPage * RESULTS_PER_PAGE
)
❌ Usar Math.floor() o Math.round() para páginas totales
// ❌ Incorrecto: Perdemos la última página parcial
const totalPages = Math.floor(jobsData.length / RESULTS_PER_PAGE)
// ❌ Incorrecto: Puede redondear mal
const totalPages = Math.round(jobsData.length / RESULTS_PER_PAGE)
// ✅ Correcto: Siempre redondea hacia arriba
const totalPages = Math.ceil(jobsData.length / RESULTS_PER_PAGE)
❌ Mutar el array con splice()
// ❌ Incorrecto: splice() MUTA el array original
const pagedResults = jobsData.splice(0, 5)
// ✅ Correcto: slice() crea un nuevo array
const pagedResults = jobsData.slice(0, 5)
Diferencia entre slice() y splice():
const original = [1, 2, 3, 4, 5]
// slice() - NO muta, retorna nueva copia
const copia = original.slice(1, 3) // [2, 3]
console.log(original) // [1, 2, 3, 4, 5] ← No cambió
// splice() - SÍ muta, modifica el original
const eliminados = original.splice(1, 2) // [2, 3]
console.log(original) // [1, 4, 5] ← Cambió!
Lo que hemos visto en esta clase
- 📊 Paginación completa - Cálculo de páginas y corte de datos
- 🔢 Math.ceil() - Redondear hacia arriba para páginas totales
- 🔍 Math.floor(), Math.round(), Math.ceil() - Diferencias entre las funciones de redondeo
- ✂️ Array.slice() - Extraer porción de array sin mutar
- 🧮 Cálculo de índices - Fórmula:
(page-1) * sizeypage * size - 📈 Flujo completo - De datos a renderizado y actualización
- ⚡ Optimización - Solo renderizar datos necesarios
- 🎯 Separación de responsabilidades - Cada componente hace una cosa
En la próxima clase veremos más conceptos avanzados de React que te ayudarán a construir aplicaciones más complejas.
💡 Recuerda: Para paginación siempre usa
Math.ceil()para el total de páginas yslice()para cortar los datos. La fórmula clave es: inicio =(página - 1) * tamaño, fin =página * tamaño.