Aprende a implementar la paginación completa con cálculo de páginas totales y corte de datos con slice
midudev
Más clases y directos en YouTube
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.jsximport { 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 = 5function 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) // 5Math.ceil(4.5) // 5Math.ceil(4.9) // 5Math.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 arribaconst totalPages = Math.ceil(23 / 5) // 5// ❌ Incorrecto: Perderíamos los últimos 3 trabajosconst totalPages = Math.floor(23 / 5) // 4// ❌ Incorrecto: No tiene sentido 4.6 páginasconst 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) // 4Math.floor(4.5) // 4Math.floor(4.9) // 4Math.floor(5.0) // 5Math.floor(-4.9) // -5 (hacia abajo en la recta numérica)
Ejemplo de uso:
// Calcular edad a partir de años decimalesconst edadExacta = 25.8const 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) // 5Math.ceil(4.5) // 5Math.ceil(4.9) // 5Math.ceil(5.0) // 5Math.ceil(-4.1) // -4 (hacia arriba en la recta numérica)
Ejemplo de uso:
// Calcular número de cajas necesariasconst productos = 23const productosPorCaja = 10const 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) // 4Math.round(4.4) // 4Math.round(4.5) // 5 ← En el medio, redondea hacia arribaMath.round(4.9) // 5Math.round(5.0) // 5Math.round(-4.5) // -4
Ejemplo de uso:
// Redondear precio al euro más cercanoconst precioExacto = 19.67const 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áginaMath.floor(23 / 5) // ❌ 4 - Perdemos los últimos 3 trabajosMath.round(23 / 5) // ❌ 5 - Funciona, pero no es semánticamente correctoMath.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 elementosnumeros.slice(0, 5) // [0, 1, 2, 3, 4]// Obtener los elementos del índice 5 al 10numeros.slice(5, 10) // [5, 6, 7, 8, 9]// Obtener los últimos 3 elementosnumeros.slice(-3) // [7, 8, 9]// Obtener todos menos el primero y el últimonumeros.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 = 1inicio = (1 - 1) * 5 = 0fin = 1 * 5 = 5jobsData.slice(0, 5) // Índices 0, 1, 2, 3, 4 (5 elementos)
Página 2
currentPage = 2inicio = (2 - 1) * 5 = 5fin = 2 * 5 = 10jobsData.slice(5, 10) // Índices 5, 6, 7, 8, 9 (5 elementos)
Página 3
currentPage = 3inicio = (3 - 1) * 5 = 10fin = 3 * 5 = 15jobsData.slice(10, 15) // Índices 10, 11, 12, 13, 14 (5 elementos)
Página 5 (última, incompleta)
currentPage = 5inicio = (5 - 1) * 5 = 20fin = 5 * 5 = 25jobsData.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 = 5const currentPage = 2const 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 3const 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 datosconst 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 Routerconst [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
// ❌ Incorrectoconst pagedResults = jobsData.slice( currentPage * RESULTS_PER_PAGE, // Página 1 empezaría en índice 5 (currentPage + 1) * RESULTS_PER_PAGE)// ✅ Correctoconst 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 parcialconst totalPages = Math.floor(jobsData.length / RESULTS_PER_PAGE)// ❌ Incorrecto: Puede redondear malconst totalPages = Math.round(jobsData.length / RESULTS_PER_PAGE)// ✅ Correcto: Siempre redondea hacia arribaconst totalPages = Math.ceil(jobsData.length / RESULTS_PER_PAGE)
❌ Mutar el array con splice()
// ❌ Incorrecto: splice() MUTA el array originalconst pagedResults = jobsData.splice(0, 5)// ✅ Correcto: slice() crea un nuevo arrayconst pagedResults = jobsData.slice(0, 5)
Diferencia entre slice() y splice():
const original = [1, 2, 3, 4, 5]// slice() - NO muta, retorna nueva copiaconst copia = original.slice(1, 3) // [2, 3]console.log(original) // [1, 2, 3, 4, 5] ← No cambió// splice() - SÍ muta, modifica el originalconst 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.
