Estado por Props - Lifting State Up

En la clase anterior aprendimos a pasar funciones como props para que el hijo notifique al padre. Ahora vamos a completar el ciclo: crear estado en el padre y pasarlo como props al hijo, haciendo que todo funcione de forma reactiva.

El problema: Estado fijo

Hasta ahora, currentPage es un valor fijo que no cambia:

// src/App.jsx
function App() {
  const currentPage = 3 // ← Valor fijo, nunca cambia
  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

Problema: Aunque el callback se ejecuta, la página no cambia visualmente porque currentPage siempre es 3.

Agregando estado con useState

Para que currentPage pueda cambiar, necesitamos usar estado:

import { useState } from 'react'
//...

function App() {
  const [currentPage, setCurrentPage] = useState(1)
  const totalPages = 5

  const handlePageChange = (page) => {
    setCurrentPage(page)
  }

  return (
    <>
      <Header />

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

      <Footer />
    </>
  )
}

export default App

¿Qué cambió?

Antes (valor fijo)

const currentPage = 3 // Nunca cambia

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

Después (con estado)

const [currentPage, setCurrentPage] = useState(1) // Estado reactivo

const handlePageChange = (page) => {
  setCurrentPage(page) // Actualiza el estado
}

Ahora sí funciona:

  1. Usuario hace click en “Página 3”
  2. Pagination llama a onPageChange(3)
  3. handlePageChange(3) llama a setCurrentPage(3)
  4. El estado cambia de 1 a 3
  5. React re-renderiza App con el nuevo valor
  6. Pagination recibe currentPage={3} como prop
  7. El botón “3” se muestra activo

Entendiendo los renderizados

Vamos a agregar console.log para ver cuándo se renderiza cada componente:

// src/App.jsx

function App() {
  console.log('🔵 App renderizado')

  const [currentPage, setCurrentPage] = useState(1)
  const totalPages = 5

  // ...
}
// src/components/Pagination.jsx
function Pagination({ currentPage = 1, totalPages = 5, onPageChange }) {
  console.log('🟢 Pagination renderizado', { currentPage })

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

  const handlePrevious = (e) => {
    e.preventDefault()
    if (currentPage > 1) {
      onPageChange(currentPage - 1)
    }
  }

  const handleNext = (e) => {
    e.preventDefault()
    if (currentPage < totalPages) {
      onPageChange(currentPage + 1)
    }
  }

  const handlePageClick = (e, page) => {
    e.preventDefault()
    onPageChange(page)
  }

  // ... resto del componente
}

Al abrir la aplicación verás en la consola:

🔵 App renderizado
🟢 Pagination renderizado { currentPage: 1 }
🔵 App renderizado
🟢 Pagination renderizado { currentPage: 1 }

¿Por qué aparece dos veces? 🤔

React.StrictMode: Renderizados dobles

En main.jsx probablemente tengas esto:

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
)

<React.StrictMode> es un componente especial que:

  • Solo funciona en desarrollo (no en producción)
  • Renderiza los componentes dos veces intencionalmente
  • Ayuda a detectar problemas potenciales en tu código
  • Verifica que tus componentes sean funciones puras

¿Por qué renderiza dos veces?

React quiere asegurarse de que tus componentes sean funciones puras, es decir, que:

  • Dado las mismas props/estado → siempre retornen el mismo resultado
  • No tengan efectos secundarios inesperados

Renderizar dos veces ayuda a detectar:

  • Componentes que modifican variables externas
  • Componentes que dependen de efectos secundarios
  • Código que no es idempotente

Ejemplo de problema que detectaría:

// ❌ Mal: Modifica variable externa
let counter = 0

function BadComponent() {
  counter++ // ← ¡Efecto secundario!
  return <div>Counter: {counter}</div>
}

En StrictMode, este componente mostraría valores inconsistentes porque counter se incrementa dos veces.

¿Debo preocuparme?

No. Es completamente normal y esperado:

  • ✅ En desarrollo: Ves renderizados dobles (está bien)
  • ✅ En producción: Solo renderiza una vez
  • ✅ Es una herramienta de ayuda, no un bug

Para verlo sin StrictMode:

// main.jsx
ReactDOM.createRoot(document.getElementById('root')).render(<App />)

Ahora solo verás:

🔵 App renderizado
🟢 Pagination renderizado { currentPage: 1 }

💡 Recomendación: Deja StrictMode activado en desarrollo. Te ayudará a escribir código más robusto.

Observando cambios de estado

Ahora, cuando hagas click en “Página 3”, verás en la consola:

🔵 App renderizado
🟢 Pagination renderizado { currentPage: 3 }

¿Qué pasó?

  1. Click en “Página 3”
  2. Se llama a setCurrentPage(3)
  3. React detecta que el estado cambió
  4. React re-renderiza App (por eso ves el log azul)
  5. React re-renderiza Pagination con la nueva prop (por eso ves el log verde)

Importante: React solo re-renderiza los componentes necesarios. Si tienes otros componentes que no dependen de currentPage, no se re-renderizarán.

El flujo completo con estado

Veamos el flujo completo paso a paso:

Montaje inicial (Primera carga)

1. React monta App

2. console.log('🔵 App renderizado')

3. useState(1) crea el estado currentPage = 1

4. App retorna JSX incluyendo <Pagination currentPage={1} />

5. React monta Pagination

6. console.log('🟢 Pagination renderizado', { currentPage: 1 })

7. Pagination retorna JSX con botones

8. Todo se renderiza en pantalla

Cuando haces click (Actualización)

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

2. handlePageClick llama a onPageChange(3)

3. handlePageChange llama a setCurrentPage(3)

4. React detecta cambio de estado: 1 → 3

5. React re-renderiza App

6. console.log('🔵 App renderizado')

7. App retorna JSX con <Pagination currentPage={3} />

8. React detecta que Pagination recibió nueva prop

9. React re-renderiza Pagination

10. console.log('🟢 Pagination renderizado', { currentPage: 3 })

11. Pagination retorna JSX con botón "3" activo

12. React actualiza el DOM (solo lo que cambió)

Lifting State Up (Elevar el estado)

Este patrón se llama “Lifting State Up” (elevar el estado):

Antes: Cada componente tiene su propio estado aislado

function Pagination() {
  const [currentPage, setCurrentPage] = useState(1) // Estado local
  // ...
}

function JobListings() {
  const [currentPage, setCurrentPage] = useState(1) // Estado duplicado!
  // ...
}

Problema: Los dos componentes no están sincronizados.

Después: El estado vive en el ancestro común

function App() {
  const [currentPage, setCurrentPage] = useState(1) // Estado compartido

  return (
    <>
      <JobListings page={currentPage} />
      <Pagination currentPage={currentPage} onPageChange={setCurrentPage} />
    </>
  )
}

Ventajas:

  • ✅ Una sola fuente de verdad
  • ✅ Los componentes están sincronizados
  • ✅ Más fácil de mantener
  • ✅ Más fácil de depurar

¿Por qué el estado vive en App?

Pregunta importante: ¿Por qué no poner el estado en Pagination?

// ❌ Estado en Pagination (problema)
function Pagination({ totalPages, onPageChange }) {
  const [currentPage, setCurrentPage] = useState(1)

  const handleClick = (page) => {
    setCurrentPage(page) // Actualiza estado local
    onPageChange(page) // Notifica al padre
  }

  // ...
}

Problemas:

  1. Duplicación: Si JobListings también necesita la página, tendrías dos estados separados
  2. Sincronización: ¿Qué pasa si el padre quiere cambiar la página desde fuera?
  3. Single Source of Truth: React recomienda una sola fuente de verdad para cada dato

Solución correcta:

// ✅ Estado en App (correcto)
function App() {
  const [currentPage, setCurrentPage] = useState(1)

  return (
    <>
      {/* Ambos usan el mismo estado */}
      <JobListings page={currentPage} />
      <Pagination currentPage={currentPage} onPageChange={setCurrentPage} />
    </>
  )
}

Componentes controlados vs no controlados

Componente controlado (recomendado)

El valor está controlado por el padre mediante props:

// Pagination es un componente controlado
function Pagination({ currentPage, onPageChange }) {
  // currentPage viene del padre
  // No hay estado local
  // ...
}

Ventajas:

  • ✅ El padre tiene control total
  • ✅ Más predecible
  • ✅ Más fácil de testear

Componente no controlado

El valor está controlado por el componente con estado interno:

// Pagination es un componente no controlado
function Pagination() {
  const [currentPage, setCurrentPage] = useState(1)
  // Maneja su propio estado
  // ...
}

Desventajas:

  • ⚠️ El padre no tiene control
  • ⚠️ Difícil sincronizar con otros componentes
  • ⚠️ Más difícil de depurar

Regla general: Prefiere componentes controlados cuando necesites compartir el estado.

Simplificando el código

Podemos simplificar handlePageChange:

Versión verbosa

const handlePageChange = (page) => {
  setCurrentPage(page)
}

// <Pagination onPageChange={handlePageChange} />

Versión simplificada

<Pagination onPageChange={setCurrentPage} />

¿Por qué funciona?

  • setCurrentPage ya es una función que acepta un valor
  • No necesitamos crear otra función que solo llame a setCurrentPage
  • Ambas versiones son equivalentes

Logs más informativos

Podemos mejorar los logs para ver más información:

function App() {
  const [currentPage, setCurrentPage] = useState(1)

  console.log('🔵 App renderizado', {
    currentPage,
    timestamp: new Date().toLocaleTimeString(),
  })

  // ...
}
function Pagination({ currentPage = 1, totalPages = 5, onPageChange }) {
  console.log('🟢 Pagination renderizado', {
    currentPage,
    totalPages,
    timestamp: new Date().toLocaleTimeString(),
  })

  // ...
}

Ahora verás:

🔵 App renderizado { currentPage: 1, timestamp: '10:30:45' }
🟢 Pagination renderizado { currentPage: 1, totalPages: 5, timestamp: '10:30:45' }

Cuando hagas click:

🔵 App renderizado { currentPage: 3, timestamp: '10:30:47' }
🟢 Pagination renderizado { currentPage: 3, totalPages: 5, timestamp: '10:30:47' }

Optimización: Evitar renderizados innecesarios

Si totalPages nunca cambia, podemos hacer que Pagination solo se re-renderice cuando currentPage cambia:

import { memo } from 'react'

const Pagination = memo(function Pagination({ currentPage, totalPages, onPageChange }) {
  console.log('🟢 Pagination renderizado', { currentPage })

  // ... resto del código
})

export default Pagination

memo:

  • Memoriza el resultado del componente
  • Solo re-renderiza si las props cambiaron
  • Optimización de rendimiento

💡 Nota: No abuses de memo. Úsalo solo cuando tengas problemas de rendimiento reales.

Patrón completo: Estado → Props → Callback

Este es el patrón fundamental de React:

function App() {
  // 1. Estado vive en el padre
  const [currentPage, setCurrentPage] = useState(1)

  return (
    <Pagination
      // 2. Estado se pasa como prop
      currentPage={currentPage}
      // 3. Setter se pasa como callback
      onPageChange={setCurrentPage}
    />
  )
}

function Pagination({ currentPage, onPageChange }) {
  // 4. Hijo recibe el estado como prop
  // 5. Hijo llama al callback para actualizar
  return <button onClick={() => onPageChange(2)}>Ir a página 2</button>
}

Flujo:

Estado (App)
   ↓ prop
Hijo (Pagination)
   ↓ callback
Estado actualizado (App)
   ↓ prop actualizada
Hijo re-renderizado (Pagination)

¡Resumiendo que es gerundio!

En esta clase has aprendido:

  • 📊 useState en padre - Crear estado en el componente padre
  • ⬇️ Pasar estado como prop - El hijo recibe el estado actualizado
  • 🔄 Lifting State Up - Elevar el estado al ancestro común
  • 🎭 StrictMode - Renderiza dos veces en desarrollo (es normal)
  • 📝 console.log - Observar cuándo se renderiza cada componente
  • 🔁 Flujo completo - Estado → Props → Callback → Actualización
  • 🎛️ Componentes controlados - El padre controla el valor
  • Renderizados - React solo actualiza lo que cambió
  • 🎯 Single Source of Truth - Una sola fuente de verdad para cada dato

En la próxima clase profundizaremos en renderizado de listas con .map(), aprendiendo a transformar arrays de datos en componentes React de forma dinámica y eficiente.

💡 Recuerda: El estado vive en el padre, se pasa a los hijos como props, y los hijos lo actualizan llamando a callbacks. Este es el flujo de datos unidireccional de React. Los renderizados dobles en desarrollo son normales por StrictMode y te ayudan a escribir mejor código.