Gestión de Estado Federado Bien Hecha: Zustand, TanStack Query y los Patrones Que Realmente Funcionan · NextSteps Blog
module-federationreactmicro-frontends
Gestión de Estado Federado Bien Hecha: Zustand, TanStack Query y los Patrones Que Realmente Funcionan
Martin Rojas
♦♦13 min read
Patrones de Zustand y TanStack Query que realmente funcionan en Module Federation. Configuración singleton, compartición de caché y estado de micro-frontends probado en producción.
Todos hemos pasado por esto: configuras Module Federation, divides tu aplicación en micro-frontends, y de repente tu store de Zustand se actualiza en un módulo pero no en otro. O peor aún—tu caché de TanStack Query obtiene el mismo perfil de usuario tres veces porque cada remoto piensa que está solo en el mundo.
Los patrones que funcionan perfectamente en aplicaciones React monolíticas se rompen en arquitecturas federadas. Los Context providers no cruzan los límites de los módulos. Los stores se instancian dos veces. La invalidación de caché se convierte en un problema de sistemas distribuidos para el que no te inscribiste.
Esta guía cubre los patrones que realmente funcionan en producción—configuración singleton que previene instancias duplicadas, estrategias de compartición de caché que no crean acoplamiento estrecho, y la separación crítica entre estado del cliente (Zustand) y estado del servidor (TanStack Query) que hace que las aplicaciones federadas sean mantenibles. No son recomendaciones teóricas; son lecciones de equipos en Zalando, PayPal y otras organizaciones ejecutando Module Federation a escala.
Por Qué el Estado Federado Se Rompe: El Problema del Modelo de Memoria
En una SPA monolítica, la memoria es un recurso contiguo y compartido. Un store de Redux o un proveedor de Context instanciado en la raíz es universalmente accesible. En un sistema federado, la aplicación se compone de bundles de JavaScript distintos—frecuentemente desarrollados por equipos diferentes, desplegados en momentos diferentes, y cargados asincrónicamente en tiempo de ejecución. Estos bundles se ejecutan dentro de la misma pestaña del navegador, pero están separados por alcances de cierre distintos y árboles de dependencias.
La causa raíz de la mayoría de problemas de estado en Module Federation es engañosamente simple: sin configuración singleton explícita, cada módulo federado obtiene su propia instancia de React, Redux o Zustand. Los usuarios experimentan esto como autenticación que funciona en una sección pero no en otra, toggles de tema que afectan solo parte de la interfaz, o items del carrito que desaparecen al navegar entre micro-frontends.
El Motor de Share Scope
El motor que impulsa la compartición de estado es el objeto global __webpack_share_scopes__—una API interna de Webpack que actúa como registro para todos los módulos compartidos en la sesión del navegador.
Cuando la aplicación Host se inicializa, inicializa entradas en __webpack_share_scopes__.default para cada biblioteca marcada como shared. Cada entrada contiene el número de versión y la función factory para cargar el módulo. Cuando una aplicación Remote se inicializa, realiza un handshake: inspeccionando el share scope, comparando las versiones disponibles contra sus requisitos, y usando resolución de versionado semántico para determinar compatibilidad.
Si el Host provee React 18.2.0 y el Remote requiere ^18.0.0, el runtime determina compatibilidad y el Remote usa la referencia del Host. Esta "Compartición de Referencias" asegura que cuando el Remote llama React.useContext, accede exactamente al mismo Context Registry que el Host. Si este handshake falla, el Remote carga su propio React, creando un universo paralelo donde los providers del Host no existen.
Configuración para Aplicación de Singleton
La solución requiere configuración singleton explícita en webpack:
Tres propiedades de configuración importan más: singleton: true asegura que solo exista una instancia a través de todos los módulos federados, strictVersion: true lanza errores cuando las versiones entran en conflicto en lugar de cargar silenciosamente duplicados, y requiredVersion con rangos semver explícitos previene fallas de despliegue. Cargar dinámicamente las versiones desde package.json asegura que la configuración coincida con los paquetes instalados.
El Patrón de Límite Asíncrono
El error "Shared module is not available for eager consumption" afecta a las nuevas configuraciones de Module Federation. Los puntos de entrada estándar importan React sincrónicamente, pero los módulos compartidos se cargan asincrónicamente. El runtime no ha inicializado el share scope antes de que se ejecute la importación.
Configurar eager: true fuerza la biblioteca al bundle inicial, solucionando el error pero agregando ~100-150KB gzipped al Time to Interactive. La mejor práctica arquitectónica es establecer un límite asíncrono:
// index.js - Wrapper simple que habilita carga asíncrona
import('./bootstrap');
// bootstrap.js - El punto de entrada real de tu aplicación
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'));
Cuando Webpack ve import('./bootstrap'), crea una promesa. Durante la resolución, el runtime de Federation inicializa __webpack_share_scopes__, verifica los puntos de entrada remotos, y asegura que las dependencias compartidas estén listas. Para cuando bootstrap.js se ejecuta, React está disponible en el shared scope.
Estado del Cliente: Zustand vs Redux en Arquitecturas Federadas
Zustand ha emergido como la biblioteca de estado del cliente preferida para micro-frontends debido a su tamaño de bundle de 1KB, arquitectura amigable con singleton, y ausencia de requisitos de jerarquía de providers. A diferencia de Redux o Context, un store de Zustand no requiere envolver componentes en providers que deben alinearse a través de los límites de módulos.
El Bug de "Solo Valor Inicial"
Los desarrolladores que migran a arquitecturas federadas frecuentemente encuentran un bug desconcertante: la aplicación Remote renderiza el estado inicial correctamente, pero cuando el Host actualiza el estado, el Remote falla al re-renderizar. Parece "atascado" en el valor inicial.
Esto resalta la diferencia entre compartir código y compartir instancias. Si tanto Host como Remote importan un store vía alias de build-time, el bundler incluye el código en ambos bundles. En runtime, el Host crea Store_Instance_A, el Remote crea Store_Instance_B. El Host actualiza la Instancia A; el Remote escucha la Instancia B. No se propaga ninguna actualización.
El Patrón de Store Exportado
Para garantizar sincronización, asegura que el mismo objeto JavaScript en memoria sea usado por Host y Remote:
El Remote importa desde el namespace de federation, no desde la biblioteca local:
// remote/src/App.tsx
import { useCartStore } from 'host/CartStore';
export const RemoteApp = () => {
const items = useCartStore((state) => state.items);
return <div>{items.length} items en el carrito</div>;
};
Cuando remote/App.tsx importa host/CartStore, Webpack delega la carga al runtime de Federation, que retorna la referencia del módulo que el Host ya cargó. El useCartStore se refiere al cierre exacto creado en el Host.
El Patrón de Inyección de Dependencias para Redux
Para arquitecturas basadas en Redux con requisitos de Provider, envolver Remotes en su propio <Provider store={store}> crea peligrosos providers anidados. Un patrón más limpio es la inyección de dependencias:
// El Remote declara el store como un contrato de prop
interface Props {
store: StoreType;
}
const RemoteWidget = ({ store }: Props) => {
const state = useStore(store);
return <div>{state.value}</div>;
};
export default RemoteWidget;
// El Host pasa su instancia de store
const RemoteWidget = React.lazy(() => import('remote/Widget'));
const App = () => {
return (
<Suspense fallback="Cargando...">
<RemoteWidget store={myStore} />
</Suspense>
);
};
Esto desacopla al Remote de la ubicación del store, habilita testing con stores simulados, y asegura que la propiedad permanezca con el Host.
Estado del Servidor: Estrategias de Compartición de Caché
TanStack Query v5 cambió fundamentalmente la compartición de caché al remover el prop contextSharing. El enfoque actual requiere compartición explícita de QueryClient vía exposes de Module Federation.
Ventajas: Deduplicación instantánea de caché—si el Host obtiene ['user', 'me'] y el Remote necesita los mismos datos, TanStack Query los sirve desde caché sin una solicitud de red. La invalidación global significa que las mutaciones en un módulo actualizan todos los consumidores.
Riesgo: Esto requiere compartición exitosa de Context. Si las instancias de React difieren, el Remote lanza errores "No QueryClient set".
Cada Remote crea su propio QueryClient, asegurando verdadera independencia.
Ventajas: Los Remotes pueden usar diferentes versiones de React Query. Un crash en la caché del Host no afecta los Remotes.
Desventajas: Tanto Host como Remote obtienen los mismos datos (doble tráfico de red). Si el Remote actualiza datos de usuario, la caché del Host permanece desactualizada hasta recargar.
Característica
QueryClient Compartido
QueryClient Aislado
Reutilización de Datos
Alta (hits de caché instantáneos)
Baja (depende de caché HTTP)
Acoplamiento
Estrecho (debe compartir React)
Suelto (instancias independientes)
Invalidación
Global (una mutación actualiza todo)
Local (sincronización manual requerida)
Robustez
Frágil (problemas de contexto fatales)
Robusta (fail-safe)
Usar Cuando
MFEs internos y confiables
3rd-party o dominios distintos
Invalidación de Caché Entre MFEs
Para cachés distribuidas que requieren coordinación, usa BroadcastChannel:
El Paradigma Emergente: Bases de Datos Local-First
TanStack DB, actualmente en beta, representa un cambio de paradigma de "Cachear Respuestas de API" a "Sincronizar una Réplica de Base de Datos". Es particularmente transformador para arquitecturas distribuidas porque resuelve la sincronización a nivel de protocolo.
El estado se organiza en Collections tipadas en lugar de claves string arbitrarias, actuando como contratos rígidos. Múltiples MFEs pueden consultar el mismo dataset con diferentes filtros sin coordinar quién obtiene qué—la base de datos actúa como el hub central de estado.
Para sincronización multi-pestaña, el plugin broadcastQueryClient de TanStack Query provee un puente:
Arquitectura Dirigida por Eventos: Cuando el Estado No Es la Respuesta
No toda comunicación requiere estado compartido. Para interacciones transaccionales "fire-and-forget", los eventos reducen el acoplamiento:
// Event bus compartido
export const authChannel = new BroadcastChannel('auth_events');
export const sendLogout = () => {
authChannel.postMessage({ type: 'LOGOUT' });
};
// Para comunicación dentro del mismo documento, usa CustomEvent
window.dispatchEvent(new CustomEvent('cart:open', { detail: { productId } }));
Los CustomEvents aprovechan el event loop nativo del navegador con cero dependencias de bibliotecas—ideal para señales de UI como un botón "Comprar Ahora" disparando un drawer de carrito.
Hardening de Producción
Manejo de Errores de Tres Capas
El RetryPlugin de Module Federation 2.0 maneja fallas de red transitorias:
Combina con hooks errorLoadRemote para fallbacks de carga y React Error Boundaries para errores de runtime.
Prevención de Memory Leaks
En arquitecturas federadas, los MFEs se montan y desmontan mientras los usuarios navegan. Los listeners registrados en stores globales o event buses que no se limpian se acumulan, causando llamadas de API duplicadas y comportamiento impredecible. Linting estricto que exija funciones de limpieza en useEffect es obligatorio.
Equipos pequeños (2-5 desarrolladores): Usa Zustand + TanStack Query como singletons. Compartición build-time vía paquetes internos de Turborepo. Module Federation agrega complejidad innecesaria a esta escala.
Equipos medianos (5-15 desarrolladores): Enfoque híbrido—paquetes compartidos para funcionalidad core (auth, sesión), Module Federation para módulos de features. Invierte en estrategia de manejo de errores de tres capas.
Organizaciones grandes (múltiples equipos): Interfaces basadas en props entre MFEs, autenticación centralizada vía shell, políticas estrictas de alineación de versiones. Depurar problemas de estado distribuido excede el overhead de coordinación.
La regla esencial: Nunca almacenes datos del servidor en bibliotecas de estado del cliente. TanStack Query maneja caching, refetching e invalidación—duplicar en Zustand crea bugs de sincronización.
Checklist de Producción
Patrón de límite asíncrono (import('./bootstrap')) en cada punto de entrada de módulo federado
Configuración singleton para React, react-dom, y todas las bibliotecas de estado con requiredVersion explícito
Lógica de estado extraída a paquetes libs/data-access compartidos
Host expone instancias de estado; Remotes consumen vía import from 'host/Store'
Todos los event listeners retornan funciones de limpieza en useEffect
Invalidación basada en BroadcastChannel para coordinación entre MFEs
Manejo de errores de tres capas (retry, fallback, boundary)
Resoluciones de pnpm/yarn exigiendo versiones idénticas de bibliotecas