Módulo 1: Thinking In Components

Qué es un componente

Descripción

Ya sabes a dónde vamos: construir la UI como una función de los datos. Ahora empezamos a construirlo, y el primer ladrillo es la pieza misma. Esta lección responde una pregunta que parece obvia y no lo es: ¿qué es, exactamente, un componente?

La respuesta es de una simpleza casi decepcionante, y por eso hay que tomarla en serio: un componente es una función. Nada más. Una función que recibe un objeto de datos —las props— y devuelve una descripción de UI. No es una clase mágica, no es una plantilla especial, no es "HTML con superpoderes": es una función normal de JavaScript, con una entrada y una salida, que puedes llamar, probar y razonar como cualquier otra.

Lo único que la hace un componente —y no una función cualquiera— es la forma de su entrada y su salida. La entrada son las props: un objeto con los datos que el componente necesita para hacer su trabajo (para ProductCard, el nombre, el precio y si hay stock). La salida es UI: no HTML directamente, sino una descripción de UI —un árbol de objetos— que algo más (React, o nuestro renderToString) convertirá en lo que se ve. Que la salida sea una descripción y no una manipulación de la pantalla es la propiedad que lo cambia todo, y en esta lección la vas a ver, literal, en la consola.

Conexión con el módulo. Esta es la definición sobre la que se apoya el módulo entero. Si un componente es una función, entonces UI = f(props) (lección 3) es solo decir que la UI es lo que esa función devuelve; componer (lección 5) es llamar una función dentro de otra; las props que fluyen hacia abajo (lección 6) son los argumentos que un componente le pasa a las funciones-hijas que llama; y descomponer (lección 7) es partir una función grande en funciones pequeñas. Toda la guía se para sobre esta única idea: componente = función. Si la interiorizas bien aquí, el resto encaja solo.

Una analogía: la receta y sus ingredientes

Piensa en una receta de cocina. Una receta no es el plato; es una descripción de cómo se hace el plato. Tiene dos cosas: una lista de ingredientes (la entrada) y unas instrucciones que, seguidas, producen el resultado (la salida). La misma receta de "pan" con harina de trigo produce un pan; con harina integral produce otro; con más agua, uno más húmedo. La receta no cambia: cambian los ingredientes, y con ellos el resultado.

Un componente es exactamente una receta. Los ingredientes son las props —los datos que le das—. La receta es la función —lo que escribes una vez—. Y el plato es la UI —lo que la función produce cuando la "cocinas" con esos ingredientes—.

Ahora, un detalle crucial de la analogía, el que separa a un buen cocinero de un desastre. La receta describe el plato; no lo sirve en tu mesa. Escribir "hornea 40 minutos a 180°" no hace que aparezca pan caliente frente a ti: es una descripción que alguien (el horno, el cocinero) ejecuta. Del mismo modo, un componente describe la UI; no la pinta en la pantalla. Devuelve la "receta" de cómo se ve, y React (o nuestro renderToString) es el "horno" que la convierte en algo real. El componente que intenta pintar la pantalla directamente es como una receta que, en lugar de decir "hornea", saliera de la cocina a servirte el plato: mezcla dos trabajos que deben estar separados.

Y como toda receta, un componente es determinista: los mismos ingredientes, siguiendo los mismos pasos, dan el mismo plato. Mismas props, misma UI. Esa fiabilidad es lo que hace que puedas razonar sobre un componente sin sorpresas, y es el tema de la lección 3.

Ejemplo trabajado: ProductCard, de árbol a HTML

Vamos a ver, ejecutado, cómo un componente devuelve datos (un árbol) y cómo esos datos se vuelven HTML. Es el paso que hace concreta la frase "un componente devuelve una descripción, no HTML".

Primero, el componente tal como lo escribirás en React. Esto es JSX, la API real (módulo 2 lo profundiza):

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>
  );
}

Fíjate en la convención de nombres: ProductCard empieza con mayúscula (PascalCase). No es un capricho: React distingue componentes de etiquetas HTML por esa mayúscula. <article> (minúscula) es una etiqueta HTML; <ProductCard> (mayúscula) es tu componente. Si nombraras tu componente productCard (minúscula), React lo confundiría con una etiqueta HTML desconocida. Componentes en PascalCase, siempre.

Ahora la versión ejecutable en Node. Recuerda el truco del módulo: el JSX compila a un árbol de objetos, que aquí escribimos con la función mínima h, y renderToString lo convierte en HTML. La lógica del componente es idéntica a la del JSX de arriba:

// h(): construye el arbol de la UI. JSX <div className="x">..</div>
// compila a exactamente esto: h('div', { className: 'x' }, ...)
function h(tag, props, ...children) {
  return { tag, props: props || {}, children: children.flat() };
}

// renderToString(): recorre el arbol y produce el HTML como string.
function renderToString(node, indent = 0) {
  const pad = '  '.repeat(indent);
  if (node == null || typeof node === 'boolean') return '';
  if (typeof node !== 'object') return pad + node; // texto
  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);
}

// ProductCard: una funcion que recibe props y devuelve UI (un arbol).
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')
  );
}

const mouse = { id: 'p1', name: 'Wireless Mouse', priceCents: 2599, category: 'accessories', inStock: true };

console.log('--- el arbol que devuelve ProductCard (es datos, no HTML) ---');
console.log(JSON.stringify(ProductCard(mouse), null, 2));
console.log('\n--- ese mismo arbol, renderizado a HTML ---');
console.log(renderToString(ProductCard(mouse)));

Dediquemos un segundo a renderToString, porque es el motor de todo el módulo y conviene que no sea una caja negra. Recibe un nodo y lo convierte en texto siguiendo reglas simples: si el nodo es null o un booleano, no produce nada (sirve para lo condicional, que veremos); si es texto o número, lo devuelve tal cual; si es un objeto (un elemento), arma sus atributos —traduciendo className a class, el famoso detalle de JSX— y luego se llama a sí mismo para cada hijo (por eso es recursivo: un árbol se recorre recursivamente). El indent solo sirve para que la salida quede legible con sangría. No es React —React hace muchísimo más—, pero captura la idea esencial: un árbol de objetos entra, un string de HTML sale.

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

--- el arbol que devuelve ProductCard (es datos, no HTML) ---
{
  "tag": "article",
  "props": {
    "className": "product-card"
  },
  "children": [
    {
      "tag": "h3",
      "props": {
        "className": "product-name"
      },
      "children": [
        "Wireless Mouse"
      ]
    },
    {
      "tag": "p",
      "props": {
        "className": "product-price"
      },
      "children": [
        "$25.99"
      ]
    },
    {
      "tag": "span",
      "props": {
        "className": "product-stock"
      },
      "children": [
        "In stock"
      ]
    }
  ]
}

--- ese mismo arbol, renderizado a HTML ---
<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>

Esta salida es la lección entera, así que léela con cuidado.

La primera mitad es lo que ProductCard(mouse) devuelve: un objeto de JavaScript. No es HTML, no es una pantalla, no es nada visible todavía. Es puro dato: un tag ("article"), unos props ({ className: "product-card" }), y una lista de children, cada uno a su vez un objeto con su tag, sus props y sus children. Es un árbol: el article tiene tres hijos (h3, p, span), y cada uno tiene un hijo de texto ("Wireless Mouse", "$25.99", "In stock"). Cuando un componente "devuelve UI", esto es lo que devuelve: una descripción de la UI como estructura de datos. En React de verdad se le llama el árbol de elementos (o, informalmente, el DOM virtual), y aunque su forma exacta es distinta, la idea es idéntica: la UI, antes de ser pintada, es datos que puedes inspeccionar.

La segunda mitad es ese mismo árbol después de pasar por renderToString: ahora sí, el HTML como texto. Fíjate en las correspondencias exactas: el objeto con tag: "article" se volvió <article>; su props.className se volvió class="product-card" (la traducción classNameclass); sus tres hijos se volvieron las tres líneas anidadas; los hijos de texto se volvieron el contenido. Nada se inventó ni se perdió: renderToString solo recorrió el árbol de la primera mitad y lo escribió como HTML.

El puente entre las dos mitades es la frase clave del módulo: el componente produce la descripción (primera mitad); el renderizador produce lo visible (segunda mitad). En el navegador, tú escribes componentes que devuelven la primera mitad, y React hace el trabajo de la segunda —solo que, en vez de un string, pinta el DOM real—. Que estas dos responsabilidades estén separadas es lo que te permite escribir UI declarativa sin tocar nunca la pantalla a mano.

Un componente es "solo una función": la prueba

Para que la definición no quede como un eslogan, hagamos la prueba de que ProductCard es de verdad una función normal. Una función normal:

  • se llama con argumentos y devuelve un valor: ProductCard(mouse) devolvió el árbol. Lo llamamos como cualquier función.
  • su valor de retorno se puede guardar e inspeccionar: guardamos el árbol y lo imprimimos con JSON.stringify. Es un dato como cualquier otro.
  • no tiene efectos ocultos: llamar a ProductCard(mouse) no cambió nada en el mundo —no tocó una pantalla, no escribió un archivo—; solo calculó y devolvió un valor.

Esa última propiedad tiene un nombre —función pura— y es tan importante que la lección 3 la dedica entera. Por ahora quédate con esto: no hay magia. ProductCard es una función que, dado un producto, calcula y devuelve cómo se ve su tarjeta. Puedes razonar sobre ella exactamente como sobre formatPrice: entra algo, sale algo, sin sorpresas.

Errores comunes

Nombrar el componente en minúscula. Qué pasa: se define function productCard(props) o function search_bar(props) y luego React no lo reconoce como componente. Por qué pasa: en JavaScript normal los nombres de función suelen ir en camelCase, y uno arrastra la costumbre. Cómo detectarlo: al usar <productCard />, React lo trata como una etiqueta HTML desconocida (en minúscula) en vez de como tu componente, y no se renderiza lo que esperas. Cómo corregirlo: los componentes van en PascalCase, siempre (ProductCard, SearchBar, CartItem). React usa la mayúscula inicial para distinguir "esto es un componente mío" de "esto es una etiqueta HTML estándar". Es una regla dura de React, no una preferencia de estilo.

Creer que el componente devuelve HTML. Qué pasa: se piensa que ProductCard devuelve el string "<article>...</article>". Por qué pasa: el JSX se parece a HTML y la salida final es HTML, así que uno salta el paso intermedio. Cómo detectarlo: te sorprende ver, en la primera mitad de la salida, un objeto en vez de un string; o intentas hacer operaciones de string (.replace, .includes) sobre lo que devuelve un componente. Cómo corregirlo: interioriza que un componente devuelve un árbol de objetos —una descripción—, no HTML. El HTML aparece después, cuando el renderizador recorre ese árbol. Confundir las dos cosas te lleva a intentar manipular strings donde deberías componer objetos.

Poner efectos dentro del cuerpo del componente. Qué pasa: dentro de ProductCard alguien mete un console.log, una escritura a localStorage, o peor, una manipulación del DOM. Por qué pasa: la costumbre imperativa de "hacer cosas" mientras se construye la pantalla. Cómo detectarlo: el cuerpo del componente, además de construir y devolver el árbol, hace algo que se nota afuera (imprime, guarda, modifica). Cómo corregirlo: el cuerpo de un componente debe solo calcular y devolver la descripción de la UI a partir de las props; nada de efectos secundarios. Los efectos —sincronizar con sistemas externos— tienen su lugar propio (useEffect, módulo 6) y una razón para estar ahí. Un componente que "hace cosas" al renderizar deja de ser una función pura y pierde la predecibilidad que lo hace útil.

Ejercicios

Ejercicio 1 — Entrada y salida. Para el componente ProductCard del ejemplo, identifica con precisión: (a) cuál es su entrada y de qué tipo es; (b) cuál es su salida y de qué tipo es; (c) qué relación hay entre la primera mitad y la segunda mitad de la salida en consola.

Ver solución
  • (a) La entrada son las props: un objeto de JavaScript con los datos del producto ({ id, name, priceCents, category, inStock }). En el ejemplo, mouse. ProductCard recibe un solo argumento, ese objeto.
  • (b) La salida es un árbol de objetos que describe la UI: un objeto con tag, props y children, donde cada hijo puede ser a su vez otro objeto (un elemento) o un string (texto). No es HTML ni un string; es una estructura de datos.
  • (c) La relación es que la segunda mitad es la primera mitad renderizada. La primera es lo que ProductCard(mouse) devuelve (el árbol de datos); la segunda es ese mismo árbol después de pasar por renderToString, que lo recorre y lo escribe como HTML. El componente produce la primera; el renderizador produce la segunda. Nada se agrega ni se pierde entre una y otra: es la misma información en dos formas.

Ejercicio 2 — Predice el árbol. Sin correr nada, escribe qué árbol de objetos (aproximado, con tag/props/children) devolvería este componente CartItem para las props { name: 'USB-C Hub', quantity: 2 }:

function CartItem(props) {
  return (
    <li className="cart-item">
      <span className="cart-item-name">{props.name}</span>
      <span className="cart-item-qty">{'x' + props.quantity}</span>
    </li>
  );
}
Ver solución

El árbol sería un li con dos hijos span:

{
  tag: "li",
  props: { className: "cart-item" },
  children: [
    { tag: "span", props: { className: "cart-item-name" }, children: ["USB-C Hub"] },
    { tag: "span", props: { className: "cart-item-qty" }, children: ["x2"] }
  ]
}

Y su HTML renderizado sería:

<li class="cart-item">
  <span class="cart-item-name">USB-C Hub</span>
  <span class="cart-item-qty">x2</span>
</li>

Lo clave: {props.name} se resolvió a "USB-C Hub", y {'x' + props.quantity} se resolvió a "x2" (concatenación de string con número). Las llaves del JSX contienen JavaScript que se evalúa al construir el árbol; lo que queda en el árbol es el resultado de esa evaluación, no la expresión.

Ejercicio 3 — ¿Es un componente válido? Para cada función, di si es un componente válido según la definición (una función que recibe props y devuelve una descripción de UI) y, si no lo es, explica por qué:

// (a)
function priceTag(props) {
  return h('span', {}, formatPrice(props.priceCents));
}

// (b)
function StockLabel(props) {
  document.getElementById('stock').textContent = 'In stock';
  return h('span', {}, 'In stock');
}

// (c)
function EmptyState() {
  return h('p', {}, 'No products found');
}
Ver solución
  • (a) priceTag — no del todo válido, por el nombre. Su cuerpo está bien (recibe props, devuelve una descripción de UI), pero el nombre está en minúscula. Como componente de React fallaría, porque React lo confundiría con una etiqueta HTML. Corrección: renómbralo PriceTag (PascalCase). Como función pura es correcta; como componente necesita la mayúscula.
  • (b) StockLabel — inválido, por el efecto secundario. Además de devolver la descripción, manipula el DOM (document.getElementById(...).textContent = ...). Un componente no debe tocar la pantalla ni tener efectos secundarios en su cuerpo; debe solo calcular y devolver. Corrección: elimina la línea del document; el return ya describe lo que se ve, y React se encarga de pintarlo.
  • (c) EmptyState — válido. Está en PascalCase, no tiene efectos secundarios, y devuelve una descripción de UI. Que no reciba props no es un problema: un componente puede no necesitar datos de entrada (aquí siempre muestra el mismo mensaje). Recibir props es opcional; no tener efectos y devolver UI, no.

Resumen y siguiente paso

En esta lección clavaste la definición sobre la que se para el módulo entero: un componente es una función que recibe props (un objeto de datos) y devuelve UI (un árbol de objetos que describe lo que se ve). Lo ejecutaste y viste, literal, las dos mitades: primero el árbol de datos que ProductCard(mouse) devuelve —la UI es datos, inspeccionables con JSON.stringify—, y luego ese mismo árbol convertido en HTML por renderToString. Entendiste que el componente describe y el renderizador pinta, que esa separación es lo que te libera de manipular la pantalla, y las reglas duras: componentes en PascalCase y sin efectos secundarios en su cuerpo.

Antes de avanzar deberías poder: definir un componente en una frase (función: props entran, UI sale); explicar por qué su salida es un árbol de objetos y no HTML; nombrar componentes en PascalCase y decir por qué; y detectar un componente inválido (nombre en minúscula, o efecto secundario en el cuerpo).

La lección 3 toma esta función y la observa en movimiento: si un componente es una función, entonces la UI es lo que esa función devuelve para unas props dadas —UI = f(props)—. Vas a ejecutar el mismo ProductCard con tres productos distintos y ver la UI cambiar campo por campo, y a comprobar que un componente es una función pura: mismas props, misma UI, siempre, sin sorpresas. Es la propiedad que hace que React sea predecible, y la vas a medir.

Recursos