Manejando el caos, domina el Arte de trabajar con estados complejos en React
"El manejo de estado en React es como una rama que se mueve en el viento, siempre en un estado de cambio constante, pero manteniendo su estabilidad y equilibrio en todo momento."
El estado es esencial en React para crear una interfaz de usuario dinámica y reactiva.
En cada aplicación React, se utilizan valores de estado en muchos componentes, desde
valores simples que contienen un contador cambiante hasta valores más complejos como
la información de autenticación del usuario. useState()
Hook es una herramienta
útil para administrar el estado.
Cuando se crean aplicaciones React más complejas (como tiendas en línea o paneles de administración), se enfrenta a desafíos relacionados con el estado. Los valores de estado pueden usarse y cambiarse en diferentes componentes, lo que complica su gestión. Es posible manejar estos problemas con useState()
, props y otros conceptos. Sin embargo, las soluciones basadas en useState()
pueden volverse complejas y difíciles de mantener.

"Cuando se trata del manejo de estado en React.js, es como trabajar con una multitud de personas. Cada una con su propia información y comportamiento, pero juntos trabajando hacia un objetivo común."
Problemas con "Cross-Components"
Prop drilling significa que un valor de estado se pasa a través de múltiples componentes a través de props. Y se pasa a través de componentes que no necesitan el estado en sí mismos, excepto para reenviarlo a un componente secundario (como lo hacen los componentes NavInfo y MenuInfo).
import MenuInfo from '../../Components/MenuInfo'
function NavInfo({ info }) {
return (
<nav>
<MenuInfo info={info} />
</nav>
)
}
Como desarrollador, es recomendable y lo que trato siempre de evitar es el patrón de ”prop drilling” debido a sus debilidades. Los componentes que forman parte de este patrón ya no son reutilizables y hay una gran cantidad de código de sobrecarga que debe escribirse. Solo es aceptable si los componentes involucrados se usan solo en una parte específica de la aplicación y la probabilidad de reutilizarlos o refactorizarlos es baja.
La solución al presentarse “Prop drilling” es usar la API de contexto para manejar el estado en varios componentes.
Manejando el estado con API Context
Context API es una característica de React que permite pasar datos a través del árbol de componentes sin tener que pasar props manualmente a través de cada nivel. En lugar de ello, los datos pueden ser accesibles por cualquier componente hijo que se suscriba a ese contexto.
La API de Contexto se compone de dos partes principales: el proveedor (Provider) y el consumidor (Consumer). El proveedor es responsable de almacenar los datos y hacerlos accesibles para cualquier componente hijo que los necesite. El consumidor es responsable de consumir estos datos y usarlos en el renderizado del componente.
Para utilizar la API de Contexto en una aplicación de React, primero se debe crear un contexto mediante el método createContext()
y luego envolver el componente raíz de la aplicación con un componente proveedor que proporcione los datos a compartir. Los componentes hijos que necesiten acceder a estos datos pueden suscribirse al contexto utilizando el componente Consumer
.
Uso del contexto para el manejo de varios Componentes
¿Cómo compartir datos entre varios componentes en React?.
Supongamos que tenemos una aplicación que muestra una lista de tareas.
Queremos compartir la lista de tareas entre varios componentes, incluyendo un componente para agregar nuevas tareas y otro componente para marcar las tareas como completadas.
El primer paso es crear el contexto en alguna carpeta llamada Context/context.js
import { createContext } from 'react'
const TaskContext = createContext()
Creamos el componente proveedor que contendrá la lista de tareas y las funciones para modificarla
import React, { useState } from 'react'
function TaskProvider(props) {
const [tasks, setTasks] = useState([])
const addTask = (newTask) => {
setTasks([...tasks, newTask])
}
const markTaskCompleted = (taskIndex) => {
const newTasks = [...tasks]
newTasks[taskIndex].completed = true
setTasks(newTasks)
}
const taskContext = {
tasks,
addTask,
markTaskCompleted,
}
return (
<TaskContext.Provider value={taskContext}>
{props.children}
</TaskContext.Provider>
)
}
export default TaskProvider
En el ejemplo, el componente proveedor utiliza el estado local de React para almacenar la lista de tareas y las funciones para agregar nuevas tareas y marcar tareas como completadas.
Luego, utilizamos el componente proveedor en la raíz de nuestra aplicación para envolver los componentes que necesitan acceder a la lista de tareas:
import TaskProvider from './TaskProvider'
import TaskList from './TaskList'
import AddTaskForm from './AddTaskForm'
import CompleteTaskButton from './CompleteTaskButton'
function App() {
return (
<TaskProvider>
<TaskList />
<AddTaskForm />
<CompleteTaskButton />
</TaskProvider>
)
}
export default App
Por último, para consumir el contexto dentro de un componente, utilizamos el hook useContext de React
import { useContext } from 'react'
import TaskContext from './TaskContext'
function TaskList() {
const { tasks } = useContext(TaskContext)
return (
<ul>
{tasks.map((task, index) => (
<li key={index}>{task.description}</li>
))}
</ul>
)
}
export default TaskList
El componente TaskList utiliza el hook useContext para acceder a la lista de tareas almacenada en el contexto.
Así de simple se llega a usar la API Context, con la finalidad de compartir datos entre varios componentes de una manera fácil y eficiente.
Ilustracion de como se ve el manejo del estado con Context

Contexto o 'Lifting State Up'
Llegando a este punto debes tener claro el uso de context, ahora te preguntaras ¿Qué es Lifting State Up?, es como la info que se usa para pintar un componente y lo administra el componente mismo.
Cuando "levantas" el state, lo que haces es mover esa info a su componente padre para que otros componentes puedan usarla también.
Se hace esto cuando varios componentes necesitan la misma info o cuando el state de un componente afecta a otro. Al mover el state al componente padre, te aseguras de que todos los componentes que usan esa info siempre tengan lo último.
Pero ojo, no todos los estados se tienen que "levantar" al componente padre. A veces, es mejor mantenerlo en el componente donde se usa y simplemente pasar la info necesaria a través de "props" a los componentes hijos. Al final, es tu decisión ver si lo levantas o no dependiendo de cómo se relacionan los componentes.
Explicare con un ejemplo 'Lifting state'.
Supongamos que tienes una app de carrito de compras.
// Componente del carrito de compras
function Cart() {
const [cartItems, setCartItems] = useState([])
// Función para agregar un producto al carrito
const addToCart = (product) => setCartItems([...cartItems, product])
return (
<div>
<h2>Carrito de compras</h2>
<ProductList addToCart={addToCart} />
<CartItemsList cartItems={cartItems} />
</div>
)
}
// Componente de la lista de productos
function ProductList({ addToCart }) {
const products = [
{ id: 1, name: 'Camisa', price: 20 },
{ id: 2, name: 'Pantalón', price: 30 },
{ id: 3, name: 'Zapatos', price: 50 },
]
return (
<div>
<h3>Productos</h3>
<ul>
{products.map((product) => (
<li key={product.id}>
{product.name} - ${product.price}{' '}
<button onClick={() => addToCart(product)}>
Agregar al carrito
</button>
</li>
))}
</ul>
</div>
)
}
// Componente de la lista de elementos del carrito
function CartItemsList({ cartItems }) {
return (
<div>
<h3>Carrito</h3>
<ul>
{cartItems.map((item) => (
<li key={item.id}>
{item.name} - ${item.price}
</li>
))}
</ul>
</div>
)
}
En este ejemplo, el componente Cart maneja el estado del carrito de compras utilizando el hook useState. El estado inicial del carrito de compras es un array vacío, y la función addToCart se utiliza para agregar un producto al carrito de compras.
El componente ProductList muestra una lista de productos y pasa la función addToCart como prop al botón "Agregar al carrito". Cuando se hace clic en el botón, la función addToCart se ejecuta y agrega el producto al carrito de compras utilizando la función setCartItems.
Finalmente, el componente CartItemsList muestra la lista de elementos del carrito utilizando la información almacenada en el estado del carrito de compras.
Al levantar el estado del carrito de compras al componente Cart, podemos asegurarnos de que tanto ProductList como CartItemsList siempre tengan acceso a la información actualizada del carrito de compras.
Para verificar el entendimiento te dejare un ejercicio de practica:
Lab I
La aplicación debe tener los siguientes componentes:
-
App: componente principal que renderiza los otros componentes y almacena el estado global de la aplicación. Debe tener un estado para almacenar la lista de productos disponibles y la lista de productos seleccionados para la compra.
-
ProductList: componente que muestra la lista de productos disponibles para la compra. Recibe la lista de productos como props y muestra cada producto con su nombre, precio y un botón "Agregar al carrito". Cuando se hace clic en el botón "Agregar al carrito", se debe agregar el producto correspondiente a la lista de productos seleccionados para la compra.
-
SelectedProductList: componente que muestra la lista de productos seleccionados para la compra. Recibe la lista de productos seleccionados como props y muestra cada producto con su nombre y precio. También muestra un botón para eliminar cada producto de la lista.
-
Checkout: componente que muestra la lista de productos seleccionados para la compra y el total de la compra. Tiene un botón "Comprar" que, cuando se hace clic, vacía la lista de productos seleccionados y muestra un mensaje de confirmación de compra.
Para completar el desafío, deberás implementar el levantamiento de estado para que los componentes ProductList, SelectedProductList y Checkout se comuniquen con el componente App y actualicen el estado de la lista de productos seleccionados para la compra.
Además, debes implementar la lógica necesaria para calcular el total de la compra en el componente Checkout
Limitaciones de useState()
Para la administración de estados tabien puede ser un desafio en escenarios donde algun estado solo se usa dentro de un solo componente.
useState()
es una gran herramienta para la administración del estado, pero este hook
puede llegar a tener sus limitaciones, es decir
si necesita derivar un nuevo valor de estado basado en el valor de otra variable de estado, por ejemplo:
setIsLoading(fetchedPosts ? false : true)
En este ejemplo se toma de un componente donde se envía una solicitud HTTP para obtener algunas publicaciones de blog.
Aqui te dejo otro ejemplos para que entiendas mejor las limitaciones
const fetchPosts = useCallback(async function fetchPosts() {
setIsLoading(fetchedPosts ? false : true)
setError(null)
try {
const response = await fetch('https://examples/posts')
if (!response.ok) {
throw new Error('Failed to fetch posts.')
}
const posts = await response.json()
setIsLoading(false)
setError(null)
setFetchedPosts(posts)
} catch (error) {
setIsLoading(false)
setError(error.message)
setFetchedPosts(null)
}
}, [])
Esta función manda un mensaje HTTP y cambia un montón de valores de estado dependiendo de cómo va la solicitud.
useCallback()
es para evitar un lío con useEffect()
. Normalmente, fetchedPosts
se tiene que agregar como una dependencia en la lista de dependencias que se pasa como segundo argumento a la función useCallback()
. Pero en este ejemplo, no se puede hacer porque fetchedPosts
cambia dentro de la función que useCallback()
envuelve, y entonces su valor no es solo una dependencia, sino que también se cambia activamente. Esto causa un lío sin fin.
Entonces, una idea para solucionar este problema sería juntar varios sectores de estado en un solo objeto (fetchedPosts, isLoading, y error). Así, todos los valores de estado estarían juntos y se podrían acceder de manera segura al usar el formulario de actualización de estado funcional. El código de actualización de estado podría ser así:
setHttpState((prevState) => ({
fetchedPosts: prevState.fetchedPosts,
isLoading: prevState.fetchedPosts ? false : true,
error: null,
}))
¡Esta opción está bien, pero no siempre es la mejor porque el manejo de objetos de estado cada vez más complejos y anidados puede ser un poco complicado y aumentar el tamaño del código de su componente!
Por eso, ¡React tiene una solución alternativa a useState()
: el Hook useReducer()
!
Challenge Técnico
Hola Developer ha llegado la hora de practicar los conceptos aprendidos. En esta seccion se te daran dos puntos el Tarea y la Descripción.
Recomendación:
No veas la solución hasta que lo hayas intentado lo suficiente.
Descripción del Problema
Tarea:
Se necesita solucionar un bug en la implementación actual del componente que muestra la información del usuario y el que permite editarla. Actualmente, los cambios realizados en el componente de edición no se reflejan correctamente en el componente de visualización. Se requiere implementar una solución utilizando el Context API de React para actualizar el objeto que almacena la información del usuario y asegurar que los cambios realizados sean reflejados correctamente en ambos componentes.
Para solucionar el problema, se deben seguir los siguientes pasos:
-
Utilizar el método "createContext" de React para crear un contexto que almacene el objeto con la información del usuario.
-
Implementar dos componentes: uno que muestre la información del usuario y otro que permita editarla.
-
Utilizar el método "useContext" para acceder al contexto creado desde ambos componentes.
-
En el componente de edición, implementar una función que actualice el objeto del contexto con los nuevos valores.
-
En el componente de visualización, asegurarse de que los valores mostrados sean actualizados con los valores actualizados del contexto.
-
Verificar que los cambios realizados en el componente de edición sean reflejados correctamente en el componente de visualización.