Saltar al contenido principal
Logo de JSCamp

Terminando la paginación

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

Suscribirse

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 totalesJobListings (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 5 directamente 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 inicio y fin

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) * size y page * 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 y slice() para cortar los datos. La fórmula clave es: inicio = (página - 1) * tamaño, fin = página * tamaño.