Ir al contenido

Adapter de React

El adapter de React es el más completo de los trece: es el único que soporta a la vez props vivas (update) e hidratación SSR (hydrate).

Ventana de terminal
npm install wu-framework react react-dom
// src/main.jsx — la micro-app
import { wuReact } from 'wu-framework/adapters/react';
import App from './App';
wuReact.register('cart', App);

Esa es toda la integración. Levanta el servidor de desarrollo por su cuenta y la app se monta en #root en modo standalone; cárgala desde un shell y se monta en el Shadow DOM del shell.

await wuReact.register('cart', CartApp, {
props: { currency: 'EUR', locale: 'es-ES' },
strictMode: true,
standalone: true,
standaloneContainer: '#root',
onMount: (container) => console.log('[cart] montado'),
onUnmount: (container) => console.log('[cart] desmontando'),
});
Opción Tipo Por defecto Notas
props object {} Props iniciales. Se fusionan con lo que llegue vía wu.update().
strictMode boolean true Envuelve el árbol en React.StrictMode. Aplica tanto al montaje como a la hidratación.
standalone boolean true Monta directamente cuando no se encuentra un shell.
standaloneContainer string '#root' Selector usado en modo standalone.
onMount function null (container) => void, después de montar y después de hidratar.
onUnmount function null (container) => void, antes de root.unmount().

Devuelve Promise<boolean>.

ensureReact() resuelve React en este orden:

  1. window.React + window.ReactDOM — el montaje habitual de micro-frontends. Si falta window.ReactDOM.createRoot (expusiste react-dom en vez de react-dom/client), importa dinámicamente react-dom/client para obtener createRoot y hydrateRoot.
  2. import('react') + import('react-dom/client') dinámicos.

Si ninguna funciona, register() registra un error y devuelve false.

El adapter de React anuncia update, así que el shell puede enviar props nuevas sin remontar:

// En el shell
await wu.update('cart', { currency: 'USD' });

Internamente el adapter fusiona las props nuevas sobre las actuales y vuelve a llamar a root.render() sobre la raíz existente. React reconcilia en el sitio: el árbol de componentes conserva su estado, los efectos no vuelven a ejecutar su limpieza y no se tira nada del DOM.

Al desmontar, las props enviadas en vivo se descartan y currentProps vuelve a las props que pasaste a register(). Esto importa cuando wu-core reubica una app en otro contenedor: el nuevo montaje parte de las props de registro, no de lo último que envió el shell.

Cuando el contenedor lo produjo wu-framework/server, wu-core llama a hydrate en lugar de mount, pasando exactamente las props con las que renderizó el servidor:

// Las props del servidor ganan sobre las de registro: ellas produjeron el markup.
wuReact.register('cart', CartApp, { props: { currency: 'EUR' } });
// El servidor renderizó con { currency: 'USD' } → la hidratación usa 'USD'.

El adapter llama a hydrateRoot(container, element) sin vaciar el contenedor: vaciarlo destruiría justamente el markup que se está hidratando.

Dos planes B mantienen esto seguro:

  • No hay hydrateRoot disponible (React < 18, o un bundle sin react-dom/client) → avisa y monta normalmente. Correcto, solo que repintado.
  • La hidratación lanza (desajuste de markup) → registra el error y monta normalmente. Un desajuste nunca debe dejar la app sin montar.

Consulta Hidratación para el lado del servidor.

strictMode vale true por defecto. En desarrollo, React montará, desmontará y volverá a montar tu componente; el adapter no intenta suprimir eso. La protección contra la doble invocación de StrictMode a nivel de Wu (desmontaje diferido) vive en wu-core, no en el adapter.

Pon strictMode: false si alguna dependencia de terceros de tu árbol se porta mal bajo doble invocación.

El adapter de React exporta factorías, no componentes. Le pasas tu propio namespace React para que el adapter nunca tenga que resolver una segunda copia de React, lo que rompería los hooks.

import React from 'react';
import { createWuSlot } from 'wu-framework/adapters/react';
const WuSlot = createWuSlot(React);
export default function Shell() {
return (
<main>
<WuSlot name="cart" url="http://localhost:3001" />
<WuSlot
name="checkout"
url="http://localhost:3002"
fallback={<Spinner />}
onLoad={({ name }) => console.log(name, 'cargado')}
onError={(err) => report(err)}
/>
</main>
);
}
Prop Tipo Notas
name string Nombre de la micro-app. También se usa para el id del contenedor.
url string URL de la app (se guarda como data-wu-url).
appName string Sustituye a name cuando el id del DOM y el nombre de la app deben diferir.
fallback ReactNode Reemplaza el elemento “Loading…” por defecto.
onLoad function ({ name, url }) => void
onError function (err) => void
onMount function ({ name, container }) => void
onUnmount function ({ name }) => void
className, style Se aplican al elemento del slot.

Dos detalles de implementación que conviene conocer, porque explican la forma del DOM renderizado:

  • El div de destino tiene un id estable derivado del nombre de la app (wu-slot-<appName>) y está presente desde el primer render, así que wu.mount(name, '#id') lo encuentra sin ningún appendChild imperativo.
  • El fallback y el destino de montaje son hermanos dentro de un Fragment, nunca padre e hijo. wu.mount crea una segunda raíz de React dentro del destino; si React también gestionara hijos ahí, desmontar el fallback llamaría a removeChild sobre nodos que la micro-app ya reemplazó — "The node to be removed is not a child of this node".

El efecto de montaje es seguro con StrictMode: un timeout de 50 ms más un flag cancelled hacen que el primer ciclo (descartado) de StrictMode se cancele antes de llegar a montar.

import React from 'react';
import { createUseWuEvents } from 'wu-framework/adapters/react';
const useWuEvents = createUseWuEvents(React);
function Cart() {
const { emit, on, once } = useWuEvents();
React.useEffect(() => on('user:login', (e) => setUser(e.data)), [on]);
return <button onClick={() => emit('cart:add', { sku: 'A1' })}>Añadir</button>;
}

on() devuelve una función para cancelar la suscripción y la registra internamente, así que toda suscripción se cierra cuando el componente se desmonta; devolverla desde el efecto, como arriba, es doble seguridad.

const useWuStore = createUseWuStore(React);
function Profile() {
const { state, setState, getState } = useWuStore('user');
return (
<input
value={state?.name ?? ''}
onChange={(e) => setState('name', e.target.value)}
/>
);
}

setState(path, value) escribe en <namespace>.<path>. La suscripción es sobre la propia ruta del namespace, lo que captura tanto escrituras en la raíz (set('user', {...})) como escrituras profundas (set('user.profile.name', x)) gracias a la notificación por rutas padre del store.

const useWuAI = createUseWuAI(React);
function Assistant() {
const { messages, send, isStreaming, error, abort, clear } = useWuAI({
namespace: 'support',
onActionExecuted: (data) => console.log('la IA ejecutó', data.action),
});
return (
<>
{messages.map((m) => <Bubble key={m.id} role={m.role} text={m.content} />)}
<button onClick={() => send('Muéstrame los pedidos pendientes')} disabled={isStreaming}>
Preguntar
</button>
{isStreaming && <button onClick={abort}>Parar</button>}
</>
);
}

Este es el hook de IA más rico de los trece adapters: el único con streaming real de tokens. send() itera wu.ai.stream() y va añadiendo los fragmentos de texto a un mensaje provisional del asistente conforme llegan; sendSync() espera la respuesta completa en su lugar. Los mensajes con role: 'action' se inyectan automáticamente cuando la IA ejecuta una acción, disparados por el evento ai:action:executed.

Consulta Visión general de IA para el modelo de orquestación.

import {
wuReact, // export por defecto, y el objeto que contiene todo lo de abajo
register,
createWuSlot,
createUseWuEvents,
createUseWuStore,
createUseWuAI,
getWuInstance,
waitForWu,
} from 'wu-framework/adapters/react';

Desde el barrel (wu-framework/adapters), register se re-exporta como registerReact.