Dependencias compartidas: shared y singletons
Si host y remote empaquetan cada uno su propio React, el usuario lo descarga dos veces y —peor— acaban conviviendo dos instancias que rompen hooks y contextos. El campo shared declara qué dependencias viven en un ámbito común; singleton fuerza una sola instancia y la negociación de versiones elige, en runtime, la copia compatible que todos usarán.
La federación tiene un problema estructural: si el host trae su React y cada remote trae el suyo, el usuario descarga el mismo framework varias veces y, mucho peor, terminan conviviendo varias instancias de una librería que asume ser única. Hooks que fallan, contextos que no se ven, estados que se duplican. El campo shared existe para esto: declara qué dependencias viven en un ámbito común, con singleton para forzar una única instancia y una negociación de versiones que, en runtime, elige la copia que todos compartirán.
- Entender por qué duplicar React o Vue entre host y remotes rompe cosas, no solo las infla.
- Declarar dependencias en
sharedy comprender el ámbito compartido como registro de runtime. - Usar
singleton,requiredVersionystrictVersionpara gobernar la negociación de versiones. - Sopesar
eagery su coste, y saber cuándo una dependencia debe compartirse y cuándo no.
El problema: dos Reacts en la misma página
Sin shared, cada aplicación federada es autosuficiente: el host empaqueta React, y cada remote empaqueta el suyo. En una página que compone un host y tres remotes, el navegador puede acabar descargando cuatro copias del mismo framework. El desperdicio de bytes es lo de menos.
El daño real es semántico. Librerías como React, Vue o cualquier gestor de estado guardan estado a nivel de módulo: React lleva la cuenta del hook actual en una variable interna compartida por todo el árbol. Si en la misma página hay dos instancias de React —la del host y la del remote—, un componente del remote renderizado dentro del árbol del host llama a los hooks de su React, distinto del que gestiona el árbol. El resultado es el célebre error de hooks inválidos, contextos que aparecen vacíos y un instanceof que falla porque las clases vienen de módulos distintos.
// Sintoma clasico de dos instancias de React coexistiendo:
// "Invalid hook call", o un contexto que siempre da su valor por defecto
// aunque el Provider este claramente mas arriba en el arbol.
La lección es que ciertas dependencias no toleran duplicarse: no basta con que la versión sea la misma, tiene que ser la misma instancia cargada una sola vez. Compartir no es aquí una optimización de tamaño; es una condición de correctitud.
shared y el ámbito compartido
shared declara qué dependencias se ofrecen y se consumen desde un ámbito compartido (el share scope): un registro en runtime donde cada aplicación deposita las versiones que trae y del que toma las que necesita. En lugar de que cada bundle cargue su copia sin mirar a los lados, todos publican lo que tienen en ese registro común y la runtime decide, cuando alguien pide una dependencia, qué copia entregar.
// Tanto el host como cada remote declaran lo que comparten.
new ModuleFederationPlugin({
name: "host",
shared: {
react: { singleton: true, requiredVersion: "^19.0.0" },
"react-dom": { singleton: true, requiredVersion: "^19.0.0" },
},
});
El mecanismo es elegante: una dependencia compartida se carga como un chunk asíncrono aparte, no fundido en el bundle principal. Así, cuando la página arranca, la runtime puede mirar el ámbito compartido, ver qué copias hay disponibles y cargar solo una, evitando la descarga redundante y, con singleton, la coexistencia de instancias.
singleton y la negociación de versiones
Compartir plantea de inmediato una pregunta: si el host trae React 19.1 y un remote pide React 19.0, ¿cuál se usa? Aquí entran tres opciones que gobiernan la política.
singleton: true impone que solo puede haber una instancia en toda la página. La runtime elige la versión más alta compatible entre las que se ofrecen y la sirve a todos; si alguien pedía una incompatible, emite un aviso pero sigue con la única instancia, porque tener dos rompería más que una versión ligeramente distinta. Es obligatorio para React, Vue y cualquier librería con estado de módulo.
requiredVersion declara qué rango de versiones acepta un consumidor, en semver. La runtime lo usa para negociar: busca en el ámbito compartido la versión más alta que satisfaga todos los rangos requeridos.
strictVersion: true convierte el aviso en error: si no hay una versión que cumpla el rango, la carga falla en vez de degradar. Útil cuando una incompatibilidad es preferible detectarla ruidosamente a arrastrarla en silencio.
| Opción | Qué hace | Cuándo |
|---|---|---|
singleton |
una sola instancia para toda la página | React, Vue, estado |
requiredVersion |
rango semver que el consumidor acepta | siempre que compartas |
strictVersion |
falla en vez de avisar si no encaja | contratos rígidos |
eager |
incluir en el chunk inicial, no async | evitar cascadas en la raíz |
flowchart TD H[host ofrece react 19.1] --> S[ambito compartido] R1[remote A pide react 19.0] --> S R2[remote B pide react 19.x] --> S S -->|elige la mas alta compatible| U[una sola instancia: react 19.1] style S fill:#f9e2af,color:#11111b style U fill:#a6e3a1,color:#11111b style H fill:#cba6f7,color:#11111b
Olvidar singleton: true en React no rompe el build ni suele romper el primer render: rompe a veces, cuando un componente remoto que usa hooks se monta dentro del árbol del host con una versión distinta cargada. Son los bugs más caros de la federación porque no fallan de forma determinista y el rastro no señala a la configuración. Ante cualquier fallo de hooks o de contexto entre host y remote, lo primero que hay que auditar es que las librerías con estado de módulo estén marcadas como singleton en todas las apps.
eager y el coste de compartir
Compartir no es gratis. Una dependencia compartida se carga como chunk asíncrono, lo que introduce un pequeño baile de resolución antes de que esté disponible. eager: true la incluye en el chunk inicial en vez de diferirla, eliminando ese baile a costa de renunciar a la carga perezosa: la dependencia viaja siempre, la use o no esa ruta. Se reserva para la dependencia raíz que sabes que todos necesitan desde el primer instante.
// eager: la comparte pero la mete en el bundle inicial, sin chunk async
shared: {
react: { singleton: true, eager: true, requiredVersion: "^19.0.0" },
}
Y no todo debe compartirse. Compartir tiene sentido para lo que es pesado y común —el framework, la librería de UI, el cliente de datos— y donde una sola instancia es una condición de correctitud. Para una utilidad pequeña y sin estado, compartir puede costar más —en coordinación de versiones y en complejidad— de lo que ahorra en bytes. La regla: comparte lo que debe ser único o es caro de duplicar; deja que cada quien empaquete lo trivial.
Cada dependencia en shared es un punto de acoplamiento entre apps que, se supone, se despliegan por separado. Si el host y los remotes comparten React como singleton, todos quedan atados a un rango de versiones compatible: nadie puede saltar a la próxima major de React en solitario sin negociarlo con los demás. Compartir resuelve la duplicación pero crea coordinación. Es el trade-off central del nivel, y lo veremos de frente en la última lección.
La intuición inicial sobre shared es que existe para no descargar React cuatro veces, y esa lectura, aunque cierta, es la superficial y la que lleva a usarlo mal. El propósito profundo no es de tamaño sino de identidad: hay librerías que solo funcionan si en toda la página existe una única instancia suya, porque guardan estado en el ámbito del módulo —el hook actual en React, el registro reactivo en Vue, el store de un gestor de estado— y dos copias significan dos verdades paralelas que no se ven entre sí. Por eso singleton no es una opción de rendimiento sino una de correctitud, y por eso su ausencia produce los bugs más desconcertantes de toda la arquitectura federada. Pero al garantizar esa única instancia, shared hace algo con consecuencias que trascienden lo técnico: transforma un problema de bytes en un problema de contratos de versión entre equipos. En el instante en que el host y tres remotes comparten React como singleton, ya no son cuatro aplicaciones que se despliegan de forma verdaderamente independiente: son cuatro aplicaciones atadas por un tratado tácito que dice que todas usarán una versión mutuamente compatible del framework. La negociación de versiones en runtime —elegir la más alta que satisfaga todos los rangos— es la maquinaria que mantiene ese tratado en pie, pero también revela su fragilidad: el día que un equipo necesite una major nueva e incompatible, descubrirá que la independencia que la federación prometía tenía una cláusula en letra pequeña, y que esa cláusula se llama shared. Dominar este campo no es memorizar sus opciones, sino entender que cada dependencia que compartes es una decisión de acoplamiento deliberado: ganas una instancia única y unos bytes, y pagas con un grado de coordinación que tendrás que gestionar durante toda la vida del sistema.
- Compón un host y un remote que usen React sin declararlo en
shared; provoca el error de hooks inválidos montando en el host un componente remoto con hooks. - Añade
reactyreact-domasharedconsingleton: trueen ambas apps y confirma que el error desaparece y que se descarga una sola copia. - Fuerza versiones distintas de React en host y remote y observa en consola el aviso de negociación y qué versión gana.
- Añade
strictVersion: truey comprueba que ahora una incompatibilidad falla en vez de avisar. - Marca una dependencia como
eagery mide en la pestaña de red cómo cambia el momento en que se descarga.