Saltar al contenido
Framework Ecosystems LabsUn contrato, muchos ecosistemas, la misma prueba.

Clase 083 — Propiedades y eventos#

⬅️ 082 · 📚 Parte 6 · 🎓 Clases · 084 ➡️ Parte 6 — La interfaz: del HTML del servidor al componente · Nivel 🟢 introductorio · Pista frontend (Interfaz y componentes) ✅ Clase construida — 8 implementaciones verificadas contra contrato.json.

🎯 Objetivo#

Datos hacia abajo, avisos hacia arriba. La regla que organiza la comunicación entre componentes en las ocho tecnologías, y las cinco formas distintas de implementarla.

Un padre con un valor, un hijo con dos botones. El hijo no cambia el valor: avisa de cuánto quiere cambiarlo, y el padre decide.

📚 Resultados de aprendizaje#

Al terminar podrás:

🧩 La situación#

El hijo tiene los botones. El padre tiene el número. ¿Quién suma?

Parece una pregunta de estilo y no lo es: si el hijo suma, el hijo necesita poder escribir en el estado del padre — y a partir de ahí, cualquier componente de la aplicación puede cambiar cualquier cosa desde cualquier sitio. Es exactamente el problema que los frameworks de interfaz vinieron a resolver.

La respuesta, en las ocho: suma el padre. El hijo solo dice «+1».

🧮 El contrato#

# Petición Qué comprueba
1 GET / el hijo recibe el valor por una propiedad
2 GET /?valor=7 otro valor por la misma propiedad
3 GET /evento?valor=7&paso=3 el manejador del padre produce 10
4 GET /evento?valor=7&paso=-2 y 5: el hijo solo dice cuánto
5 GET /flujo.json qué recibe y qué emite, leído de su archivo

Lo que este contrato no comprueba, y conviene decirlo antes que nada: el clic. Disparar un evento en el navegador necesita un navegador, y eso llega en la clase 128 con pruebas de extremo a extremo.

Lo que sí comprueba es la otra mitad — la que se diseña mal: que el dato baje por una propiedad, que el hijo no la toque, y que el manejador del padre el de verdad, el que está en su archivo produzca el estado nuevo cuando se le llama.

Y esa separación no es un truco para poder verificarlo. Una función que calcula el estado siguiente a partir del actual y de un evento se puede probar sola, sin framework y sin navegador. Es la misma idea que hay detrás de un reductor, y es buena práctica en las ocho.

<!-- generado: fichas -->

📖 Las palabras que esta clase define#

Si alguna de estas no te dice nada todavía, esta es la clase donde se aprende. Las definiciones viven en el glosario, que reúne las del programa entero.

Palabra Qué significa
Propiedad (Prop) Un dato que un componente recibe de quien lo usa. Es de solo lectura desde dentro: modificarla rompe la dirección única del flujo de datos.
Evento El aviso que un componente emite hacia arriba para que quien lo usa decida qué hacer. Es la otra mitad de «hacia abajo datos, hacia arriba eventos»: el hijo no cambia lo que recibe, avisa de que algo pasó.

🧰 Las piezas de esta clase, una por una#

Antes del código: qué es cada framework, qué versión se está usando y qué hace falta para ejecutarlo. Todo lo de esta sección sale de los archivos reales del repositorio —el catálogo, la receta de arranque y el manifiesto de dependencias de cada ecosistema—, así que no puede quedarse desactualizado sin que la validación lo detecte.

Framework Qué es Desde Licencia Quién lo mantiene
React biblioteca de interfaz de JavaScript/TypeScript (JavaScript) 2013 MIT Meta y colaboradores
Vue framework de interfaz de JavaScript/TypeScript (JavaScript) 2014 MIT proyecto independiente
Angular framework de interfaz de TypeScript (TypeScript) 2016 MIT Google y colaboradores
Svelte framework de interfaz de JavaScript/TypeScript (JavaScript) 2016 MIT proyecto independiente
SolidJS biblioteca de interfaz de JavaScript/TypeScript (JavaScript) 2018 MIT proyecto independiente
Lit web-components-library de JavaScript/TypeScript (TypeScript) 2021 BSD-3-Clause Google
Alpine.js dom-library de JavaScript (JavaScript) 2019 MIT proyecto independiente
htmx hypermedia-library de JavaScript (JavaScript) 2020 BSD-2-Clause proyecto independiente

🔧 React#

Impuso la idea de la interfaz como función del estado y el árbol virtual. Es una biblioteca, no un framework: no arranca tu aplicación ni define su ciclo de vida.

Preparar sus dependencias, dentro de su directorio:

pnpm install --silent --ignore-scripts

Arrancarla suelta, sin el verificador:

PORT=3000 node server.mjs

Qué hay dentro de su directorio:

Archivo Qué es
Contador.mjs código JavaScript (módulo ES)
Padre.mjs código JavaScript (módulo ES)
ejecutar.json la receta que usa el verificador: qué hace falta, cómo se prepara y cómo arranca
package.json manifiesto de Node.js: nombre, tipo de módulo y dependencias con su rango de versión
pnpm-lock.yaml archivo de bloqueo: la versión exacta de cada dependencia y de sus dependencias
pnpm-workspace.yaml raíz de instalación propia, y la prohibición de ejecutar scripts al instalar
server.mjs código JavaScript (módulo ES)

🔧 Vue#

Adopción progresiva: sirve como etiqueta en una página existente o como framework completo. Su reactividad fina influyó en toda la generación siguiente.

Preparar sus dependencias, dentro de su directorio:

pnpm install --silent --ignore-scripts

Arrancarla suelta, sin el verificador:

PORT=3000 node server.mjs

Qué hay dentro de su directorio:

Archivo Qué es
Contador.mjs código JavaScript (módulo ES)
Padre.mjs código JavaScript (módulo ES)
ejecutar.json la receta que usa el verificador: qué hace falta, cómo se prepara y cómo arranca
package.json manifiesto de Node.js: nombre, tipo de módulo y dependencias con su rango de versión
pnpm-lock.yaml archivo de bloqueo: la versión exacta de cada dependencia y de sus dependencias
pnpm-workspace.yaml raíz de instalación propia, y la prohibición de ejecutar scripts al instalar
server.mjs código JavaScript (módulo ES)

🔧 Angular#

Framework completo con contenedor de dependencias, enrutado, formularios y herramientas en la caja. La opinión arquitectónica más fuerte del ecosistema JavaScript.

Preparar sus dependencias, dentro de su directorio:

pnpm install --silent --ignore-scripts
pnpm exec tsc -p tsconfig.json

Arrancarla suelta, sin el verificador:

PORT=3000 node dist/main.js

Qué hay dentro de su directorio:

Archivo Qué es
ejecutar.json la receta que usa el verificador: qué hace falta, cómo se prepara y cómo arranca
package.json manifiesto de Node.js: nombre, tipo de módulo y dependencias con su rango de versión
pnpm-lock.yaml archivo de bloqueo: la versión exacta de cada dependencia y de sus dependencias
pnpm-workspace.yaml raíz de instalación propia, y la prohibición de ejecutar scripts al instalar
src/contador.component.ts código TypeScript
src/main.ts código TypeScript
src/padre.ts código TypeScript
tsconfig.json configuración del compilador de TypeScript

🔧 Svelte#

Mueve el trabajo del navegador al compilador. Cambia el coste de ejecución por dependencia de la fase de construcción: un compromiso, no una mejora gratuita.

Preparar sus dependencias, dentro de su directorio:

pnpm install --silent --ignore-scripts

Arrancarla suelta, sin el verificador:

PORT=3000 node server.mjs

Qué hay dentro de su directorio:

Archivo Qué es
Contador.svelte componente de Svelte
Padre.mjs código JavaScript (módulo ES)
ejecutar.json la receta que usa el verificador: qué hace falta, cómo se prepara y cómo arranca
package.json manifiesto de Node.js: nombre, tipo de módulo y dependencias con su rango de versión
pnpm-lock.yaml archivo de bloqueo: la versión exacta de cada dependencia y de sus dependencias
pnpm-workspace.yaml raíz de instalación propia, y la prohibición de ejecutar scripts al instalar
server.mjs código JavaScript (módulo ES)

🔧 SolidJS#

Reactividad de grano fino sin árbol virtual: el componente se ejecuta una vez y solo se actualiza lo que leyó el valor cambiado.

Preparar sus dependencias, dentro de su directorio:

pnpm install --silent --ignore-scripts

Arrancarla suelta, sin el verificador:

PORT=3000 node server.mjs

Qué hay dentro de su directorio:

Archivo Qué es
Contador.mjs código JavaScript (módulo ES)
Padre.mjs código JavaScript (módulo ES)
ejecutar.json la receta que usa el verificador: qué hace falta, cómo se prepara y cómo arranca
package.json manifiesto de Node.js: nombre, tipo de módulo y dependencias con su rango de versión
pnpm-lock.yaml archivo de bloqueo: la versión exacta de cada dependencia y de sus dependencias
pnpm-workspace.yaml raíz de instalación propia, y la prohibición de ejecutar scripts al instalar
server.mjs código JavaScript (módulo ES)

🔧 Lit#

Capa mínima sobre los componentes web del propio estándar. La apuesta por la plataforma en lugar de por el framework.

Preparar sus dependencias, dentro de su directorio:

pnpm install --silent --ignore-scripts

Arrancarla suelta, sin el verificador:

PORT=3000 node server.mjs

Qué hay dentro de su directorio:

Archivo Qué es
Contador.mjs código JavaScript (módulo ES)
Padre.mjs código JavaScript (módulo ES)
ejecutar.json la receta que usa el verificador: qué hace falta, cómo se prepara y cómo arranca
package.json manifiesto de Node.js: nombre, tipo de módulo y dependencias con su rango de versión
pnpm-lock.yaml archivo de bloqueo: la versión exacta de cada dependencia y de sus dependencias
pnpm-workspace.yaml raíz de instalación propia, y la prohibición de ejecutar scripts al instalar
server.mjs código JavaScript (módulo ES)

🔧 Alpine.js#

Comportamiento declarativo en atributos HTML, sin fase de construcción. Recupera el modelo de trabajo de jQuery con el vocabulario reactivo moderno.

Arrancarla suelta, sin el verificador:

PORT=3000 node server.mjs

Qué hay dentro de su directorio:

Archivo Qué es
contador.mjs código JavaScript (módulo ES)
ejecutar.json la receta que usa el verificador: qué hace falta, cómo se prepara y cómo arranca
package.json manifiesto de Node.js: nombre, tipo de módulo y dependencias con su rango de versión
padre.mjs código JavaScript (módulo ES)
server.mjs código JavaScript (módulo ES)

🔧 htmx#

Devuelve el estado al servidor: el HTML es la respuesta y los atributos deciden qué fragmento se reemplaza. Demuestra que la evolución del campo no es una línea recta hacia el cliente.

Arrancarla suelta, sin el verificador:

PORT=3000 node server.mjs

Qué hay dentro de su directorio:

Archivo Qué es
contador.mjs código JavaScript (módulo ES)
ejecutar.json la receta que usa el verificador: qué hace falta, cómo se prepara y cómo arranca
package.json manifiesto de Node.js: nombre, tipo de módulo y dependencias con su rango de versión
padre.mjs código JavaScript (módulo ES)
server.mjs código JavaScript (módulo ES)

Si alguna cadena de herramientas no está en tu máquina, node scripts/doctor.mjs dice cuál falta y con qué comando se instala. No hace falta tenerlas todas: el verificador ejecuta lo que encuentra y declara lo que omitió.

<!-- fin generado: fichas -->

🌐 Las implementaciones — el código a la vista#

Cinco mecanismos distintos para la misma regla.

React · react/Contador.mjs y react/Padre.mjs#

El canal de subida es una función que baja:

export function Contador({ valor, alCambiar }) {
  return h(
    "div",
    { "data-hijo": "contador", "data-valor": String(valor) },
    h("span", null, String(valor)),
    h("button", { onClick: () => alCambiar(1) }, "+1"),
    h("button", { onClick: () => alCambiar(-1) }, "-1"),
  );
}
 * En React el canal de subida es **una función que baja como una propiedad
 * más**. No hay mecanismo aparte: `alCambiar` es igual que `valor`, solo que
 * resulta ser invocable.

Y la regla, escrita donde se aplica:

 * Y aquí está la regla que organiza toda la comunicación entre componentes:
 * **el hijo no cambia el valor**. Lo muestra, y cuando el usuario pulsa el
 * botón, llama a la función que le dieron para avisar de cuánto quiere
 * cambiarlo.
 *
 * Quién decide es el padre. El hijo ni siquiera sabe qué va a pasar con su
 * aviso — puede que el padre sume, puede que ignore, puede que pida
 * confirmación. Esa ignorancia es lo que hace al hijo reutilizable.

El manejador, separado a propósito:

export function alRecibirCambio(valorActual, paso) {
  return valorActual + paso;
}

Vue · vue/Contador.mjs#

El evento se declara, igual que la propiedad:

  props: {
    valor: { type: Number, required: true },
  },
  emits: ["cambiar"],
 * Aquí está la diferencia con React, y es de las que se notan en un equipo:
 * `emits: ["cambiar"]` es **un contrato de salida declarado**. Cualquiera que
 * abra el archivo sabe qué eventos puede escuchar sin leer el cuerpo del
 * componente.
        h("button", { onClick: () => this.$emit("cambiar", 1) }, "+1"),

Y Vue defiende la regla más que ninguno:

 * La regla de fondo es la misma que en las ocho: **el hijo no cambia la
 * propiedad**. Vue lo lleva más lejos que casi ninguna — en desarrollo, mutar
 * una propiedad desde el hijo produce un aviso en la consola.

Angular · angular/src/contador.component.ts#

Cada dirección tiene su decorador:

export class ContadorComponent {
  @Input() valor = 0;
  @Output() cambiar = new EventEmitter<number>();
}
 * Angular es el único de los ocho donde las dos direcciones tienen **su propio
 * decorador**, y eso hace el contrato del componente legible de un vistazo: la
 * lista de entradas y la de salidas están una encima de la otra.

Y la sintaxis del padre lo dice con puntuación — angular/src/main.ts:

    // `(cambiar)` escucha la salida del hijo. Los paréntesis son la mitad de
    // arriba de la sintaxis de Angular: `[propiedad]` baja, `(evento)` sube, y
    // `[(ngModel)]` —los dos juntos— es el atajo de las dos direcciones que la
    // clase 086 desmonta.

Con una nota sobre el peso:

 * `EventEmitter` es un objeto de RxJS por debajo. Suena a mucho para emitir un
 * número, y lo es — pero significa que la salida de un componente es un flujo
 * observable, con todo lo que RxJS trae detrás.

Svelte · svelte/Contador.svelte#

  let { valor, alCambiar } = $props();
  // En Svelte 5 los eventos personalizados se fueron: lo que antes era
  // `createEventDispatcher` ahora es una propiedad que resulta ser función,
  // exactamente como en React. Es uno de los cambios más discutidos de la
  // versión 5, y la razón es la simetría — un solo canal en lugar de dos.

Un framework que quitó su mecanismo de eventos para parecerse a React es un dato sobre hacia dónde va el ecosistema, y merece verse en el archivo.

SolidJS · solid/Contador.mjs#

Se parece a React y tiene una trampa que no perdona:

export function Contador(props) {
  const valor = () => props.valor;
 * Es la trampa número uno de quien llega desde React: escribir
 * `function Contador({ valor })` rompe la reactividad de Solid, porque
 * desestructurar lee el valor UNA VEZ y se queda con esa copia.
 *
 * En React eso no importa —la función se vuelve a llamar entera en cada
 * cambio—; en Solid la función se llama una sola vez, así que hay que leer
 * `props.valor` en el momento de usarlo. De ahí el `() =>` de abajo.

Es la clase 005 en estado puro: código que parece de React, escrito en Solid, que compila y funciona hasta que algo cambia.

Lit · lit/Contador.mjs#

El canal de subida ya existía:

  avisar(paso) {
    // El hijo NO cambia `this.valor`. Emite y se olvida.
    this.dispatchEvent(
      new CustomEvent("cambiar", { detail: paso, bubbles: true, composed: true }),
    );
  }
 * Aquí está la diferencia grande de Lit con los otros siete: el canal de subida
 * no lo inventa el framework, **ya existe**. `CustomEvent` y `dispatchEvent` son
 * del estándar del DOM desde siempre, y el padre lo escucha con
 * `addEventListener` como escucharía un clic.

Con dos consecuencias que hay que conocer:

 *   - el evento BURBUJEA si se lo pides (`bubbles: true`), como cualquier evento
 *     nativo, así que puede escucharlo un ancestro lejano — para bien y para mal;
 *   - `composed: true` hace falta para que atraviese el DOM en la sombra, y
 *     olvidarlo es uno de los fallos clásicos de los componentes web.

Alpine.js · alpinejs/contador.mjs#

El mismo mecanismo que Lit, declarado en un atributo:

    `<button x-on:click="$dispatch('cambiar', 1)">+1</button>` +
 * La diferencia con Lit no es el mecanismo, es dónde se declara: allí en una
 * clase, aquí en un atributo. El canal es el mismo evento del DOM.

Y el padre escucha en el marcado — alpinejs/server.mjs:

// El padre escucha el evento que burbujea y decide. `$event.detail` trae el
// paso; el padre suma. La misma división que en las otras siete, escrita en un
// atributo en lugar de en un archivo.

htmx · htmx/contador.mjs#

El aviso es una petición:

    `<button hx-get="/evento?valor=${v}&paso=1" hx-target="closest [data-padre]">+1</button>` +
 * En htmx no hay propiedades ni eventos: hay **una petición**. El dato baja
 * porque el servidor lo escribe en el fragmento, y el aviso sube porque el botón
 * lleva `hx-get` con el paso dentro de la dirección.
 *
 * Eso convierte el flujo de datos de esta clase en algo mucho más familiar de lo
 * que parece: **es el mismo de la web de 1995**, con la diferencia de que en vez
 * de recargar la página entera se sustituye un trozo.

Y la ventaja que compensa la ida y vuelta:

 * Y tiene una propiedad que ninguno de los otros siete tiene: el estado vive en
 * un solo sitio —el servidor—, así que no hay dos verdades que sincronizar.
 * Se paga con una ida y vuelta por cada cambio.

La ruta que sirve a los dos clientes — htmx/server.mjs:

    // LA MISMA RUTA SIRVE PARA LAS DOS COSAS.
    //
    // El contrato la pide como JSON para comprobar el manejador; htmx la pide
    // como HTML para pegar el fragmento nuevo. Es la bifurcación de la clase
    // 081, aplicada a un componente.

🔬 Comparación#

Datos hacia abajo Avisos hacia arriba ¿Se declara la salida?
React propiedades una función que baja
Vue props con tipo $emit sobre un evento de emits
Angular @Input() @Output() con EventEmitter
Svelte $props() una función que baja
SolidJS props, que se leen una función que baja
Lit static properties CustomEvent del DOM
Alpine.js x-data $dispatchCustomEvent
htmx el fragmento del servidor una petición

Tres cosas que se leen de la tabla:

⚠️ Errores frecuentes#

✅ Verificación#

node scripts/run-class.mjs 083

Y para ver el flujo que declara cada una:

curl -s http://127.0.0.1:4100/flujo.json

🧪 Reto de transferencia#

  1. Haz que el hijo sume. Cambia una implementación para que el hijo modifique el valor y observa qué pasa —o qué no pasa— al renderizar dos veces.
  2. Cambia la regla del padre para que ignore los pasos negativos. El hijo no se entera y sigue funcionando: eso es la prueba de que la frontera está bien puesta.
  3. Escribe la lista de eventos que emiten los componentes de tu proyecto. Si tu framework no la declara, hacerla a mano es un ejercicio de arqueología revelador.

🔗 Enlaces#

Fuentes#