Callbacks - Pasar Funciones como Props

En la clase anterior creamos el componente Pagination que recibe props con datos. Ahora vamos a aprender a pasar funciones como props para que el componente hijo pueda comunicarse con el padre y notificarle cuando algo cambia.

El problema: Componente sin interacción

Hasta ahora nuestro Pagination solo muestra información, pero no hace nada cuando haces click:

function Pagination({ currentPage = 1, totalPages = 5 }) {
  const pages = Array.from({ length: totalPages }, (_, i) => i + 1)

  return (
    <nav className="pagination">
      <a href="#"></a>

      {pages.map((page) => (
        <a key={page} className={currentPage === page ? 'is-active' : ''} href="#">
          {page}
        </a>
      ))}

      <a href="#"></a>
    </nav>
  )
}

Problema: Cuando haces click en un botón, no pasa nada. ¿Cómo hacemos que el componente reaccione?

Pasando funciones como props

La solución es que el padre le pase una función al hijo mediante props:

// src/App.jsx
function App() {
  const currentPage = 3
  const totalPages = 5

  const handlePageChange = (page) => {
    console.log('Página cambiada a:', page)
  }

  return (
    <>
      <Header />

      <main>
        <SearchForm />
        <JobListings />
        <Pagination
          currentPage={currentPage}
          totalPages={totalPages}
          onPageChange={handlePageChange}
        />
      </main>

      <Footer />
    </>
  )
}

export default App

¿Qué acabamos de hacer?

  1. Creamos una función handlePageChange en el padre
  2. Pasamos esta función como prop onPageChange={handlePageChange}
  3. Ahora Pagination tiene acceso a esta función

Convención: Las props que son funciones suelen empezar con on:

  • onClick
  • onChange
  • onSubmit
  • onPageChange
  • onDelete
  • onUpdate

Recibiendo y usando la función en el hijo

Ahora el hijo recibe la función y puede llamarla:

function Pagination({ currentPage = 1, totalPages = 5, onPageChange }) {
  const pages = Array.from({ length: totalPages }, (_, i) => i + 1)

  const handlePrevious = (e) => {
    e.preventDefault()
    if (currentPage > 1) {
      onPageChange(currentPage - 1) // ← Llamamos a la función del padre
    }
  }

  const handleNext = (e) => {
    e.preventDefault()
    if (currentPage < totalPages) {
      onPageChange(currentPage + 1) // ← Llamamos a la función del padre
    }
  }

  const handlePageClick = (e, page) => {
    e.preventDefault()
    onPageChange(page) // ← Llamamos a la función del padre
  }

  const styleLinkLeft = {
    opacity: currentPage === 1 ? 0.5 : 1,
    cursor: currentPage === 1 ? 'not-allowed' : 'pointer',
  }

  const styleLinkRight = {
    opacity: currentPage === totalPages ? 0.5 : 1,
    cursor: currentPage === totalPages ? 'not-allowed' : 'pointer',
  }

  return (
    <nav className="pagination">
      <a href="#" style={styleLinkLeft} onClick={handlePrevious}>
        <svg
          width="16"
          height="16"
          viewBox="0 0 24 24"
          fill="none"
          stroke="currentColor"
          strokeWidth="2"
          strokeLinecap="round"
          strokeLinejoin="round"
        >
          <path stroke="none" d="M0 0h24v24H0z" fill="none" />
          <path d="M15 6l-6 6l6 6" />
        </svg>
      </a>

      {pages.map((page) => (
        <a
          key={page}
          className={currentPage === page ? 'is-active' : ''}
          href="#"
          onClick={(e) => handlePageClick(e, page)}
        >
          {page}
        </a>
      ))}

      <a href="#" style={styleLinkRight} onClick={handleNext}>
        <svg
          width="16"
          height="16"
          viewBox="0 0 24 24"
          fill="none"
          stroke="currentColor"
          strokeWidth="1.5"
          strokeLinecap="round"
          strokeLinejoin="round"
        >
          <path stroke="none" d="M0 0h24v24H0z" fill="none" />
          <path d="M9 6l6 6l-6 6" />
        </svg>
      </a>
    </nav>
  )
}

export default Pagination

¿Qué hace cada función?

handlePrevious

const handlePrevious = (e) => {
  e.preventDefault() // Evita que el <a> navegue
  if (currentPage > 1) {
    onPageChange(currentPage - 1) // Notifica al padre
  }
}
  • Verifica que no estemos en la primera página
  • Llama a onPageChange (la función del padre) con la página anterior

handleNext

const handleNext = (e) => {
  e.preventDefault()
  if (currentPage < totalPages) {
    onPageChange(currentPage + 1) // Notifica al padre
  }
}
  • Verifica que no estemos en la última página
  • Llama a onPageChange con la página siguiente

handlePageClick

const handlePageClick = (e, page) => {
  e.preventDefault()
  onPageChange(page) // Notifica al padre con la página clickeada
}
  • Llama a onPageChange con el número de página específico

¿Qué es un callback?

Un callback es una función que se pasa como argumento a otra función, y que será llamada más tarde (de ahí el nombre “call back” = “llamar de vuelta”).

// onPageChange es un callback
<Pagination onPageChange={handlePageChange} />

En este caso:

  1. El padre (App) pasa handlePageChange como callback
  2. El hijo (Pagination) guarda este callback en la prop onPageChange
  3. Cuando el usuario hace click, el hijo “llama de vuelta” al padre ejecutando onPageChange(page)

El flujo de comunicación

Ahora que tenemos el callback configurado, veamos qué pasa cuando el usuario interactúa:

1. Usuario hace click en "Página 3"

2. Se dispara handlePageClick(e, 3) en Pagination

3. handlePageClick llama a onPageChange(3)

4. onPageChange es handlePageChange del padre (App)

5. handlePageChange(3) ejecuta console.log('Página cambiada a:', 3)

Por ahora solo imprime en consola, pero en la próxima clase veremos cómo usar estado para que realmente cambie la página visualmente.

Convención de nombres

Es común usar estas convenciones para funciones:

En el componente padre

// Función que se pasa como prop: onAlgo
<Pagination onPageChange={handlePageChange} />
<Button onClick={handleClick} />
<Form onSubmit={handleSubmit} />

// Función que maneja el evento: handleAlgo
const handlePageChange = (page) => { ... }
const handleClick = () => { ... }
const handleSubmit = (data) => { ... }

En el componente hijo

// Recibir: onAlgo
function Pagination({ onPageChange }) {
  // Función interna: handleAlgo
  const handleNext = () => {
    onPageChange(currentPage + 1)
  }
}

Patrón:

  • on + Evento → props que son funciones
  • handle + Evento → funciones que manejan eventos

Validando que la prop sea una función

Es buena práctica validar que las funciones prop realmente sean funciones:

function Pagination({ currentPage = 1, totalPages = 5, onPageChange }) {
  // Validar que onPageChange sea una función
  if (!onPageChange || typeof onPageChange !== 'function') {
    console.warn('Pagination: onPageChange debe ser una función')
    onPageChange = () => {} // Función vacía por defecto
  }

  // resto del código...
}

O podemos usar un valor por defecto:

function Pagination({ currentPage = 1, totalPages = 5, onPageChange = () => {} }) {
  // Si no se pasa onPageChange, usa función vacía
  // ...
}

Ejemplo adicional: Button con callback

Veamos otro ejemplo para reforzar el concepto:

// Componente Button reutilizable
function Button({ children, onClick, variant = 'primary' }) {
  return (
    <button className={`btn btn-${variant}`} onClick={onClick}>
      {children}
    </button>
  )
}

// Uso en App
function App() {
  const handleDelete = () => {
    console.log('Eliminando...')
  }

  const handleSave = () => {
    console.log('Guardando...')
  }

  return (
    <div>
      <Button onClick={handleSave}>Guardar</Button>
      <Button onClick={handleDelete} variant="danger">
        Eliminar
      </Button>
    </div>
  )
}

El patrón es el mismo:

  1. El padre define funciones (handleSave, handleDelete)
  2. El padre pasa las funciones como props (onClick={handleSave})
  3. El hijo recibe y ejecuta las funciones cuando ocurre el evento

Pasando argumentos a callbacks

A veces necesitas pasar información adicional:

function JobCard({ job, onApply }) {
  return (
    <article>
      <h3>{job.title}</h3>
      <button onClick={() => onApply(job.id)}>Aplicar</button>
    </article>
  )
}

function App() {
  const handleApply = (jobId) => {
    console.log('Aplicando a trabajo:', jobId)
  }

  return <JobCard job={job} onApply={handleApply} />
}

Nota: Usamos una arrow function onClick={() => onApply(job.id)} para poder pasar argumentos.

Diferencia: onClick vs onClick()

Es importante entender esta diferencia:

// ✅ Correcto: Pasa la función
<button onClick={handleClick}>Click</button>

// ❌ Incorrecto: EJECUTA la función inmediatamente
<button onClick={handleClick()}>Click</button>

// ✅ Correcto: Arrow function para pasar argumentos
<button onClick={() => handleClick(id)}>Click</button>

// ✅ Correcto: Pasar referencia directa sin argumentos
<button onClick={handleClick}>Click</button>

Regla:

  • onClick={handleClick} → Pasa la referencia a la función
  • onClick={handleClick()}Ejecuta la función inmediatamente (¡no queremos esto!)
  • onClick={() => handleClick(arg)} → Crea una nueva función que ejecutará handleClick con argumentos

¡Resumiendo que es gerundio!

En esta clase has aprendido:

  • 🔄 Callbacks - Funciones pasadas como props
  • 📤 Comunicación hijo → padre - El hijo llama a funciones del padre
  • 🎯 Convención on/handle - onEvent para props, handleEvent para funciones
  • Validación - Verificar que las props sean funciones
  • 🔀 Flujo de comunicación - El hijo notifica al padre mediante callbacks
  • onClick vs onClick() - Diferencia crucial entre referencia y ejecución
  • 📝 Ejemplo práctico - Button reutilizable con onClick
  • 🎯 Pasar argumentos - Usar arrow functions para pasar datos

En la próxima clase completaremos el ciclo aprendiendo sobre estado en el componente padre, haciendo que los cambios se reflejen visualmente en la interfaz.

💡 Recuerda: Las funciones como props (callbacks) permiten que los componentes hijos notifiquen a sus padres sobre eventos. El hijo no modifica datos directamente, solo informa al padre que algo ocurrió llamando a la función callback.