Módulo 2: Jsx And Rendering
Atributos y palabras reservadas: `className`, `htmlFor`
Descripción
Ya sabes que JSX compila a llamadas a función y que dentro de las llaves va una expresión. Esta lección aterriza esa tesis en la parte que a todos confunde al principio: los atributos. ¿Por qué en React se escribe className y no class? ¿Por qué htmlFor y no for? ¿Por qué onClick con mayúscula intermedia? Parecen tres reglas sueltas para memorizar. No lo son: son una sola consecuencia de la tesis del módulo. Como el JSX es JavaScript, los atributos terminan siendo llaves de un objeto de JavaScript (el segundo argumento de createElement), y ahí no pueden usarse las palabras reservadas del lenguaje —class y for lo son— ni convenciones que choquen con cómo JavaScript nombra las cosas.
Vamos a ver cuatro cosas sobre los atributos, todas ejecutadas sobre el SearchBar de Mercado: (1) por qué className traduce a class y htmlFor a for, y quién hace esa traducción; (2) el camelCase de los atributos de varias palabras (onClick, tabIndex); (3) los atributos cuyo valor es una expresión (value={query}) en vez de un string literal; y (4) los atributos booleanos (disabled), que aparecen o no según un valor true/false.
Conexión con el módulo. Los atributos son el segundo argumento de cada createElement, así que están en cada elemento que renderizas —los viste ya en el className del ProductCard—. Esta lección cierra la parte "estática" de la sintaxis: después de aquí sabes escribir cualquier etiqueta con sus atributos correctamente. Los atributos cuyo valor es una expresión (value={query}, disabled={!inStock}) son además el puente hacia lo que viene: en el módulo 3 esa expresión será una variable de estado, y en el módulo 4 el onClick recibirá un handler. Aquí solo el marcado; la vida llega después.
Una analogía: el formulario oficial con nombres de campo fijos
Piensa en un formulario oficial —el de una aduana, el de un banco—. Cada casilla tiene un nombre de campo exacto que el sistema espera: "Apellido paterno", "Fecha de nacimiento (DD/MM/AAAA)". No puedes inventar el nombre ni usar un sinónimo: si el sistema espera date_of_birth y tú escribes birthday, el campo no se llena. Los nombres están fijados por el sistema que va a leer el formulario, no por ti.
Los atributos de JSX son así, con un giro. El sistema que los lee es JavaScript (porque el JSX se vuelve un objeto de JavaScript), y JavaScript ya tiene unos nombres reservados para su propio uso —class (para declarar clases), for (para los bucles)—. Si el "formulario" (el objeto de props) usara esos nombres, chocaría con el idioma en el que está escrito. Así que React fijó nombres que no chocan: donde el HTML dice class, el objeto de JavaScript dice className; donde el HTML dice for, dice htmlFor. No es que React sea caprichoso: es que el formulario está escrito en JavaScript, y tiene que respetar las palabras que JavaScript se reserva.
Y hay un traductor en la aduana: cuando el formulario finalmente se convierte en HTML real, alguien traduce los nombres de vuelta —className → class, htmlFor → for—. Ese traductor es React (en nuestro Node, renderToString). Tú escribes los nombres de JavaScript; el HTML final lleva los nombres de HTML. Guarda la imagen: nombres fijados por el idioma en que se lee el formulario, y un traductor a la salida.
Por qué class y for no se pueden usar
Vale la pena ser preciso, porque entenderlo de raíz evita memorizar. En JavaScript, palabra reservada es una palabra que el lenguaje aparta para su propia sintaxis y que no puedes usar libremente como identificador en ciertos contextos. class es reservada: sirve para declarar clases (class ProductCard { ... }). for es reservada: sirve para los bucles (for (let i = 0; ...)).
Ahora, recuerda a qué compila el JSX. <label className="x" htmlFor="y"> se vuelve:
React.createElement('label', { className: 'x', htmlFor: 'y' }, ...);
El segundo argumento es un objeto de JavaScript, y sus llaves son identificadores. Históricamente, usar una palabra reservada como class en ese lugar era problemático, y para evitar cualquier fricción con el lenguaje, React adoptó las variantes que el propio DOM ya usa en JavaScript: la propiedad de un elemento en el DOM para su clase CSS es element.className (no element.class), y la del for de un <label> es element.htmlFor. React simplemente sigue esa convención del DOM: los atributos de JSX se nombran como las propiedades del DOM en JavaScript, no como los atributos en el HTML. Por eso class→className, for→htmlFor, y también tabindex→tabIndex, readonly→readOnly, maxlength→maxLength: la propiedad del DOM va en camelCase.
Esa es toda la regla, y explica de golpe las tres "rarezas": className y htmlFor porque class y for son reservadas; el camelCase porque así se llaman las propiedades del DOM en JavaScript. Una causa, tres consecuencias.
Ejemplo trabajado: el SearchBar y el botón de agregar
Vamos a ejecutar los cuatro puntos sobre dos piezas del storefront: el SearchBar (que tiene un <label> con htmlFor y un <input>) y un AddButton (que tiene un disabled booleano).
Primero, en JSX real —la API que escribirás—:
function SearchBar(props) {
return (
<div className="search-bar">
<label htmlFor="product-search">Search products</label>
<input
id="product-search"
type="search"
className="search-input"
placeholder={props.placeholder}
value={props.query}
/>
</div>
);
}
function AddButton(props) {
return (
<button className="add-to-cart" type="button" disabled={!props.inStock}>
Add to cart
</button>
);
}
Fíjate en cuatro detalles. (1) El <label htmlFor="product-search"> y el <input id="product-search">: el htmlFor del label apunta al id del input —así el navegador sabe que ese label etiqueta ese campo—. (2) className en el <div> y en el <input>. (3) placeholder={props.placeholder} y value={props.query}: sus valores no son strings literales entre comillas, sino expresiones entre llaves —el valor sale de las props—. (4) disabled={!props.inStock}: un atributo booleano, cuyo valor es true o false.
Ahora la versión ejecutable en Node. El renderToString se extiende con dos cosas nuevas: la traducción de htmlFor→for (agregada al mapa ATTR) y el manejo de atributos booleanos (si el valor es true, aparece el nombre solo; si es false, no aparece):
function h(tag, props, ...children) {
return { tag, props: props || {}, children: children.flat() };
}
const VOID = new Set(['input', 'img', 'br', 'hr']);
// JSX usa nombres de JavaScript; renderToString los traduce a los del HTML.
const ATTR = { className: 'class', htmlFor: 'for' };
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)
.filter(([, v]) => v !== false && v != null) // atributo booleano en false: no aparece
.map(([k, v]) => (v === true ? ` ${ATTR[k] || k}` : ` ${ATTR[k] || k}="${v}"`))
.join('');
if (VOID.has(node.tag)) return `${pad}<${node.tag}${attrs} />`;
const kids = node.children.filter((c) => c != null && c !== false);
if (kids.length <= 1 && kids.every((c) => typeof c !== '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}>`;
}
// SearchBar: label + input. En JSX, class es palabra reservada de JS -> className,
// y for tambien -> htmlFor.
function SearchBar(props) {
return h('div', { className: 'search-bar' },
h('label', { htmlFor: 'product-search' }, 'Search products'),
h('input', {
id: 'product-search',
type: 'search',
className: 'search-input',
placeholder: props.placeholder,
value: props.query,
})
);
}
// AddButton: disabled es un atributo BOOLEANO. disabled={true} aparece; false no.
function AddButton(props) {
return h('button', {
className: 'add-to-cart',
type: 'button',
disabled: !props.inStock,
}, 'Add to cart');
}
const mouse = { name: 'Wireless Mouse', priceCents: 2599, inStock: true };
const keyboard = { name: 'Mechanical Keyboard', priceCents: 8900, inStock: false };
console.log('=== 1) className -> class ; htmlFor -> for ===');
console.log(renderToString(SearchBar({ placeholder: 'Search products...', query: '' })));
console.log('\n=== 2) disabled es booleano: solo aparece cuando es true ===');
console.log('AddButton para un producto EN stock (mouse):');
console.log(renderToString(AddButton(mouse)));
console.log('\nAddButton para un producto AGOTADO (keyboard):');
console.log(renderToString(AddButton(keyboard)));
Qué esperar. Al correr el archivo con Node, la salida es exactamente esta:
=== 1) className -> class ; htmlFor -> for ===
<div class="search-bar">
<label for="product-search">Search products</label>
<input id="product-search" type="search" class="search-input" placeholder="Search products..." value="" />
</div>
=== 2) disabled es booleano: solo aparece cuando es true ===
AddButton para un producto EN stock (mouse):
<button class="add-to-cart" type="button">Add to cart</button>
AddButton para un producto AGOTADO (keyboard):
<button class="add-to-cart" type="button" disabled>Add to cart</button>
Lee las dos partes con atención.
Parte 1: la traducción. En el código escribimos className y htmlFor —los nombres de JavaScript—; en el HTML salieron class y for —los nombres de HTML—. La traducción la hizo renderToString (en React real, React). Es exactamente la aduana de la analogía: tú llenas el formulario con los nombres del idioma en que se lee (JavaScript), y a la salida se traducen a los del HTML. Fíjate también en el <input ... />: es un elemento void (sin contenido ni etiqueta de cierre), y sus valores placeholder="Search products..." y value="" vienen de las expresiones {props.placeholder} y {props.query} —el value salió vacío porque le pasamos query: ''—.
Parte 2: el booleano. El mismo AddButton, dos productos. Para el mouse (inStock: true), disabled es !true = false, y el atributo no aparece en el HTML: el botón queda habilitado. Para el teclado (inStock: false), disabled es !false = true, y aparece como disabled a secas, sin valor. Así funcionan los atributos booleanos en HTML: su presencia es lo que importa, no un valor. En JSX lo controlas con una expresión que da true o false, y React lo pone o lo quita. Un producto agotado, un botón deshabilitado —descrito, no manipulado—.
Profundización: valor literal vs valor como expresión
Un atributo en JSX puede recibir su valor de dos maneras, y la diferencia es la misma tesis del módulo:
<input type="search" /> {/* valor LITERAL: un string entre comillas */}
<input value={props.query} /> {/* valor EXPRESION: JavaScript entre llaves */}
Con comillas, el valor es un string literal fijo: type="search" siempre vale "search". Con llaves, el valor es una expresión de JavaScript que se evalúa: value={props.query} vale lo que valga props.query en ese momento. Es la misma distinción de los huecos del cuerpo del JSX, ahora en los atributos. Y por eso puedes poner cualquier expresión: disabled={!props.inStock}, className={isActive ? 'tab active' : 'tab'}, id={'product-' + props.id}. Todo lo que produce un valor cabe entre las llaves de un atributo.
Un error clásico que nace de aquí: mezclar comillas y llaves, value="{props.query}". Eso no evalúa la expresión: le pasa el string literal "{props.query}", con las llaves y todo, como valor. Si el valor viene de JavaScript, van solo llaves, sin comillas: value={props.query}. Si es un texto fijo, van solo comillas: type="search". Nunca los dos.
Profundización: un par de atributos que sorprenden
Además de className y htmlFor, hay unos cuantos atributos cuyo nombre en JSX no es el que esperarías del HTML, siempre por la misma razón (usan el nombre de la propiedad del DOM, en camelCase):
HTML JSX Por que
────────────── ────────────── ─────────────────────────────────────────
class className class es palabra reservada de JavaScript
for htmlFor for es palabra reservada de JavaScript
tabindex tabIndex la propiedad del DOM va en camelCase
maxlength maxLength idem
readonly readOnly idem
onclick onClick idem (los eventos, a fondo en el modulo 4)
No hay que memorizar la tabla entera: basta con la regla que la genera —los atributos de JSX se nombran como las propiedades del DOM en JavaScript, en camelCase, y las palabras reservadas se cambian—. Cuando dudes de un nombre, la doc de React lo tiene, y el editor te avisa. Lo importante es que ninguno es arbitrario.
Errores comunes
Escribir class o for en JSX. Qué pasa: por costumbre del HTML se escribe <div class="..."> o <label for="...">, y no funciona como se espera (React avisa y usa className/htmlFor). Por qué pasa: años de HTML dejan el reflejo. Cómo detectarlo: React imprime una advertencia ("Invalid DOM property class. Did you mean className?"), o la clase simplemente no se aplica. Cómo corregirlo: usa className y htmlFor. Y no lo memorices como excepción: recuerda la causa —class y for son palabras reservadas de JavaScript, y el objeto de props es JavaScript—. Con la causa clara, no se te olvida.
Mezclar comillas y llaves en un valor de expresión. Qué pasa: se escribe value="{props.query}" esperando que se evalúe, pero llega el string literal "{props.query}". Por qué pasa: se copia el patrón de los atributos con comillas y se le meten llaves adentro. Cómo detectarlo: en la pantalla aparece el texto {props.query} tal cual, con llaves. Cómo corregirlo: si el valor es una expresión, van solo llaves: value={props.query}. Las comillas son para valores literales (type="search"); las llaves, para expresiones. Nunca los dos juntos.
Pasar un string donde va un booleano (o al revés). Qué pasa: se escribe disabled="false" pensando que deshabilita según ese texto. Pero "false" es un string no vacío, que en HTML/JSX cuenta como presencia del atributo: el botón queda deshabilitado, justo lo contrario de lo que se quería. Por qué pasa: se confunde el string "false" con el booleano false. Cómo detectarlo: el atributo booleano se comporta al revés de lo esperado. Cómo corregirlo: para un atributo booleano pasa una expresión booleana entre llaves: disabled={!props.inStock}, no disabled="false". Con true el atributo aparece; con false desaparece; un string cualquiera no es ni uno ni otro.
Ejercicios
Ejercicio 1 — Corrige los atributos. Este JSX viene con reflejos de HTML. Reescríbelo con los nombres correctos de JSX:
<div class="field">
<label for="qty">Quantity</label>
<input id="qty" type="number" maxlength="3" readonly="true" />
</div>
Ver solución
<div className="field">
<label htmlFor="qty">Quantity</label>
<input id="qty" type="number" maxLength={3} readOnly={true} />
</div>
Cambios: class→className, for→htmlFor (palabras reservadas de JavaScript); maxlength→maxLength y readonly→readOnly (camelCase de la propiedad del DOM). Además, maxLength={3} usa una expresión numérica (más claro que el string "3"), y readOnly={true} es la forma correcta de un booleano —no readonly="true", que sería un string—. id y type se quedan igual: no son palabras reservadas ni de varias palabras.
Ejercicio 2 — ¿Aparece o no? Para cada caso, di si el atributo disabled aparece en el HTML final y por qué: (a) <button disabled={true}>; (b) <button disabled={false}>; (c) <button disabled={product.inStock}> con product.inStock === true; (d) <button disabled="false">.
Ver solución
- (a)
disabled={true}— Aparece (<button disabled>). El valor es el booleanotrue; el botón queda deshabilitado. - (b)
disabled={false}— No aparece. El valor es el booleanofalse; React omite el atributo, el botón queda habilitado. - (c)
disabled={product.inStock}coninStock === true— Aparece. La expresión evalúa atrue, así que es como (a). (Ojo: esto deshabilita un producto que está en stock; casi seguro queríasdisabled={!product.inStock}.) - (d)
disabled="false"— Aparece, y ese es el error."false"es un string no vacío; para un atributo booleano, cualquier presencia cuenta, así que el botón queda deshabilitado —lo contrario de lo que sugiere el texto—. Para un booleano hay que pasar la expresión booleana entre llaves, no un string.
La regla: los atributos booleanos se controlan con true/false reales (entre llaves), y el false es lo que los quita. Un string nunca es un booleano.
Ejercicio 3 — Literal o expresión. El SearchBar debe mostrar un placeholder fijo ("Search products...") y un value que viene de las props (props.query). Escribe el <input> en JSX decidiendo, para cada atributo, si va con comillas (literal) o con llaves (expresión), y explica por qué.
Ver solución
<input
id="product-search"
type="search"
className="search-input"
placeholder="Search products..."
value={props.query}
/>
id,type,className,placeholdervan con comillas: son valores literales fijos, no dependen de ningún dato. Elplaceholdersiempre dice lo mismo.valueva con llaves: su valor es una expresión (props.query), que sale de las props y puede cambiar. Con comillas (value="props.query") le pasaríamos el string literal"props.query", no el dato.
La regla que decide: ¿el valor es un texto fijo o sale de JavaScript? Fijo → comillas. De JavaScript → llaves. (En el módulo 3, ese value={props.query} se conectará con el estado, y en el 4 con el evento onChange que lo actualiza.)
Resumen y siguiente paso
En esta lección desarmaste la "regla rara" de los atributos y descubriste que es una sola consecuencia de "JSX es JavaScript": como los atributos son llaves de un objeto de JavaScript, se nombran como las propiedades del DOM —en camelCase— y las palabras reservadas del lenguaje se cambian. De ahí, class→className, for→htmlFor, tabindex→tabIndex. Lo mediste con el SearchBar: escribiste className y htmlFor, y renderToString los tradujo a class y for en el HTML. Viste los valores como expresión (value={props.query}, con llaves y sin comillas) frente a los literales (type="search", con comillas), y los atributos booleanos (disabled), que aparecen con true y desaparecen con false —un botón deshabilitado para un producto agotado, descrito, no manipulado—.
Antes de avanzar deberías poder: explicar por qué className y htmlFor se llaman así; distinguir un valor literal (comillas) de uno de expresión (llaves) y no mezclarlos; y controlar un atributo booleano con una expresión true/false.
La lección 4 usa todo esto para lo primero que hace que una UI se sienta "inteligente": el renderizado condicional. Vas a mostrar u ocultar partes de la interfaz según los datos, con las tres herramientas de siempre —el && para "muéstralo solo si", el ternario ? : para "esto o aquello", y el early return para "si no hay nada, describe el estado vacío"—, aterrizadas en el badge "Sold out" del ProductCard y en el "No products found" del catálogo. Y todo, otra vez, ejecutado en Node.
Recursos
- React, "Writing Markup with JSX" — react.dev/learn/writing-markup-with-jsx. La sección sobre atributos:
camelCase,className, y por qué difieren del HTML. En inglés. - React, "Common components (e.g.
<div>)" — react.dev/reference/react-dom/components/common. La referencia de los atributos que aceptan los elementos del DOM en React, con los nombres exactos (className,htmlFor,tabIndex...). En inglés. - MDN, "class (HTML attribute)" — developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/class. Qué es el atributo
classen HTML, para ver de qué "traduce" elclassNamede React. En inglés. - MDN, "Boolean attributes" — developer.mozilla.org/en-US/docs/Glossary/Boolean/HTML. Cómo funcionan los atributos booleanos (
disabled,checked) en HTML: su presencia es lo que importa. En inglés.