Ir al contenido
wannacode

El primer componente

En resumen: Un componente de React es una función que devuelve JSX: describe cómo se ve la pantalla según los datos, y React cambia en la página solo la diferencia.

Problema

El campo pierde el foco

El módulo «JavaScript en la página» terminó con un carrito dibujado a partir de los datos por un único render(). Aquí cada cambio del campo de cantidad llama a render(). El cliente quiere 15 botellas de leche: hace clic en el campo, borra el 2 y escribe 15. Pruébalo.

HTML, CSS, JS

Tras borrar, el campo está vacío y ya no tiene el foco: el 1 y el 5 no van a ninguna parte. render() sustituyó el campo por uno nuevo con la primera tecla. El campo nuevo se ve igual, pero es otro elemento, y el foco estaba en el viejo. Tocar a mano cada lugar por separado hace que se olviden lugares. Dibujarlo todo de nuevo hace perder el foco. Hay un tercer camino.

La idea

La interfaz como función de los datos

render() tenía una buena idea: la pantalla se construye solo a partir de los datos. Con los mismos datos la pantalla es la misma, sea lo que sea lo que hubiera antes. El error estaba en el último paso: render() quita y crea los elementos él mismo.

Por eso el trabajo se divide en dos:

  • Tú escribes una función que dice cómo se ve la pantalla para esos datos. No toca la página; devuelve una descripción: aquí hay un <main>, dentro un <h1> con el texto «El carrito», luego un campo con el valor 2.
  • La biblioteca guarda el DOM real. La primera vez lo construye a partir de la descripción. En cada cambio de los datos toma la descripción nueva, la compara con la anterior y cambia en la página solo la diferencia.

Una función así se llama componente. En corto: la pantalla es una función de los datos, y el componente es esa función. React y Vue lo hacen de formas distintas. La idea es la misma, y elegir entre ellos es cosa del equipo, no de una biblioteca «mejor».

Míralo

El primer componente

En React un componente es una función que devuelve JSX: una notación que parece HTML pero vive dentro de JavaScript.

const product = { name: 'Leche fresca', price: 1.6, stock: 35 };
export default function App() {
return (
<main>
<h1>{product.name}</h1>
<p>Precio: {money(product.price)}</p>
</main>
);
}
  • function App() es una función normal. El nombre empieza con mayúscula: así distingue React un componente de una etiqueta HTML.
  • return ( … ) devuelve la descripción. Los paréntesis solo sirven para que JSX pueda empezar en una línea nueva.
  • {product.name} es una expresión de JavaScript entre llaves. Su resultado se convierte en texto en la página.
  • export default permite que otro archivo importe el componente. El sandbox pone App en la página por sí solo. En un proyecto real lo hace una línea en main.jsx: createRoot(...).render(<App />).
React

Cambia el precio o el stock en el objeto product y pulsa «Ejecutar». No tocas la descripción de la pantalla: la pantalla sigue a los datos.

La idea

Las reglas de JSX

JSX parece HTML, pero no es HTML: antes de ejecutarse, cada etiqueta se convierte en una llamada a una función de JavaScript. Por eso tiene reglas propias.

  • Una sola raíz. return devuelve un elemento. Si varios tienen que ir juntos, envuélvelos en un fragmento <>…</>: no crea ningún elemento en la página.
  • className en lugar de class, htmlFor en lugar de for. class y for son palabras de JavaScript. Con class la página sigue funcionando, pero React escribe un error en la consola: «Invalid DOM property class. Did you mean className?».
  • {expresión} en el texto y en los atributos: <img src={product.image} alt={product.name} />. Sin comillas alrededor de las llaves: alt="{product.name}" es el texto {product.name}, letra por letra.
  • Todas las etiquetas se cierran, también las que no tienen contenido: <input />, <img />, <br />.
  • Los eventos se escriben en camelCase y reciben una función: onClick={add}. No onClick={add()}: eso llama a add enseguida, mientras se dibuja el componente, y le pasa a onClick el resultado.

Pruébalo

Encuentra el error

Un clic en «Añadir» no escribe nada en la consola.

Elige la línea con el error.

Pruébalo

Completa

El lector de pantalla tiene que leer el nombre del producto como descripción de la imagen.

<img src={product.image} alt= />

La idea

Componentes hechos de componentes

La página de la tienda tiene partes: la parte de arriba con el menú, la tarjeta del producto, el carrito. Cada parte se convierte en un componente en su propio archivo, y un componente mayor los usa como etiquetas. Así App no es una función larga, sino unas pocas líneas que dicen qué hay en la página.

ShopHeader.jsx
export default function ShopHeader() {
return (
<header>
<a href="/">La tienda</a>
<nav>…</nav>
</header>
);
}
App.jsx
import ShopHeader from './ShopHeader';
import ProductCard from './ProductCard';
export default function App() {
return (
<>
<ShopHeader />
<main>
<ProductCard />
</main>
</>
);
}

El nombre del componente empieza con mayúscula, y no es cuestión de gusto. Para React <ShopHeader /> es un componente, y <shopHeader /> es una etiqueta HTML <shopheader> que el navegador no conoce. Entonces hay un error en la consola y falta la parte de arriba. El fragmento <>…</> está porque ShopHeader y <main> van uno junto al otro.

Pruébalo

Monta la página

ShopHeader y ProductCard están listos en sus archivos. Monta App: ShopHeader arriba, y la tarjeta del producto en main, bajo el título «Lácteos».

React

La idea

Solo la diferencia

Aquí está el carrito del problema como componente.

useState('2') es la memoria del componente: un valor que React conserva entre dos dibujos, y una función (setMilk) que lo cambia y le dice a React que dibuje el componente otra vez. Es el tema del módulo «Estado». Aquí solo está para que algo pueda cambiar.

React

Haz lo mismo que en el problema: clic en el campo de leche, borra el 2 y escribe 15. El campo conserva el foco y lo que escribiste, y el total pasa a 25,20 €. El componente describe toda la pantalla con cada tecla, igual que render(). La diferencia es que la página no se construye de nuevo. La biblioteca ve que en la descripción nueva el campo está en el mismo lugar con la misma etiqueta, y cambia solo su valor y el texto del total.

Míralo

Mira la comparación

import { useState } from 'react';
const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
export default function App() {
const [milk, setMilk] = useState('2');
const [bread, setBread] = useState('1');
const total = Number(milk) * 1.6 + Number(bread) * 1.2;
return (
<main>
<h1>El carrito</h1>
<label>
Leche fresca
<input type="number" value={milk} onChange={(e) => setMilk(e.target.value)} />
</label>
<label>
Pan blanco
<input type="number" value={bread} onChange={(e) => setBread(e.target.value)} />
</label>
<p>Total: {money(total)}</p>
</main>
);
}
Paso a paso, en palabras
  1. El cliente ha borrado el 2 y ha escrito 1: los datos dicen milk = "1". En el centro está la última descripción que devolvió el componente; a la derecha está el DOM, con el foco en el campo de leche.
  2. El cliente escribe 5. onChange llama a setMilk("15"): los datos cambian, y React anota que hay que volver a dibujar App.
  3. React vuelve a llamar a la función App. No toca la página: devuelve una descripción nueva, un árbol de objetos simples. Construirla es barato.
  4. La biblioteca compara la descripción nueva con la anterior. Las mismas etiquetas en el mismo orden y dos diferencias: el valor del campo y el texto del total.
  5. Solo las dos diferencias llegan al DOM. El <input> es el mismo elemento de antes, por eso el foco y el cursor se quedan en él.
  6. Tú describes toda la pantalla para los datos dados, como con render(). La biblioteca encuentra qué cambió y toca solo eso. Las dos bibliotecas lo hacen, cada una a su manera.

La descripción es barata: es un árbol de objetos simples, y el componente lo rehace cada vez. El DOM es la parte cara, así que la biblioteca solo toca ahí las diferencias. Tú escribes como con render(): toda la pantalla a partir de los datos. El precio de render() (el foco perdido, miles de elementos nuevos) no se paga.

Más precisamente, para curiosos

DOM virtual. A la descripción se la suele llamar DOM virtual, y a su comparación con la anterior, reconciliación (React) o patching (Vue). Un nodo de la descripción es un objeto como { type: 'h1', props: { children: 'El carrito' } }.

Los dos caminos. JSX se compila a llamadas como jsx('h1', { children: product.name }). Cuando cambia la memoria de un componente, React vuelve a llamar a su función, y a las de los componentes que contiene, y luego compara. En los dos casos al final hay una descripción nueva, una comparación y unos pocos cambios en el DOM.

El texto es texto. {product.name} inserta texto, como textContent. Un nombre como <img src=x onerror=alert(1)> se muestra como caracteres y no se convierte en una etiqueta. El HTML de una cadena solo entra con dangerouslySetInnerHTML, y el nombre ya avisa de que hay que tener cuidado.

Componentes de clase. En código antiguo un componente es una clase: class Cart extends React.Component { render() { … } }. Sigue funcionando, pero el código nuevo usa funciones y hooks (useState es un hook).

StrictMode. Un proyecto nuevo de Vite envuelve App en <StrictMode>. En modo de desarrollo llama a cada componente dos veces, para sacar a la luz el código que toca algo fuera de sí mismo. Por eso un console.log en el cuerpo de un componente puede aparecer dos veces. El sandbox dibuja App sin StrictMode.

Ejercicio

La tarjeta del producto

En ProductCard está el objeto product (Queso blanco). Haz la tarjeta: el nombre como título de nivel dos, «Precio: 7,90 €» (con money), «En stock: 12 uds.» y un botón «Añadir» cuyo nombre para el lector de pantalla sea «Añadir Queso blanco». Un clic en el botón escribe «Añadido: Queso blanco» en la consola (la función add ya está hecha). Después monta App: ShopHeader arriba, y la tarjeta en main, bajo el título «Lácteos».

React

Test

Ponte a prueba

1. Un componente se dibuja por segunda vez con los mismos datos. ¿Qué muestra?
2. ¿Qué línea es JSX válido?
3. ¿Por qué el foco se queda en el campo dentro del componente, mientras que render() del módulo anterior lo pierde?
4. El archivo tiene function productCard(), y App usa <productCard />. ¿Qué pasa?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·