Módulo 1: Thinking In Components

La UI como función de las props: `UI = f(props)`

Descripción

En la lección anterior clavaste que un componente es una función que recibe props y devuelve UI. Esta lección toma esa función y la pone en movimiento, para ver la idea central de toda la guía funcionando frente a ti: la UI es una función de las props, UI = f(props).

Desmenucemos la ecuación con cuidado, porque cada símbolo importa. La f es tu componente. Las props son la entrada: los datos que le das. La UI es la salida: la interfaz que la función produce para esa entrada. La ecuación dice tres cosas a la vez. Primero, que la UI está determinada por las props: dime qué props le pasaste a ProductCard y te digo, sin verlo correr, qué UI produce. Segundo, que si las props cambian, la UI cambia con ellas —no hay UI "fija"; hay una función que la genera según la entrada—. Y tercero, que la relación es de una sola dirección: las props producen la UI, nunca al revés (la UI no modifica las props).

Todo esto ya lo intuiste en la lección 1, cuando el mismo ProductCard dio tarjetas distintas para el mouse y el teclado. Aquí lo hacemos explícito y lo medimos: correremos el mismo componente con tres productos, veremos la salida cambiar campo por campo, y comprobaremos la propiedad que hace confiable a todo esto —que un componente es una función pura: mismas props, misma UI, siempre—.

Conexión con el módulo. UI = f(props) es la tesis del módulo (y, con estado en lugar de props, la de la guía entera). Todo lo demás son consecuencias suyas. La composición (lección 5) funciona porque cada componente es una f predecible que puedes anidar dentro de otra sin sorpresas. El flujo de props hacia abajo (lección 6) tiene sentido porque las props son la entrada que gobierna la salida. Y el reuso (lección 7) es posible porque la misma f produce resultados correctos para cualquier entrada válida. Si f no fuera predecible —si el mismo ProductCard a veces diera una cosa y a veces otra para las mismas props—, nada de esto se sostendría. Por eso esta lección es el pilar.

Una analogía: la máquina expendedora

Imagina una máquina expendedora de refrescos. Tiene botones (A1, A2, B3...) y, por cada botón, entrega un producto. La relación es fija y predecible: aprietas B3, cae la misma botella de agua, siempre. No importa la hora, no importa quién lo apriete, no importa cuántas veces: el mismo botón produce el mismo resultado. Esa es la esencia de una función: una entrada produce una salida determinada.

Un componente es esa máquina. Las props son el botón que aprietas (la entrada); la UI es lo que cae (la salida). ProductCard con las props del mouse "entrega" la tarjeta del mouse; con las props del teclado, la del teclado. Mismas props → misma tarjeta, siempre.

Ahora imagina una máquina rota: aprietas B3 y a veces cae agua, a veces cae una goma de mascar, a veces nada. Nadie confiaría en ella; no podrías razonar sobre qué vas a obtener. Una función que a veces da una cosa y a veces otra para la misma entrada es igual de inútil: se llama impura, y es justo lo que un componente no debe ser. Un componente que dependa de la hora, de un número aleatorio, o que modifique algo del mundo al ejecutarse, es una máquina expendedora rota: impredecible.

El valor de una máquina que funciona es que puedes razonar sobre ella sin usarla: sabes que B3 da agua, punto. El valor de un componente puro es idéntico: puedes saber qué UI produce para unas props sin ejecutarlo, solo leyéndolo. Esa capacidad de predecir es, literalmente, lo que hace manejable una app con cientos de componentes. Guarda la máquina expendedora; es UI = f(props) en una imagen.

Ejemplo trabajado: el mismo componente, tres productos

Vamos a ver la ecuación funcionando: correremos el mismo ProductCard con tres productos distintos y observaremos, campo por campo, cómo la salida sigue a la entrada. Luego comprobaremos el determinismo.

El componente, en JSX (la API real; es el mismo de las lecciones anteriores):

function ProductCard(props) {
  return (
    <article className="product-card">
      <h3 className="product-name">{props.name}</h3>
      <p className="product-price">{formatPrice(props.priceCents)}</p>
      <span className="product-stock">
        {props.inStock ? 'In stock' : 'Out of stock'}
      </span>
    </article>
  );
}

Y la versión ejecutable en Node. Definimos tres productos como entrada y aplicamos ProductCard a cada uno, imprimiendo la entrada (f(props)) y su salida (el HTML):

function h(tag, props, ...children) {
  return { tag, props: props || {}, children: children.flat() };
}
function renderToString(node, indent = 0) {
  const pad = '  '.repeat(indent);
  if (node == null || typeof node === 'boolean') return '';
  if (typeof node !== 'object') return pad + node;
  const attrs = Object.entries(node.props)
    .map(([k, v]) => ` ${k === 'className' ? 'class' : k}="${v}"`).join('');
  const kids = node.children.filter((c) => c != null && c !== false);
  if (kids.length === 1 && typeof kids[0] !== 'object')
    return `${pad}<${node.tag}${attrs}>${kids[0]}</${node.tag}>`;
  const inner = kids.map((c) => renderToString(c, indent + 1)).join('\n');
  return `${pad}<${node.tag}${attrs}>\n${inner}\n${pad}</${node.tag}>`;
}
function formatPrice(cents) {
  return '$' + (cents / 100).toFixed(2);
}
function ProductCard(props) {
  return h('article', { className: 'product-card' },
    h('h3', { className: 'product-name' }, props.name),
    h('p', { className: 'product-price' }, formatPrice(props.priceCents)),
    h('span', { className: 'product-stock' },
      props.inStock ? 'In stock' : 'Out of stock')
  );
}

// El MISMO componente, tres props distintas -> tres UIs distintas.
const inputs = [
  { name: 'Wireless Mouse',      priceCents: 2599, inStock: true },
  { name: 'Mechanical Keyboard', priceCents: 8900, inStock: false },
  { name: 'USB-C Hub',           priceCents: 3499, inStock: true },
];

for (const props of inputs) {
  console.log(`f(${JSON.stringify(props)})`);
  console.log('=>');
  console.log(renderToString(ProductCard(props)));
  console.log('');
}

// Determinismo: mismas props -> mismo HTML, siempre.
const a = renderToString(ProductCard(inputs[0]));
const b = renderToString(ProductCard(inputs[0]));
console.log('mismas props dos veces -> mismo HTML? ' + (a === b));

Qué esperar. Al correr el archivo, la salida es exactamente esta:

f({"name":"Wireless Mouse","priceCents":2599,"inStock":true})
=>
<article class="product-card">
  <h3 class="product-name">Wireless Mouse</h3>
  <p class="product-price">$25.99</p>
  <span class="product-stock">In stock</span>
</article>

f({"name":"Mechanical Keyboard","priceCents":8900,"inStock":false})
=>
<article class="product-card">
  <h3 class="product-name">Mechanical Keyboard</h3>
  <p class="product-price">$89.00</p>
  <span class="product-stock">Out of stock</span>
</article>

f({"name":"USB-C Hub","priceCents":3499,"inStock":true})
=>
<article class="product-card">
  <h3 class="product-name">USB-C Hub</h3>
  <p class="product-price">$34.99</p>
  <span class="product-stock">In stock</span>
</article>

mismas props dos veces -> mismo HTML? true

Esta salida es UI = f(props) desnuda. Vamos campo por campo.

Observa el nombre. En la entrada cambia ("Wireless Mouse", luego "Mechanical Keyboard", luego "USB-C Hub") y en la salida cambia exactamente igual, dentro del <h3>. La función tomó props.name y lo puso donde debía. No hay más misterio: la entrada gobierna la salida.

Observa el precio. En la entrada viene en centavos como entero (2599, 8900, 3499) y en la salida aparece formateado ($25.99, $89.00, $34.99). Aquí la función no solo copió el dato: lo transformó con formatPrice. Esto es importante: un componente puede calcular sobre sus props, no solo mostrarlas crudas. Pero fíjate en que la transformación también es determinista —formatPrice(2599) siempre da "$25.99"—, así que la predecibilidad se mantiene.

Observa el stock, que es el caso más revelador. En la entrada es un booleano (true, false, true) y en la salida es un texto (In stock, Out of stock, In stock). La función eligió una de dos ramas con el ? : según el valor. Cuando inStock fue true, salió "In stock"; cuando fue false (el teclado), salió "Out of stock". La UI no es un molde rígido: se ramifica según los datos. Y aun así, sigue siendo predecible: true siempre da "In stock", false siempre da "Out of stock".

Y la última línea, mismas props dos veces -> mismo HTML? true, es la demostración del determinismo. Corrimos ProductCard con exactamente las mismas props dos veces, y las dos salidas fueron idénticas (por eso a === b dio true). Esto no es un accidente: es la propiedad que buscamos. Un componente es una máquina expendedora que funciona.

Por qué "función pura" no es un tecnicismo

La palabra formal para lo que acabamos de comprobar es que ProductCard es una función pura. Una función pura cumple dos condiciones:

  1. Su salida depende solo de su entrada. Dadas las mismas props, devuelve siempre la misma UI. No consulta la hora, ni un número aleatorio, ni una variable global que pueda cambiar. Todo lo que necesita, lo recibe en las props.
  2. No tiene efectos secundarios. Al ejecutarse, no cambia nada fuera de sí misma: no modifica las props que recibió, no escribe en disco, no toca la pantalla, no muta variables externas. Solo calcula y devuelve.

¿Por qué React insiste tanto en que los componentes sean puros? Por una razón muy concreta: React puede llamar a tus componentes cuando quiera, las veces que quiera, y en el orden que quiera. Para decidir qué actualizar en la pantalla, React re-ejecuta componentes constantemente. Si ProductCard diera resultados distintos para las mismas props —si fuera impuro—, la pantalla se volvería impredecible: parpadearía, mostraría datos viejos, o peor. La pureza es el contrato que le permite a React ser eficiente y correcto. Tú prometes "para estas props, siempre esta UI"; React aprovecha esa promesa para hacer su trabajo.

La consecuencia práctica para ti es enorme: puedes razonar sobre un componente leyéndolo, sin ejecutarlo. Ves ProductCard, ves las props que le llegan, y sabes qué produce. No tienes que rastrear qué pasó "antes" ni en qué estado quedó "el mundo". Cada componente es una isla predecible. En una app con cientos de ellos, esa propiedad es la diferencia entre poder mantenerla y ahogarte.

Dibujado, UI = f(props) es un flujo de una sola dirección:

flowchart LR
    Props["props<br/>{ name, priceCents, inStock }"] --> F["ProductCard<br/>(la funcion f)"]
    F --> UI["UI<br/>&lt;article&gt;...&lt;/article&gt;"]

Las props entran, pasan por la función, sale la UI. La flecha nunca va en reversa: la UI no modifica las props, y las props no dependen de la UI. Esa unidireccionalidad —que veremos en escala en la lección 6— empieza aquí, en una sola función.

Errores comunes

Introducir impureza sin darte cuenta. Qué pasa: un componente usa algo que cambia entre llamadas —Math.random(), Date.now(), un contador global— para decidir qué renderizar. Por qué pasa: parece inofensivo ("solo quiero un id único", "solo quiero la fecha de hoy"). Cómo detectarlo: el mismo componente con las mismas props produce salidas distintas en corridas distintas; el determinismo se rompe. Cómo corregirlo: todo lo que el componente necesita debe entrar por las props. Si necesitas la fecha, pásala como prop (props.now); si necesitas un id, pásalo como prop (props.id). Así la función vuelve a ser pura: su salida depende solo de su entrada. Lo variable se calcula afuera y se inyecta adentro.

Mutar las props para "ajustar" la salida. Qué pasa: dentro del componente se hace props.name = props.name.toUpperCase() o props.priceCents += tax antes de renderizar. Por qué pasa: uno quiere transformar el dato y lo hace sobre la prop misma. Cómo detectarlo: el componente modifica el objeto de props que recibió. Cómo corregirlo: nunca mutes las props (son de solo lectura, como veremos a fondo en la lección 6). Si necesitas una versión transformada, calcula un valor nuevo sin tocar la prop: const displayName = props.name.toUpperCase() y usa displayName. La prop original queda intacta; el cálculo produce algo aparte. Mutar la entrada rompe la pureza y puede corromper datos que otros componentes comparten.

Confundir "la UI cambia" con "el estado". Qué pasa: alguien ve que el mismo ProductCard produce salidas distintas y concluye "ah, entonces tiene estado". Por qué pasa: se mezcla que la salida varíe con por qué varía. Cómo detectarlo: llamas "estado" a lo que en realidad son solo props distintas. Cómo corregirlo: aquí la UI cambia porque tú le pasas props distintas al llamar la función —la variación viene de afuera, controlada por quien llama—. El estado (módulo 3) es algo diferente: datos que viven dentro del componente y que, al cambiar, hacen que React lo re-ejecute solo, sin que nadie lo vuelva a llamar a mano. En este módulo no hay estado: toda variación de la UI se debe a props distintas que tú controlas. Mantén la distinción clara; te ahorrará confusión en el módulo 3.

Ejercicios

Ejercicio 1 — Predice tres salidas. Sin correr nada, escribe el HTML que ProductCard produciría para estas tres props: (a) { name: 'Webcam', priceCents: 4500, inStock: true }; (b) { name: 'Desk Lamp', priceCents: 1999, inStock: false }; (c) { name: 'Monitor Stand', priceCents: 12000, inStock: true }.

Ver solución
(a)
<article class="product-card">
  <h3 class="product-name">Webcam</h3>
  <p class="product-price">$45.00</p>
  <span class="product-stock">In stock</span>
</article>

(b)
<article class="product-card">
  <h3 class="product-name">Desk Lamp</h3>
  <p class="product-price">$19.99</p>
  <span class="product-stock">Out of stock</span>
</article>

(c)
<article class="product-card">
  <h3 class="product-name">Monitor Stand</h3>
  <p class="product-price">$120.00</p>
  <span class="product-stock">In stock</span>
</article>

Que hayas podido predecirlo sin ejecutar el código es la demostración de la pureza: la salida está completamente determinada por la entrada. Los precios: 4500$45.00, 1999$19.99, 12000$120.00 (centavos entre 100, con dos decimales). El stock: true → "In stock", false → "Out of stock".

Ejercicio 2 — Encuentra la impureza. Este componente rompe la pureza. Explica cómo la rompe y por qué eso es un problema, y reescríbelo para que sea puro:

function ProductCard(props) {
  const discount = Math.random() < 0.5 ? 0 : 500; // "oferta sorpresa"
  return h('article', { className: 'product-card' },
    h('h3', {}, props.name),
    h('p', {}, formatPrice(props.priceCents - discount))
  );
}
Ver solución

Cómo rompe la pureza: usa Math.random(), que devuelve un número distinto cada vez. Por eso el precio mostrado cambia entre llamadas aunque las props sean idénticas: a veces resta 500, a veces no. La salida ya no depende solo de la entrada; depende también del azar. Es la máquina expendedora rota: aprietas el mismo botón y a veces cae una cosa, a veces otra.

Por qué es un problema: React puede re-ejecutar el componente cuando quiera; con esta impureza, el precio de un producto parpadearía entre dos valores sin que nadie cambiara nada. Además, no podrías razonar sobre qué muestra la tarjeta sin ejecutarla, y las pruebas serían imposibles (¿qué esperas si el resultado es aleatorio?).

Versión pura: el descuento debe entrar por las props, no calcularse con azar adentro:

function ProductCard(props) {
  return h('article', { className: 'product-card' },
    h('h3', {}, props.name),
    h('p', {}, formatPrice(props.priceCents - (props.discountCents || 0)))
  );
}

Ahora la decisión de si hay descuento (y de cuánto) se toma afuera y se pasa como props.discountCents. La función vuelve a ser determinista: mismas props → mismo precio, siempre. Lo variable se calcula fuera y se inyecta dentro.

Ejercicio 3 — Argumenta la pureza. Un compañero dice: "no entiendo por qué tanto lío con la pureza; mi componente funciona igual aunque use Date.now() adentro". Explícale, con un ejemplo concreto de Mercado, qué problema le va a aparecer y por qué la pureza lo evita.

Ver solución

El problema aparecerá en cuanto React re-ejecute el componente, cosa que hace todo el tiempo. Ejemplo concreto: supón un ProductCard que muestra "Publicado hace X minutos" calculando X con Date.now() adentro. Cada vez que React re-renderice esa tarjeta —porque cambió algo en otra parte de la pantalla, por ejemplo el carrito—, el "hace X minutos" saltará a un valor nuevo, aunque el producto no haya cambiado en absoluto. El usuario vería el texto cambiar solo, de forma errática, sin ninguna acción de su parte. Peor: dos tarjetas idénticas podrían mostrar tiempos distintos según el instante exacto en que React ejecutó cada una.

La pureza lo evita porque obliga a que todo lo que el componente muestra dependa solo de sus props. Si el tiempo de referencia entra como prop (props.now), entonces, dado un props.now fijo, la tarjeta muestra siempre lo mismo por más veces que React la re-ejecute. Lo que varía (el reloj) se calcula en un solo lugar controlado, afuera, y se pasa hacia adentro. Así el componente sigue siendo predecible sin importar cuándo ni cuántas veces React decida llamarlo. "Funciona igual" solo mientras nadie re-renderice; en una app real, algo re-renderiza constantemente, y ahí la impureza se vuelve un bug intermitente muy difícil de cazar.

Resumen y siguiente paso

En esta lección viste, ejecutada, la tesis del módulo: UI = f(props) —la UI es lo que un componente devuelve para unas props dadas—. Corriste el mismo ProductCard con tres productos y observaste la salida seguir a la entrada campo por campo: el nombre que se copia, el precio que se transforma con formatPrice, el stock que se ramifica con un ? :. Y comprobaste el determinismo (mismas props → mismo HTML, a === b dio true), la marca de una función pura: salida que depende solo de la entrada, sin efectos secundarios. Entendiste por qué React lo exige —puede re-ejecutar componentes cuando quiera— y qué ganas tú: poder razonar sobre un componente leyéndolo, sin correrlo.

Antes de avanzar deberías poder: explicar UI = f(props) con la máquina expendedora; predecir la salida de un componente puro para unas props sin ejecutarlo; nombrar las dos condiciones de una función pura (salida depende solo de la entrada; sin efectos secundarios); y detectar y corregir una impureza (azar, fecha, mutación de props).

La lección 4 da el siguiente paso natural: si un componente es una función pura que produce una porción de UI, ¿cómo se decide qué porciones hacer? Vas a aprender a descomponer una pantalla —el storefront de Mercado— en componentes: mirar el diseño, identificar responsabilidades y repeticiones, trazar los límites, y dibujar el árbol. Pasaremos de "una tarjeta" a "toda la tienda, partida en piezas".

Recursos