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:
- Usuario hace click en “Página 3”
Paginationllama aonPageChange(3)handlePageChange(3)llama asetCurrentPage(3)- El estado cambia de 1 a 3
- React re-renderiza
Appcon el nuevo valor PaginationrecibecurrentPage={3}como prop- 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ó?
- Click en “Página 3”
- Se llama a
setCurrentPage(3) - React detecta que el estado cambió
- React re-renderiza
App(por eso ves el log azul) - React re-renderiza
Paginationcon 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:
- Duplicación: Si
JobListingstambién necesita la página, tendrías dos estados separados - Sincronización: ¿Qué pasa si el padre quiere cambiar la página desde fuera?
- 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?
setCurrentPageya 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.