Los componentes de React deben ser puros: la idea detrás de StrictMode y el compilador
Por qué React espera componentes puros, cómo el doble renderizado de StrictMode revela la impureza, dónde deben situarse los efectos secundarios, y por qué el compilador de React depende de la pureza.
Fuentes: React Components Must Be Pure — the Secret Behind StrictMode and the Compiler
Hasta ahora, la historia de React ha sido “escríbelo de esta manera y la pantalla aparecerá”. Sin embargo, existe una regla que React espera silenciosamente que siga cada componente, y que explica dos cosas que confunden a la mayoría de los recién llegados: por qué StrictMode llama a tu componente dos veces, y cómo el Compilador de React puede optimizar el código de manera tan audaz. Esa regla es la pureza.
Qué es una función pura
Recuerda las matemáticas: y = f(x) = 2x devuelve 6 para una entrada de 3. Lo hace ayer, hoy y en la centésima llamada, y nunca intenta cambiar nada en el mundo más allá de sí mismo.
Una función así es pura, lo que significa que se cumplen dos condiciones.
- La misma entrada siempre da el mismo resultado.
- No tiene efectos secundarios, por lo que nada fuera de su propio ámbito se modifica.
El código lo hace más claro. double a continuación es puro, mientras que addToTotal no lo es.
// ✅ pure function — same input, same output, and it touches nothing outside
function double(x: number): number {
return x * 2;
}
double(3); // 6
double(3); // 6 — six no matter how many times you call it
// 🔴 impure function — it changes the outer variable `total` (a side effect)
let total = 0;
function addToTotal(x: number): number {
total += x; // side effect!
return total; // the result changes on every call
}
addToTotal(3); // 3
addToTotal(3); // 6 — same input, different result
Los componentes también deben ser puros
React asume que cada componente es una función pura. Con las mismas props y estado, debe devolver siempre el mismo JSX.
type Props = { name: string };
// ✅ pure — same name, always the same result
function Greeting({ name }: Props) {
return <h1>Hello, {name}!</h1>;
}
Al asignarle a este componente el nombre “Jane”, siempre se obtiene <h1>Hello, Jane!</h1>. React depende de esa previsibilidad para actualizar la pantalla de manera eficiente.
Incumplir la regla: modificar elementos externos durante el renderizado
Los problemas surgen cuando un componente modifica algo fuera de sí mismo mientras se está renderizando.
let count = 0; // a variable outside the component
// 🔴 not pure — changes an external variable while rendering
function Counter() {
count = count + 1; // side effect!
return <p>{count}</p>;
}
Counter incrementa la variable externa count en cada renderizado, por lo que su salida depende de la frecuencia con la que se llama: 1 la primera vez, 2 la segunda. Ambas condiciones de pureza no se cumplen. React puede renderizar un componente varias veces o en un orden diferente, por lo que un componente como este genera errores difíciles de predecir.
Pruébalo y observa por qué obtienes 2
Al ejecutar ese Counter, lo primero que aparece en la pantalla es 2, no 1, y no aumenta posteriormente. Al principio resulta desconcertante, pero entenderlo constituye una excelente lección sobre pureza.
¿Por qué 2? En el entorno de desarrollo, StrictMode llama a la función del componente dos veces al montarlo, por lo que un único montaje ejecuta Counter dos veces.
- Primera llamada:
countpasa de 0 a 1 y el resultado es<p>1</p>. - Segunda llamada:
countpasa de 1 a 2 y el resultado es<p>2</p>.
La pantalla muestra el segundo resultado. Con StrictMode desactivado, la función se ejecuta una sola vez y se vería 1.
¿Por qué se detiene en 2 en lugar de seguir a 3, 4 y así sucesivamente? Un componente solo se ejecuta cuando React lo vuelve a renderizar. Este no tiene estado y su padre no lo está volviendo a renderizar, por lo que se ejecuta en el montaje inicial (dos veces, gracias a StrictMode) y luego permanece inactivo, dejando count atascado.
Para observar cómo varía el valor, fuerce un nuevo renderizado. El siguiente ejemplo lo hace con un clic en un botón. (useState recibirá un trato adecuado más adelante, así que por ahora considérelo como un dispositivo que vuelve a dibujar la pantalla al presionar el botón.)
import { useState } from 'react';
let count = 0;
function Counter() {
const [, setTick] = useState(0); // a device to trigger re-renders
count = count + 1; // 🔴 changes an external variable on every render
return (
<div>
<p>{count}</p>
<button onClick={() => setTick((n) => n + 1)}>Re-render</button>
</div>
);
}
Cada vez que se hace clic, se vuelve a renderizar Counter a través de su propio cambio de estado, y StrictMode lo llama dos veces cada vez, por lo que el número aumenta en dos: 2, 4, 6 y así sucesivamente. Sin StrictMode, aumentaría uno a la vez.
Una advertencia sobre esta demostración: ¿por qué se coloca el estado dentro de Counter? Si lo movieras al componente padre y dejaras Counter como hijo sin propiedades, hacer clic en el botón no cambiaría el 2 que se muestra. Un componente que no recibe entrada es aquel cuyo resultado nunca puede variar, por lo que el compilador de React lo almacena en caché y omite su re-renderizado. Para observar este efecto, debes modificar la variable externa dentro del componente que realmente se vuelve a renderizar. Curiosamente, ese comportamiento es en sí mismo una buena ilustración del hecho de que el compilador asume pureza. La forma en que el compilador almacena en caché los componentes merece un análisis aparte.
He aquí el punto clave. Un componente puro debería dar el mismo resultado sin importar cuántas veces se llame, pero este depende de la cantidad de llamadas. StrictMode lo llama dos veces a propósito para mostrarle esa impureza.
La solución es no interferir con lo externo. Durante el renderizado, calcule el resultado únicamente a partir de sus propios props y estado.
// ✅ pure — computed from input (props) alone
function Counter({ count }: { count: number }) {
return <p>{count}</p>;
}
Puedes cambiar lo que hayas creado durante el renderizado
La pureza no significa no cambiar nada. Significa no modificar elementos que existían antes de que comenzara el renderizado, como las variables externas, los props y el estado. Las variables o arrays que creas desde cero durante el renderizado son tuyos para modificar. Eso es simplemente un cálculo, no un efecto secundario.
function ProductList({ products }: { products: Product[] }) {
// ✅ this array was just created in this render — handle it however you like
const sorted = [...products].sort((a, b) => a.price - b.price);
return (
<ul>
{sorted.map((p) => (
<li key={p.id}>{p.name}</li>
))}
</ul>
);
}
Lo importante es la copia creada con [...products]. Ordenar el array original de products, que llega como propiedades, modificaría esas propiedades y violaría la regla. La copia se creó en esta renderización, por lo que está bien.
Primer misterio: ¿por qué StrictMode ejecuta las cosas dos veces?
En el desarrollo, StrictMode llama deliberadamente a las funciones de los componentes dos veces para detectar aquellos que no son puros. Se trata del mismo contenedor <StrictMode> que se coloca alrededor de la aplicación al configurar el proyecto.
// main.tsx — in development, the components inside this render twice
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
);
Un componente puro produce resultados idénticos en ambas ejecuciones, por lo que no ocurre nada. Un componente como Counter mencionado anteriormente, que accede a una variable externa, muestra signos de alerta de inmediato: la salida varía o un valor se duplica. StrictMode saca a la luz estos síntomas mientras aún se está desarrollando, mucho antes de que se conviertan en problemas difíciles de resolver una vez que la aplicación está en funcionamiento.
Por lo tanto, la doble ejecución no es un error. Es una verificación de estado. Si ejecutarse dos veces no causa problemas, el componente es puro. En una versión de producción, solo se ejecuta una vez.
Dónde realmente pertenecen los efectos secundarios
Plantearás una pregunta lógica: el registro de datos, el envío de información a un servidor y el inicio de un temporizador son claramente necesarios, ¿entonces dónde deben ir? A cualquier lugar que no sea la propia renderización.
- Reaccionar a acciones del usuario como clics y escritura debe hacerse en los manejadores de eventos.
- La sincronización con un sistema externo después de que se haya dibujado la pantalla debe incluirse en Efectos.
Supongamos que registras una visualización de página directamente en el cuerpo de renderizado: se ejecutará con cada renderizado, y se duplicará en modo StrictMode durante el desarrollo. Si trasladas esa llamada a un manejador de eventos, se producirá solo cuando alguien haga clic realmente.
// 🔴 side effect during rendering — fires on every render
function ProductPage({ id }: { id: number }) {
logView(id); // a side effect in the render body — not allowed
return <h1>Product {id}</h1>;
}
// ✅ in an event handler — runs only at the moment the user clicks
function BuyButton({ id }: { id: number }) {
return <button onClick={() => logPurchase(id)}>Buy</button>;
}
Mantenga el cuerpo de renderizado puro y traslade los efectos secundarios a sus lugares adecuados. Ese principio vuelve a aparecer una y otra vez.
Segundo misterio: por qué el compilador depende de la pureza
Si ha activado el React Compiler, probablemente recuerde que se hace cargo de las funciones useMemo y useCallback escritas a mano. La pureza es lo que hace posible esto.
El compilador funciona bajo la suposición de que los componentes son puros. Dado que se garantiza que la misma entrada producirá siempre la misma salida, considera seguro reutilizar el resultado anterior cuando las entradas no han cambiado, e inserta automáticamente la memorización. Si un componente modifica algo durante el renderizado, la reutilización podría generar resultados incorrectos, por lo que el compilador omite la optimización en aquellos componentes que violan esta regla. La regla de ESLint activada junto con el compilador le indica qué componente es el culpable.
// 🔴 not pure → the compiler skips optimization (ESLint points at this component)
let renderCount = 0;
function RenderCounter() {
renderCount++; // mutating an external variable during render — a violation
return <p>{renderCount}</p>;
}
// ✅ pure → the compiler memoizes automatically (reuse the previous result when inputs match)
function Label({ text }: { text: string }) {
return <p>{text}</p>;
}
function App() {
return (
<>
<RenderCounter />
<Label text="Pure Component" />
</>
);
}
export default App;
Un componente escrito de forma pura aparece en DevTools con la insignia “Memo”, aunque no se haya escrito useMemo.
En resumen, la pureza es la condición previa para que el compilador pueda ayudarte. Escribe componentes puros y las mejoras de rendimiento se lograrán automáticamente. Lo único de lo que debes preocuparte es escribir código puro.
Viendo la interfaz de usuario como un árbol
Otra perspectiva completa este tema. Los componentes contienen otros componentes: App contiene ProductList, que a su vez contiene varias ProductCard. Ese anidamiento forma naturalmente un árbol.
function App() {
return <ProductList products={products} />;
}
function ProductList({ products }: { products: Product[] }) {
return (
<ul>
{products.map((p) => (
<ProductCard key={p.id} product={p} />
))}
</ul>
);
}
A medida que los componentes se anidan, este código genera el siguiente árbol.
App
└─ ProductList
├─ ProductCard
├─ ProductCard
└─ ProductCard
A medida que avanzas, la vista en forma de árbol cobra aún más importancia. Los datos en forma de props fluyen a través de ella de arriba hacia abajo, y en cuanto al estado, que aprenderás más adelante, decidir dónde colocarlo en el árbol se convierte en el eje central de tu diseño. Ten presente la imagen “la pantalla equivale al árbol de componentes” y hasta las interfaces de usuario más complejas pueden analizarse estructuralmente.
Resumen
- React trata cada componente como una función pura: mismos props y estado, mismo JSX.
- Durante la renderización, no cambies nada externo, incluidas variables, props y estado. Está bien modificar los valores creados recientemente en la renderización actual, por ejemplo al ordenar una copia.
- La ejecución doble de StrictMode en el desarrollo es una verificación de estado que detecta componentes impuros. Un componente puro pasa sin cambios.
- Coloca los efectos secundarios en los manejadores de eventos o en Effects, nunca dentro del cuerpo de la renderización.
- La pureza es lo que permite al compilador de React optimizar automáticamente, por lo que escribir código puro te brinda mejor rendimiento.
- La interfaz de usuario es un árbol de componentes, y los datos fluyen a través de él.
Hasta ahora la pantalla es una imagen fija: puedes armarla con componentes, escribir marcado con JSX, pasar datos mediante propiedades y crear vistas dinámicas con condiciones y bucles, pero al presionar un botón no ocurre nada. El siguiente paso es hacer que la pantalla responda al usuario, y esto comienza con el manejo de eventos.
Las páginas oficiales de documentación de React sobre mantener los componentes puros y sobre entender la interfaz de usuario como un árbol abordan el mismo tema con más profundidad.
Leer más
- Construyendo un agente ReAct en Python puro: Pensamiento, Acción, Observación — Escribir un bucle de agente ReAct desde cero en Python: el contrato de Pensamiento, Acción, Observación, un bucle while controlado por PAUSE, retroalimentación de errores y dos modos de fallo en producción.
- Siete arquitecturas RAG más allá del pipeline ingenuo y cuándo aplicar cada una — El RAG ingenuo falla de maneras predecibles. Siete arquitecturas, desde notas contextuales hasta sistemas multiagente y recuperación de memoria, con sus ventajas y desventajas y cuándo utilizar cada una.