Construyendo un modelo mental para React: reconciliación, estado y hooks
Aprenda el razonamiento detrás de los conceptos fundamentales de React: reconciliación, componentes, props, estado y hooks, para desarrollar intuición en lugar de memorizar APIs.
Si ya puedes escribir código pero aún no has profundizado en React, o solo lo has probado brevemente, este artículo está escrito pensando en ti.
Cuando comienzas a aprender React, es tentador lanzarse directamente a useState, useEffect, props, hooks y una larga lista de otras API. Puedes dominar la sintaxis, crear algo que funcione, pero aún así no comprender realmente por qué React se comporta de la manera en que lo hace.
Este artículo tiene como objetivo cerrar esa brecha.
En lugar de tratar a React como un montón de API que memorizar, exploraremos la lógica detrás del diseño de React y cómo se conectan sus componentes. La sintaxis es importante, pero resulta mucho más fácil de asimilar una vez que entiendes lo que ocurre en su interior. Así que, en lugar de empezar con cómo escribir código en React, primero desarrollaremos cómo razonar sobre React.
Cubriremos componentes, props, estado, renderizado, reconciliación, hooks, propagación de props, Context y enrutamiento, relacionando cada concepto con el problema que se creó para resolver. El objetivo no es enumerar todas las API que ofrece React, sino proporcionarte un modelo mental que te ayude a comprender esas APIs en cuanto las conozcas.
Probablemente hayas escuchado que algún algoritmo inteligente es parte de la razón por la cual React parece ser tan rápido. Puede sonar casi como magia: ¿cómo logra una biblioteca de JavaScript actualizar la interfaz de usuario de manera tan eficiente?
Ese algoritmo se llama reconciliación, y es un buen punto de partida.
Reconciliación: el algoritmo que hizo posible a React
Diferenciar dos árboles DOM desde cero y calcular el conjunto mínimo de cambios necesarios es un problema que puede requerir aproximadamente O(n³) de tiempo para resolverse.
Pero, ¿y si el proceso de reconciliación hace un puñado de suposiciones razonables y le proporcionamos algunas pistas sobre dónde es probable que estén los cambios?
¡Así de simple! La complejidad disminuye a aproximadamente O(n).
Eso significa que un enfoque que de forma ingenua tardaría unos 31 años en completarse se reduce a algo cercano a 16 minutos, únicamente gracias a las suposiciones y a unas pocas indicaciones del desarrollador.
Espera… ¿pistas? ¿Qué son exactamente estas pistas y cómo las proporcionamos?
No hay necesidad de preocuparse. Es más simple de lo que suena, y pronto lo explicaremos en detalle.
Por ahora, dejemos que la reconciliación haga su trabajo en segundo plano y centremos nuestra atención en la pregunta que realmente nos importa a nosotros como desarrolladores.
Pero, ¿por qué molestarse en usar React?
¿Por qué React?
Independientemente de lo que te digan, hay un verdadero ajuste mental cuando pasas de archivos simples de HTML, CSS y JavaScript a conceptos como componentes, hooks, props y estado.
En comparación con herramientas como FastAPI o Go, React realmente puede parecer que exige un esfuerzo mucho mayor para aprenderlo.
Pero he aquí el detalle: Esa dificultad se presenta al principio.
Una vez que comiences a comprender mejor cómo funciona React, notarás que muchos de estos conceptos que parecen intimidantes se basan en principios sorprendentemente sencillos.
No es necesario que memorices toda la aplicación de una sola vez.
En su lugar, puedes concentrarte en un único componente: qué datos recibe, qué necesita registrar y cómo debe actualizarse su salida renderizada.
Este modo de pensar compartimentado es lo que hace que la creación y el mantenimiento de interfaces complejas sean manejables. Y al final, ese es el resultado que realmente importa a los desarrolladores.
Escribir código que sea más fácil de crear, entender, cambiar y mantener. El objetivo aquí es ayudarte a lograrlo.
Los cuatro pilares de React :—
Componentes
Un componente, en esencia, es una función de JavaScript que recibe un único objeto y devuelve alguna interfaz de usuario escrita en JSX.
JSX es una extensión de sintaxis para JavaScript que permite incrustar marcado similar al HTML directamente dentro del código JavaScript. Puedes aplicar estilos usando CSS simple o una biblioteca de utilidades como Tailwind CSS.
Cualquier aplicación React lo suficientemente compleja es, en última instancia, simplemente una red de componentes que intercambian datos entre sí.
Digamos que nunca has trabajado con React antes: aún así puedes comprender qué está haciendo un componente.
const data = {
question: "What is React?",
answer: "A JavaScript library for building user interfaces"
};
function FlashCard(data) {
return (
<div className="border rounded-lg bg-gray-50 p-4">
<h2>{data.question}</h2>
<p>{data.answer}</p>
</div>
);
}
Si ignoramos un par de peculiaridades sintácticas específicas de React, esa es básicamente toda la idea detrás de React.
No está tan mal, ¿verdad?
Fundamentalmente, estamos pasando algunos datos a una función y obteniendo de vuelta una parte de la interfaz de usuario.
Por lo tanto, como desarrollador, tu verdadero trabajo es diseñar componentes limpios teniendo en cuenta tres preguntas:
- ¿Qué información recibe?
- ¿Qué información mantiene?
- ¿Cómo debería actualizarse su apariencia?
¿Qué podría haber de complicado en unos pocos parámetros y algunas variables? ¿No podríamos simplemente pasar los datos que necesitemos y almacenar lo que queramos en variables locales?
En cierto modo, pero no del todo.
¿Y qué entendemos por actualización de la interfaz de usuario? Imagina a dos vendedores intentando vender el mismo coche, pero el gerente solo le informa a uno de ellos sobre un cambio de precio.
¿Qué le sucede al otro vendedor? Sigue citando el precio antiguo, sin darse cuenta de nada.
Ahora imagina que el gerente publica la actualización en un chat grupal al que todos pertenecen. Al cambiar el precio una vez, todo el equipo lo ve al instante.
Ese es, en esencia, el problema que React fue creado para resolver.
Cada vez que cambia alguna información que afecta lo que se muestra en la pantalla, todo aquello que depende de ella —en nuestra analogía, las personas; en React, los componentes— necesita una forma fiable de detectar ese cambio y reaccionar ante él. Ese mecanismo es el que proporciona React.
Props
¿Son intercambiables function User(name, age, city) y function User(name, city)? ¿Y function User(city, name)?
Una función típica que depende de parámetros posicionales solo puede aceptar un número determinado de argumentos en un orden específico; recuerde que un componente no es más que una función.
Imagínese ahora que ha creado un componente que muestra el nombre y la edad de un usuario, y actualmente se utiliza en 67 lugares diferentes dentro de su código. Entonces su supervisor le pide que también muestre la ciudad del usuario. Actualizar cada una de esas utilizaciones consumiría horas de trabajo.
Pero, ¿qué pasaría si la función estuviera diseñada de tal manera que las llamadas antiguas siguieran funcionando sin cambios, mientras que las nuevas podrían, opcionalmente, transmitir datos adicionales?
La herramienta que se necesita aquí es un único objeto que representa a una lista de parámetros. React lo denomina props.
Los props son simplemente la forma en que React agrupa todos los datos entregados a un componente en un único objeto.
/** So a component like this one **/
function User({ name = "Guest", age = 18, city = "Unknown" }) {
return (
<div>
<h2>{name}</h2>
<p>{age} years old</p>
<p>{city}</p>
</div>
);
}
/** Can be used in ways like **/
<User /> /** Guest, 18, Unknown **/
<User name="Pritam" /> /** Pritam, 18, Unknown **/
<User name="Rahul" age={21} /> /** Rahul, 21, Unknown **/
<User name="Priya" age={22} city="Delhi" /> /** Priya, 22, Delhi **/
Estados
¿Debería el gerente de la concesionaria haber guardado el nuevo precio para sí mismo? ¿Decírselo solo a un vendedor? ¿A ambos? ¿O anunciarlo a todos en la concesionaria, incluidos los guardias de seguridad y el personal de limpieza?
Imagínese una caja de almacenamiento y llénela con sus pertenencias. ¿Puede saber, solo con echar un vistazo desde afuera, si algo dentro ha cambiado? ¿Qué pasa si la caja se mueve a otro estante? Al menos notaría que su posición ha cambiado e inferiría que algo ocurrió.
Ahora imagine que esa caja es el mecanismo que React utiliza para guardar los valores que usted define.
Eso significa que debe haber una forma de enviar los valores actualizados a quienes dependen de ellos, y una forma para que esos usuarios sepan cuándo su valor ha cambiado.
const [count, setCount] = useState(initialCount);
¿Ya se ha encontrado con esta llamada a useState antes?
Le entrega al componente dos cosas:
- count: el valor actual del estado.
- setCount: una función que se llama para solicitar que ese valor del estado sea actualizado.
¿Entonces, qué hay de malo en simplemente escribir value = 5?
React no tiene forma de saber que algo dentro del contenedor ha cambiado.
Dicho de otro modo, se necesita un mecanismo para indicarle a React: "Oye, he actualizado este valor; quizás deberías actualizar la interfaz de usuario."
¿Es esta la pista de antes? En cierto sentido, sí y no.
useState te permite almacenar un valor, solicitar actualizaciones y, al mismo tiempo, notificar a React que ha habido un cambio. Pero, ¿por qué es necesario informarle explícitamente a React?
Porque de lo contrario actuarías como ese descuidado gerente de concesionario.
¿Recuerdas el error real? El gerente cambió el precio, actualizó su propia copia del tablero y olvidó informar a los demás. Tú eres más inteligente que eso: simplemente llamarás a setCount().
¿Qué sucede realmente una vez que se llama a setCount()?
React toma el nuevo estado, vuelve a renderizar el componente para determinar cómo debería verse la interfaz ahora, y luego depende de la reconciliación para averiguar qué es lo que realmente necesita cambiar en la pantalla.
Simplemente le has dicho a React que algo ha cambiado. La reconciliación es lo que determina qué cambió y qué necesita actualizarse como resultado.
Hooks
useState() — una función integrada que permite a un componente conservar un valor de estado y te brinda la forma de solicitar actualizaciones para él.
Cuando ese estado se actualiza, pueden ocurrir dos cosas:
- El nuevo valor no tiene efecto en lo que se renderiza — React puede seguir re-renderizando, pero nada cambia visualmente.
Las funciones como esta, que cuentan con capacidades especiales de React, se denominan hooks. Hay algunas más que vale la pena conocer.
useRef() — es útil cuando necesitas un contenedor para un valor que no tiene nada que ver con la interfaz de usuario.
const count = useRef(0);
count.current++;
Así que la regla general es: si el valor que cambia también debe modificar la interfaz de usuario, utiliza useState(). Si solo es necesario cambiar el valor en sí, sin ninguna consecuencia de renderizado, utiliza useRef().
También tiene un segundo uso común: hacer referencia a elementos reales del DOM, pero por ahora este modelo mental es suficiente.
useEffect() — para cuando necesitas que algo se ejecute después de que React haya terminado de renderizar la interfaz de usuario.
useEffect(() => {
console.log("Runs after every render");
});
useEffect(() => {
console.log("Runs once after the initial render");
}, []);
useEffect(() => {
console.log("After the initial render and whenever count changes");
}, [count]); /** Dependencies go here **/
useContext() — imagina un árbol de componentes en el que un dato como name debe pasar por varias capas de componentes para llegar a un componente UserName profundamente anidado.
Ahora amplía ese árbol añadiendo varios datos que también deben pasar por componentes que ni siquiera los utilizan directamente.
Ese patrón se conoce como prop drilling.
La API Context de React ofrece una solución a esto: permite que los datos estén disponibles en cualquier parte más profunda del árbol sin tener que reenviarlos manualmente a través de cada componente intermedio.
const ParentContext = createContext(null)
<ParentContext value={money}>
<ChildComponent />
</ ParentContext>
function ChildComponent() {
const money = useContext(ParentContext);
return <p>Money: {money}</p>;
}
Existen muchos más ganchos además de estos, y vale la pena experimentar con ellos por cuenta propia.
Hasta ahora, la discusión ha girado en torno a cómo React organiza los componentes y cómo se mueve la data entre ellos. Pero una aplicación real también debe decidir qué partes de esa interfaz deben aparecer bajo qué URLs.
React Router
React permite crear interfaces formadas por componentes que se actualizan solos, sin obligar al navegador a recargar toda la página. Pero una aplicación real suele necesitar más de una página, y eso plantea una nueva pregunta.
¿Qué sucede cuando tu aplicación necesita varias vistas distintas?
Podrías querer algo como:
/home → Inicio
/dashboard → Panel de control
/profile → Perfil
Si los conectas con etiquetas HTML simples, hacer clic en ellos desencadena una navegación completa del navegador. Toda la página se descarta y la aplicación se inicia de nuevo desde cero en la nueva dirección.
Lo que realmente quieres es que la URL se actualice mientras React determina silenciosamente qué componentes necesitan reemplazarse, sin descartar todo lo demás.
Ese es el problema que resuelve React Router.
Piénsalo como una capa que asocia URLs con componentes.
Por ejemplo:
<BrowserRouter>
<Routes>
<Route path="/home" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</BrowserRouter>
React Router inspecciona la URL actual y renderiza el componente asociado a ella. BrowserRouter se basa en la API History del navegador para gestionar la navegación en el lado del cliente, y Routes selecciona el Route que mejor se ajuste a la ruta actual.
Aun así, hay otro detalle con el que lidiar.
¿Qué pasa si no quieres que se vuelva a renderizar toda la pantalla?
Imagina un diseño como este:
┌─────────────────────────────┐
│ Header │
├──────────┬──────────────────┤
│ │ │
│ Menu │ Page content │
│ │ │
├──────────┴──────────────────┤
│ Footer │
└─────────────────────────────┘
Al pasar de /home a /dashboard, el encabezado, la barra lateral o el pie de página no deberían desaparecer y reaparecer. Solo el área principal de contenido necesita cambiar.
Ese es exactamente el escenario para el cual están diseñadas las rutas anidadas y <Outlet />.
function Layout() {
return (
<>
<Header />
<Menu />
<Outlet />
<Footer />
</>
);
}
Luego, las rutas mismas pueden anidarse unas dentro de otras:
<Routes>
<Route element={<Layout />}>
<Route index element={<Home />} />
<Route path="dashboard" element={<Dashboard />} />
</Route>
</Routes>
Con esta configuración, Layout permanece montado, y React Router muestra la ruta hijo que coincida dentro de <Outlet />. Según la propia documentación de React Router, <Outlet /> marca el lugar donde se renderiza la ruta hijo correspondiente.
La ruta index asocia <Home /> con la ruta /, lo que la convierte en la vista predeterminada que se muestra dentro del outlet cuando no hay una ruta más específica activa.
Por lo tanto, pasar de /home a /dashboard no significa realmente reemplazar toda la página. Es más bien como decir:
"Deja esta parte de la interfaz tal como está, y sustituye solo esta sección por el componente correspondiente a la nueva ruta."
¿Qué sigue?
Una vez que comprendas bien los problemas que React existe para resolver y los mecanismos que utiliza para abordarlos, vale la pena dedicar tiempo a leer código real para ver cómo los equipos experimentados estructuran sus aplicaciones.
Búscate un repositorio que muestre cómo se puede organizar y arquitecturar una aplicación React en producción a gran escala.
En lugar de intentar absorber todo el código base de una sola vez, elija una única funcionalidad y sígala a través de las capas de la aplicación. Observe cómo se agrupan los componentes, de dónde proviene la data, cómo se gestiona el estado y cómo las distintas partes de la aplicación se comunican entre sí.
También vale la pena encontrar un ejemplo que muestre cómo React puede combinarse con Redux en una aplicación funcional.
Si el proyecto que encuentre es más antiguo, no considere sus patrones como el estándar actual para React. Úselo en cambio para estudiar cómo una aplicación grande puede dividirse en partes y cómo Redux se integra en esa estructura.
Una vez que se sienta cómodo navegando por la estructura típica de un archivo de React y pueda crear componentes simples por su cuenta, el siguiente paso es aprender a desarrollar aplicaciones que funcionen a gran escala.
Escalar una aplicación React conlleva su propio conjunto de desafíos, entre los que se incluyen:
- SSR y componentes del servidor — cómo cambia el comportamiento cuando partes de la aplicación se ejecutan en el servidor en lugar de completamente en el navegador.
- Gestión del estado — qué hacer cuando el estado de la aplicación se vuelve demasiado grande o está compartido en exceso para que el estado local y Context lo gestionen cómodamente. Redux es una de las opciones disponibles.
- Obtención y almacenamiento en caché de datos — cómo las aplicaciones en producción gestionan los indicadores de carga, el manejo de errores, el almacenamiento en caché y el mantenimiento de la sincronización de los datos del cliente con el servidor.
- Rendimiento — reconocer cuándo el renderizado realmente se convierte en un cuello de botella y optimizar en consecuencia, en lugar de optimizar todo desde el principio.
No es necesario dominar cada uno de estos temas antes de comenzar a desarrollar.
Un enfoque más práctico es empezar a desarrollar, toparse con un problema específico y luego aprender el concepto o herramienta que resuelva ese problema concreto.
En última instancia, el objetivo de aprender React nunca fue memorizar su interfaz API. Fue comprender por qué existen esas APIs y cómo razonar sobre los problemas para los cuales fueron diseñadas.
Lecturas relacionadas
- Comprendiendo los ganchos personalizados de React: reutilización de lógica sin estado compartido — Aprenda qué son los ganchos personalizados de React, cómo extraen y comparten lógica con estado entre componentes, y qué errores comunes evitar al crearlos.
- Patrones de diseño en React: desde el OOP clásico hasta los ganchos modernos — Explica cómo se aplican en React patrones de software clásicos como Singleton, Factory y Observer, junto con patrones específicos de React como HOCs, ganchos y componentes compuestos.