La lección 01-01, al presentar Lit frente a los grandes frameworks de aplicación, dejó una promesa pendiente: "esta comparación se retomará con mucho más detalle —incluyendo ejemplos concretos de interoperabilidad— en el módulo de integración e interoperabilidad". Ese momento ha llegado. La lección anterior mostró que un componente Lit funciona en cualquier HTML plano, sin ningún framework; esta lección va un paso más allá y examina qué ocurre cuando ese mismo componente se usa dentro de una aplicación construida con React, Vue o Angular, tres entornos que, cada uno a su manera, no fueron diseñados pensando en Custom Elements desde el principio, y que por eso presentan fricciones concretas —distintas en cada caso— al integrarlos.

Contenido

  1. Por qué la interoperabilidad no es automática pese al estándar
  2. React: el problema histórico de las propiedades no-string
  3. React: eventos personalizados y la solución con useRef
  4. <task-card> dentro de un componente React de ejemplo
  5. Vue: una interoperabilidad casi directa
  6. Angular: CUSTOM_ELEMENTS_SCHEMA y el resto de matices
  7. Tabla resumen de puntos de fricción por framework

  1. Por qué la interoperabilidad no es automática pese al estándar

La lección anterior ha insistido en que un componente Lit es, al final, un Custom Element estándar, utilizable en cualquier HTML sin depender de ningún framework. Esa afirmación sigue siendo cierta dentro de una aplicación React, Vue o Angular: en los tres casos, es perfectamente posible escribir la etiqueta de un componente Lit (<task-card>) directamente en el marcado de esa aplicación, y el navegador la reconocerá y la renderizará sin ningún problema. La fricción no aparece en si el elemento se renderiza, sino en cómo cada framework decide comunicarse con él: cada uno de los tres construye, por encima del DOM real, un modelo propio de cómo se pasan datos a un elemento y cómo se reciben eventos desde él, y ese modelo propio no siempre coincide con la forma en que Custom Elements —y, por tanto, Lit— esperan recibir datos y emitir eventos.

Concretamente, hay dos puntos de fricción recurrentes, en distinto grado según el framework: primero, cómo pasar un valor que no sea una cadena de texto simple (el mismo problema de fondo ya visto en la lección anterior, pero ahora desde el lado del framework que genera el marcado, no desde HTML escrito a mano); segundo, cómo escuchar un evento personalizado (CustomEvent, visto en la lección 05-02) que un componente Lit despache, cuando el sistema de eventos propio del framework espera un formato distinto.

  1. React: el problema histórico de las propiedades no-string

React, hasta bien entrada su historia (y de forma completa solo a partir de las versiones más recientes, con el soporte de propiedades de Custom Elements todavía consolidándose en distintas versiones), ha tenido un problema conocido al usar Custom Elements dentro de JSX: JSX, por defecto, traduce cualquier atributo que se escriba sobre una etiqueta minúscula (como task-list, que React trata como un elemento nativo del DOM, no como un componente propio de React) directamente a un atributo HTML, no a una propiedad de JavaScript. Esto es exactamente el mismo problema descrito en la lección anterior —la diferencia entre atributo y propiedad—, pero ahora oculto detrás de una sintaxis que, a primera vista, parece más flexible de lo que realmente es:

// Esto NO establece la propiedad `tareas` como un array real
function Tablero() {
  const tareas = [{ id: 't1', titulo: 'Diseñar', estado: 'pendiente', prioridad: 2 }];
  return <task-list tareas={tareas}></task-list>;
}

Aunque tareas={tareas} parezca, por su sintaxis, una asignación de propiedad como las que Lit usa internamente con el prefijo de punto (.tareas="${...}"), React lo traduce, para un elemento que no reconoce como componente propio de React, a un intento de establecer un atributo HTML; y como un atributo HTML solo puede ser una cadena, React termina llamando internamente a algo equivalente a elemento.setAttribute('tareas', tareas.toString()), que para un array produce la cadena literal "[object Object]" o una representación igualmente inútil, no el array real que <task-list> necesita en su propiedad reactiva tareas.

  1. React: eventos personalizados y la solución con useRef

El segundo problema histórico es simétrico, pero para eventos: React reconoce, dentro de JSX, atributos con el prefijo on (onClick, onChange) y los traduce a su propio sistema interno de gestión de eventos (el SyntheticEvent de React), pero ese reconocimiento está limitado a los nombres de evento que React ya conoce de antemano para elementos nativos del DOM. Un CustomEvent con un nombre propio de la aplicación, como tarea-cambiada (despachado por <task-card> desde la lección 05-02), no tiene ningún atributo on equivalente reconocido por React, y escribir <task-card onTareaCambiada={...}> no engancha nada, porque React no sabe traducir ese nombre a un addEventListener('tarea-cambiada', ...) real sobre el elemento.

La solución establecida para ambos problemas —propiedades complejas y eventos personalizados— es la misma, y coincide exactamente con la técnica ya vista en la lección anterior para HTML plano: obtener una referencia directa al elemento del DOM y manipularlo con las APIs nativas, en lugar de depender de la traducción automática de JSX. En React, esa referencia se obtiene con el hook useRef, y la asignación de propiedades y el registro de escuchadores de eventos se hacen dentro de un useEffect:

import { useEffect, useRef } from 'react';

function Tablero({ tareas }) {
  const refLista = useRef(null);

  useEffect(() => {
    const elemento = refLista.current;
    elemento.tareas = tareas;

    const manejarCambio = (evento) => {
      console.log('Tarea cambiada:', evento.detail);
    };
    elemento.addEventListener('tarea-cambiada', manejarCambio);

    return () => elemento.removeEventListener('tarea-cambiada', manejarCambio);
  }, [tareas]);

  return <task-list ref={refLista}></task-list>;
}

useEffect se ejecuta después de que React haya montado el elemento real en el DOM, momento en el que refLista.current ya apunta a la instancia concreta de <task-list>; a partir de ahí, elemento.tareas = tareas es una asignación de propiedad de JavaScript corriente, idéntica a la del apartado 5 de la lección anterior, y elemento.addEventListener('tarea-cambiada', ...) es el mismo mecanismo nativo del DOM usado desde la lección 05-02, sin ninguna traducción intermedia de React de por medio. La función devuelta por useEffect retira el escuchador cuando el componente se desmonta o cuando tareas cambia y el efecto se vuelve a ejecutar, evitando escuchadores duplicados acumulados en re-renderizados sucesivos.

Es importante señalar que las versiones más recientes de React han ido reduciendo esta fricción de forma progresiva, detectando en algunos casos automáticamente si una propiedad de JSX corresponde a una propiedad real del elemento en lugar de a un atributo; pero, al no ser un comportamiento garantizado ni uniforme entre versiones, el patrón de useRef más useEffect sigue siendo la técnica robusta y explícita que no depende de qué versión concreta de React esté en uso.

  1. <task-card> dentro de un componente React de ejemplo

Con las dos piezas anteriores, así se vería un componente React que envuelve <task-card> para exponerla con una API más cómoda hacia el resto de la aplicación React que la usa:

// src/components/TarjetaTarea.jsx
import { useEffect, useRef } from 'react';
import './task-card.js'; // registra <task-card> mediante customElements.define

function TarjetaTarea({ tarea, onCambio }) {
  const refTarjeta = useRef(null);

  useEffect(() => {
    const elemento = refTarjeta.current;
    elemento.titulo = tarea.titulo;
    elemento.estado = tarea.estado;
    elemento.prioridad = tarea.prioridad;

    const manejarCambio = (evento) => onCambio(evento.detail);
    elemento.addEventListener('tarea-cambiada', manejarCambio);
    return () => elemento.removeEventListener('tarea-cambiada', manejarCambio);
  }, [tarea, onCambio]);

  return <task-card ref={refTarjeta}></task-card>;
}

export default TarjetaTarea;

TarjetaTarea es un componente React perfectamente normal desde el punto de vista del resto de la aplicación —recibe props (tarea, onCambio) con la sintaxis habitual de React, y las traduce internamente hacia las propiedades y eventos reales de <task-card>—, actuando como una capa de adaptación entre el modelo de props de React y el modelo de propiedades y CustomEvent de Lit. Nótese que titulo, estado y prioridad podrían, en este caso concreto, pasarse igualmente como atributos JSX normales (titulo={tarea.titulo}), porque son cadenas o números simples y no sufren el problema del apartado 2; se asignan aquí como propiedades dentro del mismo useEffect solo por uniformidad con el resto del patrón, no porque sea estrictamente necesario para estos tres campos concretos.

  1. Vue: una interoperabilidad casi directa

Vue resuelve la mayor parte de esta fricción de forma nativa, sin necesidad de ningún patrón equivalente a useRef más useEffect para los casos habituales. La plantilla de un componente Vue de un fichero .vue reconoce directamente la sintaxis de binding (:propiedad="valor") sobre cualquier etiqueta, incluida una etiqueta de Custom Element, y la traduce a una asignación de propiedad real del DOM cuando el valor no es una cadena simple, exactamente el comportamiento que React necesita un patrón manual para lograr:

<template>
  <task-list :tareas="tareas" @tarea-cambiada="manejarCambio"></task-list>
</template>

<script setup>
import { ref } from 'vue';
import './task-list.js';

const tareas = ref([
  { id: 't1', titulo: 'Diseñar la base de datos', estado: 'hecha', prioridad: 2 },
]);

function manejarCambio(evento) {
  console.log('Tarea cambiada:', evento.detail);
}
</script>

:tareas="tareas" establece la propiedad real tareas sobre el elemento (no un atributo de texto), porque Vue, a diferencia de React con JSX, decide en tiempo de ejecución si el valor debe aplicarse como propiedad o como atributo según el tipo del valor y según si el nombre coincide con una propiedad conocida del elemento del DOM; y @tarea-cambiada="manejarCambio" registra un escuchador de evento nativo (addEventListener('tarea-cambiada', ...)) sin necesitar que tarea-cambiada sea un nombre de evento que Vue conozca de antemano, a diferencia de la limitación señalada para React en el apartado 3. El único ajuste que a veces hace falta declarar explícitamente es informar a Vue de qué etiquetas son Custom Elements y no componentes propios de Vue mal escritos, mediante la opción compilerOptions.isCustomElement en la configuración del proyecto (normalmente en vite.config.js), para que Vue no emita un aviso de "componente desconocido" al encontrarse con <task-list> en la plantilla.

  1. Angular: CUSTOM_ELEMENTS_SCHEMA y el resto de matices

Angular ocupa una posición intermedia: su sistema de plantillas es más estricto que el de Vue a la hora de aceptar etiquetas y atributos que no reconoce de antemano, precisamente porque Angular valida en tiempo de compilación qué elementos y propiedades existen, para poder ofrecer comprobación de tipos y detectar errores antes de ejecutar la aplicación. Por defecto, Angular rechaza compilar una plantilla que contenga una etiqueta desconocida como <task-card>, con un error del tipo "'task-card' is not a known element". La solución consiste en declarar explícitamente, en el módulo o componente correspondiente, que se permite el uso de Custom Elements no reconocidos por el propio Angular:

// tablero.component.ts
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import './task-card.js';

@Component({
  selector: 'app-tablero',
  template: `
    <task-card
      [titulo]="tarea.titulo"
      [estado]="tarea.estado"
      (tarea-cambiada)="manejarCambio($event)"
    ></task-card>
  `,
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class TableroComponent {
  tarea = { titulo: 'Revisar propuesta', estado: 'progreso' };

  manejarCambio(evento: CustomEvent) {
    console.log('Tarea cambiada:', evento.detail);
  }
}

schemas: [CUSTOM_ELEMENTS_SCHEMA] desactiva, para ese componente en concreto, la comprobación estricta de etiquetas y propiedades desconocidas, permitiendo a Angular compilar la plantilla sin quejarse de <task-card> ni de sus atributos. Una vez añadido ese esquema, la sintaxis de binding de propiedades de Angular ([titulo]="...", con corchetes) sí traduce correctamente a una asignación de propiedad del DOM, de forma parecida a Vue; y la sintaxis de eventos ((tarea-cambiada)="...", con paréntesis) sí escucha correctamente un CustomEvent con ese nombre exacto, sin la limitación de nombres predefinidos que afecta a React. El coste de este enfoque es, precisamente, la pérdida de la comprobación de tipos que Angular ofrece para sus propios componentes: con CUSTOM_ELEMENTS_SCHEMA activo, Angular ya no puede avisar en tiempo de compilación si se escribe, por error, [titulol] en lugar de [titulo], y ese error solo se manifestaría en tiempo de ejecución, con la propiedad titulol simplemente ignorada por <task-card>.

  1. Tabla resumen de puntos de fricción por framework

Aspecto React Vue Angular
Pasar una propiedad compleja (array, objeto) Requiere useRef + useEffect; JSX la trataría como atributo de texto Directo con :propiedad="valor"; Vue decide propiedad o atributo automáticamente Directo con [propiedad]="valor", una vez declarado CUSTOM_ELEMENTS_SCHEMA
Escuchar un CustomEvent con nombre propio Requiere addEventListener manual dentro de useEffect; no hay atributo on reconocido Directo con @nombre-evento="manejador" Directo con (nombre-evento)="manejador($event)", una vez declarado el esquema
Configuración previa necesaria Ninguna especial, pero exige el patrón manual en cada uso compilerOptions.isCustomElement para evitar avisos schemas: [CUSTOM_ELEMENTS_SCHEMA] en cada componente que use Custom Elements
Coste de la solución Código repetitivo (una capa de adaptación por componente envuelto) Prácticamente ninguno para los casos habituales Pérdida de comprobación de tipos estricta en esa plantilla
Tendencia Mejorando en versiones recientes, pero sin garantía uniforme Ya resuelto de forma nativa desde hace tiempo Estable, requiere el paso explícito del esquema

El patrón de fondo, visible en las tres columnas, es siempre el mismo: cuanto más un framework decide inspeccionar y validar de antemano las etiquetas y atributos de una plantilla (Angular, y en menor medida React con JSX), más fricción aparece al introducir un elemento que ese framework no puede conocer de antemano por no formar parte de su propio ecosistema de componentes; cuanto más un framework decide delegar en tiempo de ejecución la decisión de si algo es una propiedad o un atributo (Vue, de forma bastante directa), menos fricción hay. Ninguno de los tres frameworks impide, en ningún caso, usar un componente Lit; la diferencia está en cuánto código de adaptación adicional hace falta escribir para lograrlo con comodidad.

Errores Comunes y Consejos

  • Pasar un array o un objeto por atributo JSX en React esperando que funcione como propiedad: como se ha explicado en el apartado 2, JSX traduce por defecto a atributo HTML sobre una etiqueta que React no reconoce como su propio componente; hace falta el patrón de useRef más useEffect del apartado 3 para cualquier valor que no sea una cadena o número simple.
  • Olvidar declarar CUSTOM_ELEMENTS_SCHEMA en Angular: sin él, la aplicación ni siquiera compila, con un error de "elemento desconocido" señalando directamente la etiqueta del componente Lit; es un paso obligatorio, no opcional, en cualquier componente Angular que use Custom Elements de terceros o propios.
  • Suponer que Vue necesita el mismo patrón manual que React: como se ha visto en el apartado 5, Vue resuelve automáticamente la mayoría de casos con su sintaxis de binding habitual; replicar el patrón de useRef en Vue no es necesario y solo añade código redundante.
  • No retirar los escuchadores de evento añadidos manualmente con addEventListener: en el patrón de React del apartado 3, olvidar la función de limpieza devuelta por useEffect acumula escuchadores duplicados en cada re-renderizado que vuelva a ejecutar el efecto, un error de gestión de memoria fácil de introducir y difícil de detectar sin herramientas de perfilado.

Ejercicios

  1. Reescribe el componente TarjetaTarea de React del apartado 4 usando la biblioteca oficial @lit/react, que ofrece una función createComponent(...) capaz de generar automáticamente un componente React envolvente a partir de la clase de un componente Lit, sin necesidad de escribir a mano el useRef y el useEffect. (Pista: investiga la firma de createComponent en la documentación de @lit/react; no hace falta memorizarla, basta con razonar qué información necesitaría recibir para poder generar el envoltorio automáticamente: la clase del elemento, su nombre de etiqueta, y qué eventos debe traducir a props de React.)
  2. Explica, basándote en el apartado 6, por qué CUSTOM_ELEMENTS_SCHEMA reduce la seguridad de tipos de Angular, y qué comprobación en concreto deja de hacer el compilador de Angular sobre una plantilla que lo declare.
  3. Un compañero de equipo, trabajando en Vue, se sorprende de que :tareas="tareas" funcione directamente sin ningún paso adicional, mientras que en React necesitó el patrón completo de useRef. Explica la diferencia de fondo entre ambos frameworks que provoca esa distinta experiencia, apoyándote en los apartados 2 y 5.

Soluciones

  1. createComponent de @lit/react necesita, como mínimo, tres piezas de información para generar el envoltorio automáticamente: la biblioteca de React (react, para poder usar sus hooks internamente sin que el proyecto tenga que declararlos), la clase del propio componente Lit (TaskCard, para poder inspeccionar sus propiedades reactivas declaradas en static properties), y el nombre de la etiqueta ya registrada ('task-card'). Con esas tres piezas, createComponent puede generar automáticamente, por dentro, exactamente el mismo patrón de useRef y useEffect escrito a mano en el apartado 4 —asignar cada propiedad reactiva conocida como propiedad de JavaScript real, no como atributo— y, para los eventos, aceptar un mapa adicional (algo como events: { onTareaCambiada: 'tarea-cambiada' }) que traduce cada CustomEvent a una prop de tipo función con la convención de nombres on... habitual en React, sin que el desarrollador tenga que escribir addEventListener a mano en cada componente envuelto.
  2. CUSTOM_ELEMENTS_SCHEMA desactiva la validación que Angular hace, en tiempo de compilación, sobre si una etiqueta existe como componente o directiva conocida por Angular, y sobre si cada [propiedad] o (evento) escrito en una plantilla corresponde realmente a una entrada declarada de esa etiqueta (una @Input() o un @Output(), en la terminología de Angular). Sin el esquema, Angular puede avisar en tiempo de compilación de un error de tecleo como [titulol] en lugar de [titulo]; con el esquema activo, esa misma comprobación queda desactivada para toda la plantilla del componente, y un error de ese tipo solo se manifestaría en tiempo de ejecución, con la propiedad mal escrita simplemente ignorada sin ningún aviso.
  3. La diferencia de fondo es el momento y el criterio en que cada framework decide si un valor debe aplicarse como propiedad de JavaScript o como atributo HTML. React con JSX aplica una regla fija y sintáctica: sobre una etiqueta que no reconoce como su propio componente, cualquier atributo de JSX se traduce por defecto a atributo HTML, sin inspeccionar el tipo del valor ni el elemento real. Vue, en cambio, decide en tiempo de ejecución, inspeccionando el propio elemento del DOM: si existe una propiedad de ese nombre en el elemento (algo que puede comprobar dinámicamente, ya que tareas es una propiedad reactiva real declarada por Lit en static properties y expuesta como propiedad de instancia del Custom Element), Vue la asigna como propiedad; si no, recurre a un atributo. Esa inspección dinámica es precisamente lo que le permite a Vue resolver correctamente casos como :tareas="tareas" sin ningún patrón manual adicional.

Conclusión

Esta lección ha cumplido la promesa de la lección 01-01: una comparación práctica, con ejemplos concretos, de cómo se integra un componente Lit dentro de React, Vue y Angular, mostrando que el problema de fondo es siempre el mismo —cómo pasar valores no-cadena y cómo escuchar eventos personalizados— pero que cada framework lo resuelve, o lo obliga a resolver manualmente, de forma distinta según cuánto valide de antemano sus propias plantillas. Con la interoperabilidad hacia otros frameworks ya cubierta, queda un escenario de integración de naturaleza distinta: qué ocurre cuando un componente Lit no se renderiza en el navegador del usuario final, sino en un servidor, antes de que la página llegue siquiera a cargarse. La siguiente lección presenta @lit-labs/ssr y el problema del renderizado en el servidor para Web Components.

Curso de Lit

Módulo 1: Introducción a Lit y Web Components

Módulo 2: Plantillas Reactivas y Renderizado

Módulo 3: Propiedades y Estado Reactivo

Módulo 4: Estilos en Componentes Lit

Módulo 5: Eventos y Comunicación entre Componentes

Módulo 6: Ciclo de Vida y Comportamiento Avanzado

Módulo 7: Directivas y Funcionalidades Avanzadas de Plantillas

Módulo 8: Integración, Interoperabilidad y Despliegue

Módulo 9: Pruebas y Buenas Prácticas

Módulo 10: Proyecto: Construyendo TaskFlow

© Copyright 2026. Todos los derechos reservados