Componentes entre frameworks
Sí: un componente de React se puede usar desde Angular o desde Vue. Esta guía explica cómo, con las cuatro vías que hay y cuál elegir en cada caso.
Hay exactamente una cosa que no funciona, y conviene quitarla de en medio antes de seguir:
// ✗ Esto no funciona en una plantilla de Angular o de Vueimport { Card } from 'la-libreria';// <Card titulo="…" />Importar el componente y ponerlo en la plantilla de OTRO framework es imposible.
<Card/> compila a React.createElement(Card), que devuelve un objeto plano
—una descripción— que solo el renderer de React sabe interpretar. Angular y Vue
reciben un objeto que no entienden, y no se pinta nada.
Eso es lo que es un componente de React, no una limitación de Wu. Y es lo único que está vedado.
Todo lo demás funciona:
// ✓ Angular le da un nodo y React renderiza dentroconst ui = await consume('ui.componentes', '^1.0.0', { wait: true });ui.montarTarjeta(this.hueco.nativeElement, { titulo: 'Hola' });<!-- ✓ O como elemento HTML normal, hijos incluidos --><ui-panel titulo="Usuarios"> <ul>@for (u of usuarios; track u.nombre) { <li>{{ u.nombre }}</li> }</ul></ui-panel>La idea de fondo es siempre la misma: en vez de entregar una función que solo React entiende, el dueño renderiza y el consumidor solo aporta un hueco.
Cuál elegir
Sección titulada «Cuál elegir»| Necesitas | Vía | Coste |
|---|---|---|
| Componente React → módulo React | provide + uso directo |
exige shared: ["react", …] |
| Componente React → Angular o Vue, solo datos | provide con montar(el, props) |
props serializables, sin hijos |
| Composición con hijos entre frameworks | Web Component con <slot> |
atributos como cadenas, sin tipos |
| UI global: modal, aviso, confirmación | micro-app servicio | sin tipos en la frontera |
Empieza por la segunda. Cubre casi todo y es la más simple. Las otras existen para lo que esa no puede.
Requisito común: una sola instancia de React
Sección titulada «Requisito común: una sola instancia de React»{ "shared": ["react", "react-dom", "react-dom/client"] }Sin esto cada módulo empaqueta su copia, el componente prestado ejecuta sus hooks contra un React distinto del que lo renderiza, y revienta con Invalid hook call. Ver compartir dependencias.
1 · Entre módulos React: provide y consume
Sección titulada «1 · Entre módulos React: provide y consume»El módulo dueño publica; los demás consumen.
import { provide } from 'wu-framework';
export function Tarjeta({ titulo, children }) { /* … */ }
provide('ui.componentes', { Tarjeta }, { version: '1.0.0', app: 'ui', shape: { Tarjeta: 'function' },});import { consume } from 'wu-framework';
const UI = consume('ui.componentes', '^1.0.0');
export default function App() { return <UI.Tarjeta titulo="Hola">contenido</UI.Tarjeta>;}Frente a un paquete npm, esto gana en una cosa concreta: cambias el componente, despliegas ese módulo, y los demás lo tienen sin recompilar. Con 24 módulos, es la diferencia entre iterar el design system y no tocarlo.
Pierdes los tipos en la frontera. Se mitiga publicando solo un .d.ts del
contrato, sin código.
2 · Hacia Angular o Vue: entregar montar(el, props)
Sección titulada «2 · Hacia Angular o Vue: entregar montar(el, props)»Se invierte quién renderiza. En vez de dar una función que solo React entiende,
el dueño da cómo montarse en un nodo. Es el mismo contrato
{ mount, unmount } que Wu usa para apps enteras, a escala de componente.
import { createRoot } from 'react-dom/client';
function montarTarjeta(el, props = {}) { const root = createRoot(el); root.render(React.createElement(Tarjeta, props)); return () => root.unmount(); // el consumidor desmonta en SU ciclo de vida}
provide('ui.componentes', { Tarjeta, montarTarjeta }, { version: '1.1.0', app: 'ui' });const UI = consume('ui.componentes', '^1.0.0'); // fuera de la clase
@Component({ /* … */ })export class AppComponent implements AfterViewInit, OnDestroy { @ViewChild('hueco') hueco!: ElementRef<HTMLElement>; private desmontar: (() => void) | null = null;
async ngAfterViewInit() { const ui: any = await consume('ui.componentes', '^1.0.0', { wait: true, timeout: 8000 }); this.desmontar = ui.montarTarjeta(this.hueco.nativeElement, { titulo: 'Hola' }); }
ngOnDestroy() { this.desmontar?.(); }}watch(activo, async (name) => { if (name) { limpiar(); return; } await nextTick(); // esperar a que exista el div const ui = await consume('ui.componentes', '^1.0.0', { wait: true, timeout: 8000 }); if (!hueco.value) return; // pudo navegar durante el await desmontar = ui.montarTarjeta(hueco.value, { titulo: 'Hola' });}, { immediate: true });Y en Vue, la guarda de después del await no es paranoia: entre el await y la
línea siguiente el usuario puede haber navegado y el v-if haber borrado el
div. Sin ella, React monta en un nodo huérfano — sin error, consumiendo memoria
y sin verse.
3 · Con hijos: Web Component y <slot>
Sección titulada «3 · Con hijos: Web Component y <slot>»Las vías anteriores mueven datos. Los hijos no cruzan: children en React
es un árbol de React.
Un Web Component sí, porque la proyección la hace el navegador.
class UiPanel extends HTMLElement { static observedAttributes = ['titulo', 'tono']; #root = null;
connectedCallback() { if (!this.shadowRoot) this.attachShadow({ mode: 'open' }); const montaje = document.createElement('div'); this.shadowRoot.appendChild(montaje); this.#root = createRoot(montaje); this.#render(); }
disconnectedCallback() { // En microtask: React protesta si desmontas durante un render, y este // callback puede dispararse en medio de uno del consumidor. const root = this.#root; this.#root = null; queueMicrotask(() => root?.unmount()); }
attributeChangedCallback() { this.#render(); }
#render() { this.#root?.render( React.createElement(Panel, { titulo: this.getAttribute('titulo') }, React.createElement('slot'), // React renderiza un <slot> normal ), ); }}
// Definir dos veces el mismo nombre LANZA. Con varios módulos, pasa.if (!customElements.get('ui-panel')) customElements.define('ui-panel', UiPanel);React no sabe qué es un slot ni le importa: para él es una etiqueta más. Quien proyecta es el DOM.
Los consumidores meten su marcado dentro:
<ui-panel titulo="Usuarios" tono="aviso"> <ul> @for (u of usuarios; track u.nombre) { <li>{{ u.nombre }}</li> } </ul></ui-panel><ui-panel titulo="Módulos"> <ul><li v-for="m in modulos" :key="m.name">{{ m.name }}</li></ul></ui-panel>Esos <li> nunca entran en el árbol de React: siguen siendo nodos de
Angular o de Vue, vivos y reactivos, proyectados en el <slot>.
Lo que hay que decirle a cada framework
Sección titulada «Lo que hay que decirle a cada framework»@Component({ schemas: [CUSTOM_ELEMENTS_SCHEMA], /* … */ })vue({ template: { compilerOptions: { isCustomElement: (tag) => tag.startsWith('ui-') } } })Y el precio: los atributos HTML son cadenas. Para datos estructurados hay
que asignar una propiedad del elemento (el.datos = {...}), no un atributo. La
vuelta va por CustomEvent. Y no hay tipos.
4 · UI global: micro-app servicio
Sección titulada «4 · UI global: micro-app servicio»Para lo que aparece una vez y encima de todo —modal, aviso, confirmación— no uses un componente: usa un módulo que se monta una vez y atiende peticiones.
Sirve a cualquier framework sin que el consumidor importe nada, y no hay versiones que sincronizar entre módulos.
Resumen
Sección titulada «Resumen»| Cruza | provide directo |
montar(el, props) |
Web Component |
|---|---|---|---|
| Props (datos) | ✅ | ✅ | ✅ como cadenas |
| Callbacks | ✅ | ✅ | vía CustomEvent |
| Hijos | ✅ solo React↔React | ✗ | ✅ |
| Tipos | ✗ | ✗ | ✗ |
| Frameworks | solo React | todos | todos |
