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 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) // 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úmerofloor()round()ceil()
4.1445
4.4445
4.5455
4.6455
4.9455
5.0555

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 inicio y fin

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áginacurrentPageiniciofinslice()Trabajos mostrados
1105slice(0, 5)0-4 (5 trabajos)
22510slice(5, 10)5-9 (5 trabajos)
331015slice(10, 15)10-14 (5 trabajos)
441520slice(15, 20)15-19 (5 trabajos)
552025slice(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) * 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.