De los scripts DOM a la interfaz de usuario como función del estado: el modelo central de React
Por qué React sustituye las actualizaciones manuales del DOM por componentes declarativos, y cómo JSX, props, estado, volver a renderizar y el flujo de datos unidireccional encajan dentro de un mismo modelo mental.
Un botón de “me gusta” que actualiza un contador, cambia un ícono y reproduce una animación es fácil de crear con llamadas simples al DOM. Sin embargo, cuando se tienen cincuenta de estos elementos en un feed en tiempo real, cada uno de los cuales también debe hacer seguimiento a comentarios, compartidos y estado guardado, el código DOM escrito a mano comienza a resultar insuficiente. React existe precisamente para resolver este tipo de problemas: usted describe cómo debe verse la pantalla con los datos actuales, y React determina qué cambios en el DOM son necesarios.
Esta guía explica las ideas que hacen posible esto, en el orden en que se van construyendo unas sobre otras: JSX, componentes, props, estado, volver a renderizar, el estilo declarativo y la forma en que los datos y eventos se propagan por un árbol de componentes. Al final, debería poder predecir cuándo se volverá a renderizar un componente, decidir dónde debe ubicarse cierto estado y detectar los errores comunes de principiantes que impiden las actualizaciones.
El problema que React fue creado para resolver
Antes de que las bibliotecas de componentes se convirtieran en la norma, una interfaz de usuario interactiva requería código imperativo: había que encontrar los elementos y luego realizar cada cambio manualmente cada vez que ocurría algo. Para un simple botón de “me gusta”, la configuración era la siguiente:
// Traditional DOM manipulation
const button = document.getElementById("like-button");
const countEl = document.getElementById("like-count");
const icon = document.getElementById("like-icon");
let liked = false;
let count = 42;
Y el manejador de clics tenía que recordar cada detalle visual de ambos estados:
button.addEventListener("click", function() {
if (!liked) {
liked = true;
count++;
countEl.textContent = count;
button.classList.add("liked");
button.classList.remove("unliked");
icon.src = "/icons/heart-filled.svg";
button.style.color = "#e0245e";
// trigger animation
button.classList.add("animate-pop");
setTimeout(() => button.classList.remove("animate-pop"), 300);
} else {
liked = false;
count--;
countEl.textContent = count;
button.classList.remove("liked");
button.classList.add("unliked");
icon.src = "/icons/heart-empty.svg";
button.style.color = "#6c757d";
}
});
Eso era manejable para un solo botón. Ahora imagine una secuencia de 50 publicaciones, cada una con sus propios controles para “me gusta”, comentarios, compartir y guardar, los cuales pueden cambiar debido a clics del usuario o porque llegan nuevos datos desde el servidor. El script se convierte en una red de búsquedas de elementos, listeners y variables que deben mantenerse sincronizados a mano. A medida que la aplicación crece, surgen tres modos de fallo:
- Sincronización. El DOM muestra una cosa y tus variables indican otra. Si actualizas el elemento contador pero olvidas la variable, o al revés, ahora tendrás dos fuentes de verdad que depurar.
- Reutilización. El manejador está vinculado a IDs de elementos específicos y a una estructura HTML concreta, por lo que mover el botón a otra página implica reescribirlo.
- Complejidad. Cada nueva función te obliga a comprender todo lo demás que podría verse afectado. Pequeños cambios pueden dañar partes alejadas de la página.
React, cuyo código fuente fue publicado por Facebook en 2013, resuelve estos tres problemas con una idea: deja de emitir comandos al DOM y en su lugar describe la interfaz que debería existir para un estado determinado. React compara esa descripción con lo que se muestra en pantalla y aplica las diferencias. Tú describes; React actualiza.
JSX: marcado que se compila a JavaScript
Lo primero que resulta inusual en el código de React es el marcado con apariencia de HTML dentro de una función:
function Greeting() {
return (
<div className="greeting">
<h1>Hello, Priya!</h1>
<p>Welcome back.</p>
</div>
);
}
Ese marcado es JSX, una extensión de sintaxis que permite escribir estructuras de elementos en archivos de JavaScript. No es HTML ni algo que el navegador entienda. Un paso de compilación (Babel, el compilador de TypeScript o un bundler que los utilice) lo reescribe en llamadas a funciones normales antes de que el código se ejecute. Se escribe:
// What you write (JSX):
return (
<h1 className="title">Hello!</h1>
);
y el compilador genera algo equivalente a:
// What the compiler transforms it into:
return React.createElement("h1", { className: "title" }, "Hello!");
React.createElement simplemente devuelve un objeto sencillo que describe el elemento: su tipo, sus propiedades y sus hijos. React lee estos objetos para decidir qué debe contener el DOM real. (Las transformaciones JSX más recientes utilizan una función auxiliar diferente de react/jsx-runtime, pero el resultado es un objeto de descripción similar.) Nadie escribe estas llamadas a mano; JSX existe porque el marcado anidado es mucho más fácil de leer.
Dónde JSX difiere de HTML
La sintaxis es similar a la de HTML, pero no idéntica. Las diferencias que más confunden a las personas son:
// HTML JSX
// ───────────────────────────── ──────────────────────────────────
// class="title" className="title" ← JS reserved word
// for="email" htmlFor="email" ← JS reserved word
// <input> (self-closing optional) <input /> ← must self-close
// onclick="handler()" onClick={handler} ← camelCase, no quotes
// style="color: red" style={{ color: "red" }} ← JS object
class y for son palabras reservadas en JavaScript, por lo que JSX utiliza className y htmlFor. Cada elemento debe cerrarse. Los manejadores de eventos están en formato camelCase y reciben una función, no una cadena. Los estilos incrustados son objetos. Para conocer en profundidad las consideraciones detrás de esta sintaxis, consulte por qué JSX no es HTML.
Incorporación de expresiones JavaScript
Los llaves abren una ventana al lenguaje JavaScript. Cualquier cosa que genere un valor puede colocarse dentro: variables, llamadas a funciones, expresiones ternarias, métodos de array. En esta tarjeta, primero se calcula el estado en línea:
function UserCard({ user }) {
const isOnline = user.lastSeen < Date.now() - 5 * 60 * 1000;
Y luego varias expresiones dan forma al marcado: una biografía truncada, un nombre de clase condicional, una etiqueta condicional y una fecha formateada:
return (
<div className="card">
<img src={user.avatar} alt={user.name} />
<h2>{user.name}</h2>
<p>{user.bio.length > 100 ? user.bio.slice(0, 100) + "..." : user.bio}</p>
<span className={isOnline ? "badge-green" : "badge-grey"}>
{isOnline ? "Online" : "Offline"}
</span>
<p>Joined: {new Date(user.joinedAt).toLocaleDateString()}</p>
</div>
);
}
La regla es simple: los paréntesis contienen expresiones, no instrucciones. Puedes usar una estructura ternaria pero no un bloque if, y .map() pero no un bucle for, directamente dentro del marcado.
Componentes: funciones que devuelven interfaz de usuario
Un componente es una pieza reutilizable y autónoma de la interfaz de usuario escrita como una función JavaScript que devuelve JSX. Esa es toda la definición:
// A component is just a function that returns JSX
function Button() {
return (
<button className="btn">
Click me
</button>
);
}
Se utiliza de la misma manera que una etiqueta HTML, y se pueden colocar tantas como se desee:
function App() {
return (
<div>
<Button />
<Button />
<Button />
</div>
);
}
Esto renderiza tres botones idénticos a partir de una sola definición.
Componer componentes pequeños en componentes más grandes
Los componentes se vuelven poderosos cuando se anidan. Piezas pequeñas y de un solo propósito se ensamblan en componentes más grandes, como bloques de construcción. Un avatar solo sabe cómo mostrar una imagen:
function Avatar({ src, alt }) {
return <img className="avatar" src={src} alt={alt} />;
}
y sobre él se construye una cadena de componentes ligeramente más grandes: un bloque de nombre, una fila con información del usuario que combina el avatar y el nombre, y una tarjeta de publicación que integra la información del usuario con el cuerpo del mensaje:
function UserName({ name, handle }) {
return (
<div>
<strong>{name}</strong>
<span>@{handle}</span>
</div>
);
}function UserInfo({ user }) {
return (
<div className="user-info">
<Avatar src={user.avatar} alt={user.name} />
<UserName name={user.name} handle={user.handle} />
</div>
);
}function PostCard({ post, author }) {
return (
<article className="post-card">
<UserInfo user={author} />
<p>{post.content}</p>
<span>{post.likes} likes</span>
</article>
);
}
Cada capa tiene una función específica. Avatar muestra una imagen, UserInfo coloca el avatar junto al nombre, y PostCard organiza toda la publicación. Esa es la práctica que React fomenta: dividir la interfaz en las partes más pequeñas y razonables, asignarle a cada una una responsabilidad clara y luego combinarlas. La guía para crear componentes React reutilizables profundiza aún más en el diseño de componentes.
Por qué los nombres de los componentes están en mayúsculas
React distingue entre los elementos nativos y los componentes por la primera letra. <button> se convierte en un botón DOM; <Button> hace que React busque una variable llamada Button en el ámbito y la ejecute. Un componente nombrado con letra minúscula se trata silenciosamente como una etiqueta HTML desconocida, lo cual es una causa común de la confusión “mi componente no muestra nada”.
Props: configurar un componente desde el exterior
Un botón que siempre dice “Haz clic en mí” no es muy útil. Los props (abreviatura de properties) son la forma en que un componente padre pasa datos a uno hijo, de modo que una misma definición puede servir para muchas situaciones.
Los props viajan en una sola dirección, desde el componente que renderiza a otro hasta el que se está rendering, y llegan como un único argumento de objeto. El padre los escribe como atributos:
// Parent passes data as props (looks like HTML attributes)
function App() {
return (
<div>
<Button label="Submit" colour="blue" />
<Button label="Cancel" colour="grey" />
<Button label="Delete" colour="red" />
</div>
);
}
Y el hijo desestructura lo que necesita:
// Child receives them as an object
function Button({ label, colour }) {
return (
<button className={`btn btn-${colour}`}>
{label}
</button>
);
}
Una definición, tres botones que se ven diferentes porque cada uno recibió valores distintos.
Las propiedades pueden contener cualquier valor
Las cadenas de texto son solo el comienzo. Los números, los booleanos, los arrays, los objetos y las funciones son todas propiedades válidas. Una tarjeta de producto, por ejemplo, puede recibir un objeto de datos, una función de callback y una bandera:
function ProductCard({ product, onAddToCart, featured }) {
return (
<div className={`card ${featured ? "card-featured" : ""}`}>
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p>₹{product.price.toLocaleString()}</p>
<p>{product.rating} ★ ({product.reviewCount} reviews)</p>
<button onClick={onAddToCart}>
Add to Cart
</button>
</div>
);
}
En el lugar de la llamada, esos valores se pasan entre llaves:
// Usage:
<ProductCard
product={{ name: "Headphones", price: 2499, rating: 4.3, reviewCount: 128 }}
onAddToCart={() => addToCart(product.id)}
featured={true}
/>
Tenga en cuenta que la función de callback inline onAddToCart hace referencia a product.id, pero product aquí es solo el literal de objeto pasado como prop, no una variable en el ámbito. En código real se utilizaría una variable definida en el componente padre, por ejemplo onAddToCart={() => addToCart(item.id)}. Pasar funciones como props es la forma estándar para que los componentes hijos informen eventos, lo cual se mencionará nuevamente más abajo.
Los props son de solo lectura
Un componente nunca debe modificar sus propios props. Durante todo el proceso de renderizado, estos son valores fijos. Cuando es necesario hacer cambios, el componente padre los modifica y vuelve a renderizar al hijo con el nuevo valor. Reasignar un prop dentro del hijo es un error:
// WRONG — never modify props
function Button({ count }) {
count = count + 1; // ← this is a mistake
return <button>{count}</button>;
}
La reasignación solo modifica una variable local; nunca llega al componente padre y desaparece en la siguiente renderización. Cuando un componente necesita un valor que pueda cambiar, para eso existe el estado:
// CORRECT — props are read-only, use state for data that changes
function Button({ initialCount }) {
const [count, setCount] = useState(initialCount);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
Aquí initialCount solo inicializa el estado; después de la primera renderización, el componente controla su propio conteo.
Estado: la memoria propia de un componente
Los props provienen del exterior. El estado es datos que pertenecen al componente y pueden cambiar con el tiempo. Cuando el estado cambia, React vuelve a renderizar el componente con el nuevo valor, y uno nunca manipula directamente el DOM.
El estado se crea con el hook useState, importado de React:
import { useState } from "react";
Un contador muestra la estructura básica:
function Counter() {
const [count, setCount] = useState(0);
// ↑ current value ↑ function to update it ↑ initial value return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setCount(count - 1)}>Decrement</button>
<button onClick={() => setCount(0)}>Reset</button>
</div>
);
}
useState(0) devuelve un par: el valor actual (count, que comienza en 0) y un setter (setCount). Al llamar al setter con un nuevo valor, se indica a React que lo guarde y vuelva a renderizar el componente.
Volver a construir el botón de “me gusta” con estado
El botón imperativo de “me gusta” que había al principio se vuelve mucho más pequeño. Comience con la importación:
import { useState } from "react";
y luego describa el botón para cualquier combinación de liked y count:
function LikeButton({ initialLikes }) {
const [liked, setLiked] = useState(false);
const [count, setCount] = useState(initialLikes); function handleClick() {
if (liked) {
setLiked(false);
setCount(c => c - 1);
} else {
setLiked(true);
setCount(c => c + 1);
}
} return (
<button
onClick={handleClick}
className={liked ? "btn-liked" : "btn-default"}
>
{liked ? "♥" : "♡"} {count}
</button>
);
}
No hay búsquedas de elementos, ni llamadas a classList ni asignaciones a textContent. El manejador actualiza dos valores de estado, y el JSX determina cómo se verá el botón con esos valores. React hace que el DOM coincida.
Fíjese en el formulario de actualización setCount(c => c - 1). Al pasar una función en lugar de un valor, se obtiene el estado más reciente, lo cual es más seguro cuando varias actualizaciones están en cola en el mismo evento.
Cada instancia mantiene su propio estado
El estado pertenece a una instancia específica de un componente, no a la definición del componente. Al renderizar tres botones similares, cada uno tiene su propio liked y count; al hacer clic en uno, los demás permanecen sin cambios:
function Feed() {
return (
<div>
<LikeButton initialLikes={24} /> {/* has its own state */}
<LikeButton initialLikes={7} /> {/* has its own state */}
<LikeButton initialLikes={156} /> {/* has its own state */}
</div>
);
}
Qué debe estar en el estado
Use el estado para valores que:
- cambian con el tiempo debido a interacciones o datos recibidos
- deben actualizar la interfaz de usuario cuando cambian
- pertenecen a esta instancia concreta del componente
Mantenga fuera del estado todo aquello que pueda calcularse a partir de propiedades o estado existentes (calcúlelo durante el renderizado en su lugar), así como los valores que cambian sin necesidad de un nuevo renderizado, como un ID de temporizador, los cuales encajan mejor en un ref.
Cómo funciona el re-renderizado
El re-renderizado es el motor que impulsa todo el modelo. Cuando cambia el estado, React llama nuevamente a la función de su componente, obtiene una descripción actualizada de la interfaz de usuario, la compara con la anterior y actualiza solo las partes del DOM que difieren.
En el primer renderizado, el contador genera su marcado y React crea los nodos del DOM:
Initial render:
count = 0
Component runs → returns <p>Count: 0</p> <button>Increment</button>
React creates DOM nodes
Cuando el usuario hace clic, el setter desencadena un nuevo renderizado y React compara las dos descripciones:
User clicks Increment:
setCount(1) called
React re-renders the component
count = 1
Component runs again → returns <p>Count: 1</p> <button>Increment</button>
React compares: <p>Count: 0</p> vs <p>Count: 1</p>
React updates only the text node inside <p>
Button is unchanged — React leaves it alone
Solo el texto dentro del párrafo cambia en el DOM real. El botón permanece sin modificaciones. React no vuelve a construir la página; calcula el conjunto más pequeño de cambios y aplica únicamente esos, por lo que las actualizaciones frecuentes suelen estar bien.
¿Qué hace que un componente se vuelva a renderizar?
Un componente se vuelve a renderizar cuando cambia su propio estado:
1. The component's own state changes (setCount, setUser, etc.)
↓
Component re-renders
También se vuelve a renderizar en algunas otras situaciones:
2. The component's props change (parent passes different values)
↓
Component re-renders3. A context the component uses changes
↓
Component re-renders4. The parent re-renders
↓
All children re-render (unless memoized)
En la práctica, “props cambiados” y “padre se volvió a renderizar” son el mismo evento visto desde dos perspectivas: un hijo solo recibe nuevos props porque su padre se volvió a renderizar. La consecuencia práctica es que, por defecto, la renderización de un padre se transmite a todos sus hijos, independientemente de si sus props han cambiado o no, a menos que estén memorizados.
Esa secuencia de renderizaciones rara vez representa un problema. Un renderizado es simplemente una llamada a función que genera objetos; la parte costosa es modificar el DOM real, algo que React minimiza al máximo. La mayoría de los problemas reales de rendimiento provienen de cómo están estructurados los componentes y el estado, no del número bruto de renderizaciones.
El DOM virtual y la reconciliación
React mantiene una representación ligera en JavaScript de la interfaz de usuario, a menudo llamada DOM virtual. Cada renderización genera un nuevo árbol de objetos de elementos, y React lo compara con el árbol anterior en un proceso denominado reconciliación. El resultado de esa comparación es la lista de operaciones reales en el DOM que deben realizarse.
Nunca se trabaja directamente con esta capa; es un detalle de implementación. Es importante porque comparar objetos simples en memoria es rápido, mientras que las operaciones reales del DOM son relativamente lentas. Al enrutar cada actualización a través de esta comparación, React puede agrupar y minimizar el trabajo costoso.
UI declarativa versus imperativa
React es declarativo, y comprender ese término representa el verdadero cambio de enfoque. Compare dos formas de renderizar una lista.
Imperativa: enumerar los pasos
La versión imperativa primero vacía el contenedor:
// Imperative: manual DOM manipulation
const list = document.getElementById("list");
list.innerHTML = ""; // clear it
luego crea, configura y agrega cada elemento manualmente:
items.forEach(item => {
const li = document.createElement("li");
li.textContent = item.name;
li.className = item.active ? "active" : "";
li.addEventListener("click", () => handleClick(item.id));
list.appendChild(li);
});
El código es una secuencia de comandos: crear este nodo, establecer esa propiedad, adjuntar este oyente, insertarlo allí. Usted es responsable de lo que ocurre antes, después y en cada paso intermedio.
Declarativa: describir el resultado
La versión declarativa indica cómo debería verse la lista para cualquier arreglo de items:
// Declarative: describe the desired output
function ItemList({ items, onItemClick }) {
return (
<ul>
{items.map(item => (
<li
key={item.id}
className={item.active ? "active" : ""}
onClick={() => onItemClick(item.id)}
>
{item.name}
</li>
))}
</ul>
);
}
No existe un método de “borrar la lista y luego reconstruirla”. Usted describe el estado deseado, y React determina cómo pasar del DOM actual a ese estado. La propiedad key le indica a React qué elemento es cual entre las diferentes renderizaciones, de modo que pueda mover, actualizar o eliminar los elementos adecuados en lugar de recrearlos todos.
Por qué el estilo declarativo es adecuado para las interfaces de usuario
- Prediccibilidad. Con las mismas propiedades y estado, un componente muestra el mismo resultado. Puede tratarse la interfaz como una función pura de los datos:
UI = f(state, props). - Menos cosas que recordar. Nunca es necesario seguir de cerca cómo se ve actualmente el DOM ni qué modificaciones necesita. Solo se describe el estado actual.
El árbol de componentes y el flujo de datos unidireccional
Cada aplicación React es un árbol. Un componente raíz, generalmente App, renderiza sus hijos, quienes a su vez renderizan los suyos, reflejando así la estructura de la pantalla:
App
├── Navbar
│ ├── Logo
│ ├── NavLinks
│ └── UserMenu
│ ├── Avatar
│ └── DropdownMenu
├── Dashboard
│ ├── Sidebar
│ │ ├── SidebarLink (×5)
│ │ └── UserStats
│ └── MainContent
│ ├── StatsRow
│ │ ├── StatCard (×4)
│ └── RecentActivity
│ ├── ActivityItem (×10)
└── Footer
El flujo de datos hacia abajo
Los datos se transfieren de padre a hijo a través de props. Un componente no puede acceder al estado de un hermano o de su padre. Ese flujo unidireccional es lo que mantiene las aplicaciones grandes comprensibles:
App (has user, notifications, theme)
│
├── Navbar (receives: user, notifications)
│ │
│ └── UserMenu (receives: user)
│ │
│ └── Avatar (receives: user.avatar, user.name)
│
└── Dashboard (receives: user, theme)
│
└── MainContent (receives: theme)
Avatar solo ve lo que le proporciona UserMenu, y UserMenu solo ve lo que le proporciona Navbar. Nada se filtra hacia los lados o hacia arriba, por lo que cuando un valor es incorrecto se debe buscar en la cadena de padres.
El flujo de eventos hacia arriba
Un componente ubicado en lo más profundo del árbol no puede modificar directamente el estado de su padre, pero sí puede llamar a una función que este le haya pasado. El padre es quien controla el estado:
// Parent owns the state and passes down both the value and the updater
function App() {
const [searchQuery, setSearchQuery] = useState("");
Y transmite tanto el valor como el método de configuración a sus hijos; la barra de búsqueda llama al método de configuración cada vez que cambia la entrada:
return (
<div>
<SearchBar
query={searchQuery}
onChange={setSearchQuery} {/* passes the setter as a prop */}
/>
<Results query={searchQuery} />
</div>
);
}// Child receives the updater and calls it on user input
function SearchBar({ query, onChange }) {
return (
<input
value={query}
onChange={e => onChange(e.target.value)} {/* calls parent's setter */}
placeholder="Search..."
/>
);
}
La consulta solo existe en App. SearchBar no almacena nada; informa sobre cada tecla pulsada a través de la propiedad onChange. Results recibe la consulta actualizada como propiedad y vuelve a renderizarse. Los datos fluyen hacia abajo como propiedades, mientras que los eventos fluyen hacia arriba como llamadas a funciones. (Los comentarios explicativos dentro de las etiquetas de apertura son solo para lectura; en JSX real, un comentario entre corchetes debe ir entre los hijos, y dentro de una etiqueta se escribiría un comentario simple /* */ o se omitiría.)
Errores que interrumpen las actualizaciones de React
Mutar el estado directamente
Es tentador modificar un objeto de estado directamente y pasarlo de vuelta:
// WRONG — mutating state directly
const [user, setUser] = useState({ name: "Priya", age: 28 });
El primer birthday de abajo modifica el objeto y le pasa al setter la misma referencia, por lo que la interfaz de usuario no se actualiza. El segundo crea un nuevo objeto con el operador spread, lo cual React considera como un cambio:
function birthday() {
user.age = 29; // ← directly modifying the object
setUser(user); // ← same object reference — React sees no change
} // UI does NOT update// CORRECT — create a new object
function birthday() {
setUser({ ...user, age: user.age + 1 }); // ← new object, React detects change
}
React determina si el estado ha cambiado comparando las referencias con Object.is, no inspeccionando su contenido. Si se modifica un objeto o array en el mismo lugar y se pasa de vuelta la misma referencia, React concluye que no ha ocurrido ningún cambio y podría omitir el renderizado.
Los arrays siguen la misma regla. Intentar agregar elementos al array existente fallará:
// WRONG — mutating the array
const [items, setItems] = useState([1, 2, 3]);
items.push(4); // ← modifies in place
setItems(items); // ← same reference — no re-render
Mientras que expandirlo en un nuevo array funciona:
// CORRECT — create a new array
setItems([...items, 4]); // ← spread creates a new array
Confundir props y estado
Una prueba rápida ayuda a distinguirlos. ¿El valor proviene de un padre? Entonces es una prop y de solo lectura. ¿El componente lo posee y lo modifica con el tiempo? Entonces es estado. Envolver un valor que nunca cambia en useState es señal de confusión:
// Wrong: using state for something that should be a prop
function UserCard() {
const [userName] = useState("Priya"); // ← why is this state? It never changes here
return <p>{userName}</p>;
}
Si el padre controla los datos, úsalo como prop:
// Correct: static data the parent controls comes as a prop
function UserCard({ name }) {
return <p>{name}</p>;
}
Almacenar valores que se podrían calcular
No todo lo que cambia necesita tener su propio estado. Mantener un recuento junto a la lista en la que se cuenta duplica la información:
// Wrong: storing derived data in state
const [items, setItems] = useState([...]);
const [itemCount, setItemCount] = useState(0); // ← why is this state?
Ahora, cada cambio en items requiere una actualización correspondiente en itemCount, y un solo olvido en la actualización hace que ambos estén desactualizados. Calcularlo durante el renderizado mantiene los dos sincronizados por diseño:
// Every time items changes, you have to remember to also update itemCount
// And if you forget once, they're out of sync// Correct: derive it during render
const [items, setItems] = useState([...]);
const itemCount = items.length; // ← just a variable — always in sync
Componentes que hacen de todo
Un único componente que renderiza una barra lateral, una tabla, un formulario y varios modales es difícil de leer, probar y reutilizar. Una heurística útil: si no puedes describir en una sola oración breve qué hace un componente, entonces está haciendo demasiado.
// Hard to maintain — does everything
function UserDashboard() {
// manages user state
// fetches orders
// handles filters
// manages pagination
// controls modal open/close
// renders sidebar
// renders order table
// renders filter controls
// renders pagination
// renders modal
return ( /* 200 lines of JSX */ );
}
Dividirlo por responsabilidades le da a cada parte un nombre y una función específica:
// Better — clear single responsibilities
function UserDashboard() {
return (
<DashboardLayout>
<OrderFilters />
<OrderTable />
<OrderPagination />
<OrderDetailModal />
</DashboardLayout>
);
}
Diseñar una aplicación con componentes
Dibuja los límites antes de escribir código
Comience con el diseño y marque las piezas. Todo lo que se repite es un componente. Cualquier elemento con una función clara también es un componente. Las cosas que cambian juntas deben estar relacionadas; aquellas que cambian de forma independiente deben separarse. En una página de lista de productos, el diseño:
Looking at the design:
[ Filter Bar ]
[ Product Card ][ Product Card ][ Product Card ]
[ Product Card ][ Product Card ][ Product Card ]
[ Pagination ]
se desglosa en este árbol:
Components:
ProductPage
├── FilterBar
│ ├── FilterGroup (×3)
│ └── SortDropdown
├── ProductGrid
│ └── ProductCard (×N)
└── Pagination
Coloque el estado en el propietario común más bajo
Para cada elemento de estado, encuentre el componente más bajo del árbol que lo necesite y guarde allí el estado:
If only ProductCard needs the "expanded" state → put it in ProductCard
If FilterBar and ProductGrid both need filters → put filters in ProductPage
If Pagination and ProductGrid both need currentPage → put it in ProductPage
Cuando dos elementos hermanos necesitan los mismos datos, mueva el estado al padre compartido más cercano y transmítalo hacia abajo. Esto se conoce como elevar el estado. Mantener el estado lo más bajo posible también limita cuántas partes del árbol se vuelven a renderizar cuando cambia.
Construya primero una versión estática y luego agregue el estado
Comience con datos codificados de antemano: sin useState, sin manejadores, solo marcado controlado por propiedades. Una vez definido el layout, determine qué valores realmente cambian con el tiempo e introduzca estado solo para esos. El primer paso es una tarjeta completamente estática:
// Step 1: static — no state, hardcoded data
function ProductCard() {
return (
<div className="card">
<img src="/headphones.jpg" alt="Headphones" />
<h3>Wireless Headphones</h3>
<p>₹2,499</p>
<button>Add to Cart</button>
</div>
);
}
El segundo paso reemplaza el contenido codificado por una propiedad product, y el tercer paso agrega un pequeño fragmento de estado para la confirmación “Añadido”, que se reinicia automáticamente después de dos segundos:
// Step 2: accept props — still no state
function ProductCard({ product }) {
return (
<div className="card">
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p>₹{product.price.toLocaleString()}</p>
<button>Add to Cart</button>
</div>
);
}// Step 3: add state for interactivity
function ProductCard({ product, onAddToCart }) {
const [added, setAdded] = useState(false); function handleAdd() {
setAdded(true);
onAddToCart(product.id);
setTimeout(() => setAdded(false), 2000);
} return (
<div className="card">
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p>₹{product.price.toLocaleString()}</p>
<button onClick={handleAdd} disabled={added}>
{added ? "Added ✓" : "Add to Cart"}
</button>
</div>
);
}
Dado que la estructura y la interactividad están separadas, cada paso es fácil de verificar por separado.
Puntos clave
El modelo completo, resumido. Por qué existe React:
Why React:
Plain JS DOM manipulation is hard to scale and maintain
React: describe the UI, let React handle DOM updates
y los aspectos esenciales de cada concepto mencionado anteriormente, desde JSX hasta errores comunes:
JSX:
HTML-like syntax in JavaScript — compiled to React.createElement()
{} embeds any JavaScript expression
Use className (not class), onClick (not onclick)Components:
Functions that return JSX
Capital letter names (Button, not button)
Reusable, composable building blocksProps:
Data passed from parent to child (like function arguments)
Read-only — a component never modifies its own props
Can be strings, numbers, objects, arrays, functionsState:
const [value, setValue] = useState(initialValue)
Data owned by a component that changes over time
Calling the setter triggers a re-render
Each component instance has its own stateRe-rendering:
Happens when state changes, props change, or parent re-renders
React diffs the virtual DOM and updates only what changed
Not expensive — surgical DOM updatesDeclarative:
Describe what the UI should look like
React figures out what changed and how to update the DOM
UI = f(state, props) — predictable, testableData flow:
Props flow down (parent → child)
Events flow up (child calls parent's function)
One-way data flow keeps the application predictableCommon mistakes:
Mutating state directly (use spread / new objects)
Storing derived values in state (compute them during render)
Overloading one component (split by responsibility)
- Los componentes son funciones, los props son sus argumentos y el estado es su memoria. La interfaz de usuario siempre es una proyección del estado actual.
- La vuelta a renderizar es económica por diseño; el trabajo en el DOM que React te ahorra es la parte costosa. Estructura bien el estado antes de preocuparte por el número de renders.
- Siempre proporciona a los setters nuevos objetos y arrays; React compara referencias, no contenidos.
- Mantén el estado en el componente más bajo que lo necesite, deriva todo lo demás durante el renderizado y deja que los eventos fluyan hacia arriba a través de los props de callback.
El contexto, los efectos, los hooks personalizados y el ajuste del rendimiento se basan en estas ideas. Con un dominio sólido de los componentes, los props, el estado y la vuelta a renderizar, las partes más avanzadas de React se convierten en extensiones de un modelo que ya comprendes, en lugar de nuevas reglas para memorizar.
Lectura relacionada
- Evitando errores de estado silencioso por mutación de referencias en JavaScript — Aprenda por qué la mutación de objetos y arrays mediante referencias interrumpe las actualizaciones de React, por qué la operación spread solo realiza copias superficiales, y cómo clonar profundamente el estado de forma segura.
- Comprendiendo los ganchos personalizados en React: reutilización de lógica sin estado compartido — Aprenda qué son los ganchos personalizados en React, cómo extraen y comparten lógica con estado entre componentes, y qué errores comunes evitar al crearlos.