Ir al contenido

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 Vue
import { 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 dentro
const 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.

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»
wu.config.json
{ "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.

mf-ui/src/App.jsx
import { provide } from 'wu-framework';
export function Tarjeta({ titulo, children }) { /* … */ }
provide('ui.componentes', { Tarjeta }, {
version: '1.0.0',
app: 'ui',
shape: { Tarjeta: 'function' },
});
mf-agentic/src/App.jsx
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.

mf-ui — el dueño
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' });
Angular — no importa React
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?.(); }
}
Vue — el hueco puede aparecer y desaparecer
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.

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.

mf-ui/src/elemento.jsx
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:

Angular
<ui-panel titulo="Usuarios" tono="aviso">
<ul>
@for (u of usuarios; track u.nombre) { <li>{{ u.nombre }}</li> }
</ul>
</ui-panel>
Vue
<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>.

Angular — sin esto NO compila
@Component({ schemas: [CUSTOM_ELEMENTS_SCHEMA], /* … */ })
Vue — sin esto avisa en cada render
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.

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.

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