Saltar al contenido principal
Logo de JSCamp

Estado por Props - Lifting State Up

Aprende a manejar estado en el componente padre y pasarlo a los hijos, entendiendo cuándo se renderiza cada componente

midudev

Más clases y directos en YouTube

Suscribirse

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.jsxfunction 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 cambiaconst handlePageChange = (page) => {  console.log('Página cambiada a:', page) // Solo imprime}

Después (con estado)

const [currentPage, setCurrentPage] = useState(1) // Estado reactivoconst 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.jsxfunction App() {  console.log('🔵 App renderizado')  const [currentPage, setCurrentPage] = useState(1)  const totalPages = 5  // ...}
// src/components/Pagination.jsxfunction 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 externalet counter = 0function 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.jsxReactDOM.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 controladofunction 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 controladofunction 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)   ↓ propHijo (Pagination)   ↓ callbackEstado actualizado (App)   ↓ prop actualizadaHijo 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.