1) Introducción detallada

Hablar de micro-frontends en mobile obliga a redefinir expectativas. En la web, el concepto suele implicar autonomía de equipos, independencia de despliegue y composición en runtime (cargar partes de la UI desde distintos artefactos, potencialmente construidos y desplegados por separado). En React Native, sin embargo, el binario que llega al usuario (APK/AAB/IPA) es el “contenedor” final; su ciclo de distribución está mediado por tiendas, revisiones, firmas y políticas de seguridad. Eso introduce fricción directa contra el ideal de “deploy independiente”.

Aun así, sí es posible aplicar micro-frontends en mobile con React Native, pero con matices: no siempre vas a conseguir independencia de despliegue total; en muchos casos se busca micro-frontends como arquitectura de código y de equipos (modularidad fuerte, límites claros, ownership, contratos estables) y, cuando el negocio lo exige, se añade una capa de entrega remota (OTA) o carga dinámica de bundles para aproximarse a la independencia de release. El resultado suele ser una arquitectura híbrida: un host app que define navegación, sesión, observabilidad y dependencias comunes; y features (micro-apps) empaquetadas como módulos aislados que pueden integrarse estáticamente o, en escenarios más avanzados, descargarse bajo demanda.

El punto crítico es distinguir entre: (a) micro-frontends como composición en tiempo de ejecución (más cercano a web) y (b) micro-frontends como boundaries y governance dentro de un repositorio/organización, aunque el binario se distribuya como un todo. En mobile, la segunda aporta valor inmediato (velocidad de equipos, mantenibilidad, escalado), y la primera requiere un análisis riguroso de rendimiento, seguridad, compatibilidad, políticas de store y estrategia de actualizaciones.

En este artículo voy a ir directo: qué significa micro-frontend en React Native, qué enfoques existen hoy (monorepo modular, “Super App”, bundles remotos con Metro, CodePush/OTA, federación de módulos), cómo se construyen contratos y aislamiento real, y qué trade-offs técnicos y operativos vas a pagar para conseguirlo.

2) Contexto histórico o teórico

Los micro-frontends emergen como respuesta natural al éxito (y también a los límites) de los microservicios. Si el backend se dividió en dominios con ownership claro, el frontend monolítico quedó como cuello de botella: un único equipo “dueño” del SPA, ciclos de release coordinados, conflictos en dependencias y estilos, y una base de código que crece hasta hacerse inmanejable. En la web, la evolución de herramientas (Webpack 5 Module Federation, import maps, custom elements) hizo viable componer UI desde artefactos independientes en runtime.

En mobile nativo (iOS/Android) existe desde hace años una práctica parecida: modularización (frameworks en iOS, módulos Gradle en Android, dynamic features, App Bundles) y super apps (un contenedor que aloja “mini-apps”). Sin embargo, el “frontend” mobile tiene restricciones más duras que el navegador: el empaquetado final se firma, la ejecución está más controlada y el mecanismo de actualización depende del store. React Native añadió una tercera dimensión: un runtime JS que ejecuta lógica y UI declarativa; con ello nacen posibilidades (descargar JS) y riesgos (políticas de Apple, seguridad, compatibilidad de bridges y versiones del engine).

Teóricamente, un micro-frontend requiere: (1) límites de dominio (DDD aplicado a UI), (2) contratos entre host y feature (tipados, navegación, eventos), (3) aislamiento (dependencias, estilos, estado), y (4) ciclo de vida independiente (build/test/release). En React Native, el (4) se tensiona porque el binario incluye el runtime, librerías nativas y un bundle JS principal. Para conseguir composición dinámica se recurre a estrategias como bundle splitting, lazy loading, carga remota de JS, o incluso a federación de módulos. Cada una tiene implicaciones profundas en Metro, Hermes, la caché, el versionado de APIs nativas, el rendimiento de arranque y la observabilidad.

Por eso, cuando alguien pregunta “¿micro-frontends en React Native?”, lo correcto es devolver otra pregunta: ¿buscas independencia de equipos o independencia de despliegue? La primera se logra con modularización y governance; la segunda, sólo con un diseño muy cuidadoso de runtime y distribución (y aceptando limitaciones).

3) Sección técnica principal 1: Qué significa micro-frontend en React Native (y qué no)

En React Native, un “micro-frontend” no es simplemente un paquete npm con componentes. Si lo reduces a eso, te quedas en un nivel de reutilización, no de autonomía. Un micro-frontend móvil útil suele cumplir estas propiedades:

a) Boundary de dominio y ownership. Cada micro-frontend (feature) representa un subdominio: “Catálogo”, “Checkout”, “Perfil”, “Soporte”. El equipo es dueño del stack de esa feature: UI, estado local, lógica de negocio de presentación, tests, métricas. No significa que repliques infra común; significa que controlas el cambio dentro del boundary.

b) Contrato con el host. El host app define un “shell”: navegación global, autenticación, configuración remota, logging, crash reporting, theming base, traducciones, y un API de integración. La feature no debería importar directamente módulos internos del host; debería consumir un contrato estable: navegación (routes), servicios (fetch/GraphQL client), storage, analytics, y “capabilities” (cámara, pagos) expuestas como interfaces.

c) Aislamiento de dependencias. En web, un micro-frontend puede traer su propia versión de React (aunque no sea recomendable) y ejecutarse aislado en un iframe o mediante federation. En React Native, duplicar React/React Native es inviable; necesitas dependencias compartidas en el runtime. Aun así, puedes aislar dependencias de JS (utilidades, reducers, lógica) y minimizar acoplamiento con una capa de compatibilidad.

d) Aislamiento de estado. Una antiarquitectura común es un store global gigantesco (Redux) donde cada feature escribe y lee sin límites. En un enfoque micro-frontend, el store global debe ser mínimo (sesión, flags, configuración), y cada feature gestiona su estado interno. Si necesitas cross-feature, usa eventos o un “domain bus”, no lecturas arbitrarias de slices ajenos.

e) Build y test independientes. Aunque termines generando un único binario, cada feature debe poder buildarse/testearse en aislamiento: tests unitarios, contract tests contra el host, y build de paquete. Esto se logra con monorepo (Nx/Turborepo) o con repos separados y un sistema de integración (artifact registry).

Lo que no es micro-frontend en React Native: (1) “tengo un folder por feature”; (2) “tengo un design system compartido”; (3) “uso navigation y hago lazy load”. Eso es buena modularidad, pero no llega a micro-frontend si no hay contratos, ownership y disciplina de integración.

Por último, hay una restricción clave: lo nativo manda. Cualquier feature que dependa de un módulo nativo nuevo (SDK de pagos, cámara avanzada, mapas) obliga a tocar iOS/Android, regenerar binario y pasar por store. Ahí se rompe la independencia de despliegue. Esto define una regla práctica: micro-frontends con independencia real requieren que la mayoría de cambios sean JS-only o que el host ya incluya de antemano los módulos nativos necesarios (enfoque “capabilities preinstaladas”).

4) Sección técnica principal 2: Estrategias de composición: estática, dinámica y OTA

En mobile con React Native hay tres grandes estrategias de composición para aproximarte a micro-frontends. La elección depende de tus objetivos (time-to-market, independencia, compliance, performance) y de cuánto control tengas sobre el runtime.

1) Composición estática (modularización + build único). Es el enfoque más realista y frecuente. Las features viven como paquetes internos (workspaces) y se integran en el host en build time. Ventajas: simplicidad, rendimiento predecible, compatibilidad total con Hermes, Metro y bundling estándar. Contras: no hay despliegue independiente; el release sigue siendo del host. Aun así, puedes conseguir autonomía organizativa fuerte: cada equipo entrega su paquete, CI valida contratos, y el host integra versiones mediante versionado semántico.

2) Composición dinámica local (bundle splitting y carga diferida). Aquí sigues distribuyendo un único binario, pero no cargas todo el JS al arranque. Divides el código en “mini bundles” internos que se cargan cuando el usuario entra a la feature. React Native no ofrece de forma oficial un “split” como Webpack, pero hay técnicas: múltiples entry points, RAM bundles (histórico), o soluciones modernas basadas en Metro config y carga de bundle. Ventajas: reduce tiempo de arranque y memoria inicial; permite que las features evolucionen con menos fricción interna. Contras: complejidad alta, tooling no estándar, y cuidado extremo con navegación, prefetch y errores de carga.

3) Composición dinámica remota (descarga de JS en runtime, OTA). Esto es lo más cercano al micro-frontend web: el host descarga un bundle JS de una feature desde un CDN, lo valida, lo cachea y lo ejecuta. Se puede implementar con soluciones tipo CodePush (o equivalentes) para actualizar bundles, o con un loader custom para “mini apps”. Ventajas: independencia de despliegue (al menos para cambios JS), experimentación y rollback rápido. Contras: compliance (Apple), seguridad (integridad y autenticidad), compatibilidad de versiones del host/runtime, observabilidad, y riesgo de “brickear” UX si el bundle remoto falla.

En la práctica, una arquitectura madura mezcla (1) y (3): el host integra estáticamente la versión “base” de cada feature (para que la app funcione offline y sea revisable), y permite “parches” remotos JS-only si el contrato y las dependencias nativas no cambian. Es decir, independencia parcial, controlada.

Hay un principio operativo que evita muchos desastres: versiona el contrato del host y gobierna la compatibilidad. Si una feature remota depende de un API del host que cambió, debes impedir su carga (gating por versión), o aplicar un adaptador. Esto se parece más a compatibilidad de SDK que a simple import.

Finalmente, no ignores el “coste invisible”: en mobile, la composición dinámica agrega una dimensión SRE/DevOps: CDN, firma de artefactos, invalidación de caché, telemetría de carga, control de rollout por cohortes, y una estrategia de recuperación (fallback a bundle embebido).

5) Sección técnica principal 3: Arquitectura del host (shell) y contratos entre micro-frontends

Si quieres micro-frontends en React Native sin que se conviertan en un infierno, el host debe ser explícitamente un platform team artifact. No un “app” más. El host define el marco de ejecución y los contratos. Los contratos deben cubrir: navegación, theming, i18n, networking, autenticación, analytics, logging, feature flags y manejo de errores.

Contratos recomendados (mínimos):

a) Registro de features. Cada micro-frontend expone un descriptor con: nombre, versión, rutas/pantallas, puntos de entrada, permisos/capabilities, y un método de renderizado. El host lo consume para montar navegación y deep links. Esto evita que el host “conozca” internamente la estructura de la feature.

b) API de servicios. El host ofrece un objeto “platform” con dependencias estables: client HTTP (con interceptores), cliente GraphQL, storage seguro, logger, analytics, feature flags, y un bus de eventos. La feature no debería crear su propio client (si lo hace, pierdes coherencia de auth, cache y observabilidad).

c) Contrato de UI base. Design system y tokens. En micro-frontends, el look&feel debe ser coherente. La forma correcta es exponer tokens (colores, spacing, typography) y componentes base versionados. Evita que cada feature pinte su propio universo. Pero ojo: un design system acoplado sin versionado mata la autonomía. Solución: versionado semántico, changelog riguroso y deprecations.

d) Gestión de errores y boundaries. Cada micro-frontend debe renderizarse bajo un error boundary controlado por el host para evitar que un fallo tumbe toda la app. Además, define una política de degradación: pantalla fallback, reintento, reporte con contexto (feature + versión + host versión).

e) Compatibilidad de versiones. El host debe exponer su “API version” y la feature debe declarar el rango compatible. Si cargas bundles remotos, este punto es obligatorio. En integración estática, también: evita upgrades implícitos.

Una implementación robusta suele usar TypeScript para definir interfaces y un paquete @app/platform-contracts que ambos lados comparten. Esto no elimina el riesgo (dependencia compartida), pero centraliza el contrato. Alternativamente, puedes generar tipos desde OpenAPI/GraphQL o usar schema validation (Zod) para contratos runtime.

Otro tema crítico: navegación. React Navigation es el estándar. En micro-frontends, lo normal es que el host sea dueño del NavigationContainer y cada feature registre stacks internos. El host no debería importar pantallas concretas; debería pedirle a la feature “dame tu stack root” y montarlo en un navigator padre. Esto reduce acoplamiento, pero exige convención de rutas y deep links.

Finalmente, datos y cachés. Si usas React Query/Apollo, define si la caché es global (host) o por feature. Global da eficiencia y coherencia; por feature da aislamiento. Un compromiso habitual: cliente global (para auth/headers) + caches segmentadas por namespace para evitar colisiones.

6) Sección técnica principal 4: Tooling y empaquetado: Metro, Hermes, CodePush y federación

La parte difícil de micro-frontends en React Native no es el patrón de carpetas; es el empaquetado y el runtime.

Metro bundler compila un grafo de dependencias desde uno (o varios) entry points. Por defecto, React Native genera un único bundle JS. Si quieres micro-frontends dinámicos, necesitas responder a preguntas como: ¿cómo genero bundles separados? ¿cómo resuelvo dependencias compartidas? ¿cómo evito duplicar código? ¿cómo garantizo que el bundle remoto es compatible con la versión de RN/Hermes del host?

Hermes compila y ejecuta JS con optimizaciones específicas; el formato de bytecode puede variar entre versiones. Si piensas distribuir bytecode precompilado, la compatibilidad se vuelve estricta. Si distribuyes JS plano, Hermes lo compila en el dispositivo, con impacto en tiempo de carga (aunque cacheable). Aquí hay decisiones operativas: precompilar vs compilar on-device, estrategia de cache, y cuándo invalidar.

OTA (CodePush o equivalente) permite actualizar el bundle JS sin pasar por store (con matices de políticas; especialmente en iOS). Esto habilita “micro-frontends” en el sentido de deploy JS rápido. Pero no es magia: (1) no puedes cambiar código nativo; (2) debes manejar versionado; (3) debes tener rollback; (4) debes instrumentar errores por release; (5) debes respetar reglas de tienda (Apple rechaza apps que cambian comportamiento sustancial, aunque en la práctica depende del caso y cumplimiento).

Module Federation (mundo Webpack) no es nativo de Metro. Existen aproximaciones comunitarias, pero requieren mucha ingeniería y no siempre alcanzan la madurez/soporte de Web. Si tu objetivo es federación “tipo Webpack” en RN, asume riesgo: actualizaciones de RN/Metro pueden romper el setup. En entornos enterprise, suelo recomendar evitar apostar todo a una federación no estándar a menos que tengas equipo de plataforma capaz de mantenerlo.

Monorepo vs multi-repo. Para micro-frontends móviles, monorepo (Nx/Turborepo + Yarn/PNPM workspaces) es extremadamente práctico: versionado coordinado, refactors, sharing de tooling, y builds incrementales. Multi-repo da independencia organizativa, pero complica el “integration test” y la compatibilidad. Mi regla: monorepo para empezar y escalar; multi-repo sólo cuando el tamaño y la estructura orgánica lo exijan y tengas plataforma madura.

Build nativo. En Android puedes apoyarte en modularización Gradle y dynamic feature modules, pero en RN eso se mezcla con JS runtime. En iOS, frameworks estáticos/dinámicos y Swift Package Manager pueden modularizar nativo, pero no resuelve la composición de JS por sí sola. Lo que sí aportan es un camino para separar capabilities nativas y reducir conflictos entre equipos.

Conclusión de esta sección: el micro-frontend móvil real es un problema de plataforma (tooling, runtime, distribución). Si no inviertes en plataforma, terminarás con un “monolito distribuido” peor que el original.

7) Sección técnica principal 5: Seguridad, rendimiento, observabilidad y gobernanza

Cuando introduces micro-frontends (y más aún carga remota), introduces nuevas superficies de fallo. En mobile, el coste del fallo es alto: mala review, caída de conversión, crashes, y degradación de marca.

Seguridad:

a) Integridad del bundle. Si descargas JS remoto, debes verificar integridad (hash) y autenticidad (firma). Un enfoque serio incluye: manifiesto firmado (ej. Ed25519), bundles versionados, y validación antes de ejecutar. Sin esto, abres la puerta a MITM o manipulación si un eslabón del CDN se compromete.

b) Permisos/capabilities. No permitas que cualquier feature llame a cualquier API. Define un “capability model”: la feature declara qué necesita (cámara, localización, pagos) y el host decide. Esto es útil incluso sin carga remota: impone disciplina y auditabilidad.

c) Datos sensibles. Prohíbe que features gestionen credenciales directamente. El host debe ser dueño de sesión, tokens, secure storage y renovación. La feature pide “getAccessToken()” a través de contrato, no lee storage.

Rendimiento:

a) Tiempo de arranque. Micro-frontends mal implementados aumentan el bundle y el tiempo de parse/ejecución. Solución: lazy load real, evitar dependencias pesadas por feature, y medir TTI (time to interactive). Hermes ayuda, pero no hace milagros.

b) Memoria. Si cada feature trae librerías similares (date libs, charts), inflas memoria. Centraliza utilidades o define librerías aprobadas. En micro-frontends, la “independencia” no debe justificar duplicación masiva.

c) Navegación y transiciones. Montar stacks completos por feature puede crear montajes pesados. Usa screen-level code splitting (cuando sea viable) y prefetch de bundles al anticipar navegación (ej. al mostrar un CTA).

Observabilidad:

a) Trazabilidad por feature. Cada evento, error y métrica debe incluir featureName, featureVersion y hostVersion. Sin esto, la depuración se vuelve imposible.

b) Correlación. Si hay llamadas de red, correlaciona con un request-id. Si hay flujos críticos (checkout), instrumenta spans (OpenTelemetry si aplica) aunque sea a nivel conceptual (timings).

c) Rollouts. Si usas OTA o bundles remotos, necesitas rollouts por porcentaje, kill switch y rollback automático si suben crashes. Esto no es opcional.

Gobernanza:

a) Reglas de dependencia. Impón “dependency boundaries” con tooling (ESLint rules, Nx tags, path aliases restringidos). Evita imports cruzados entre features.

b) Contratos versionados. Deprecations, migraciones, documentación viva. El contrato del host es un producto. Sin disciplina de versionado, la autonomía se convierte en caos.

c) Calidad. Contract tests: el host ejecuta tests que validan que cada feature cumple el contrato (exporta entrypoint, rutas válidas, no usa APIs prohibidas). Esto es el equivalente a consumer-driven contracts en backend.

8) Ejemplos de código detallados

A continuación muestro un enfoque práctico: micro-frontends como paquetes (composición estática) + contrato de plataforma + posibilidad de carga diferida (lazy) y, opcionalmente, un “loader” para bundles remotos JS-only (conceptual).

8.1. Contrato de plataforma (TypeScript)

// packages/platform-contracts/src/index.ts

export type HostVersion = {
  appVersion: string;      // 6.12.0
  buildNumber: string;     // 1234
  rnVersion: string;       // 0.74.x
  hermes: boolean;
  platform: 'ios' | 'android';
};

export type Analytics = {
  track: (event: string, props?: Record<string, unknown>) => void;
  screen: (name: string, props?: Record<string, unknown>) => void;
};

export type Logger = {
  info: (msg: string, meta?: Record<string, unknown>) => void;
  warn: (msg: string, meta?: Record<string, unknown>) => void;
  error: (msg: string, meta?: Record<string, unknown>) => void;
};

export type FeatureFlags = {
  get: (key: string) => boolean;
};

export type Auth = {
  getAccessToken: () => Promise<string>;
  isLoggedIn: () => Promise<boolean>;
};

export type PlatformAPI = {
  host: HostVersion;
  analytics: Analytics;
  logger: Logger;
  flags: FeatureFlags;
  auth: Auth;
  // Puedes añadir: storage, httpClient, navigation helpers, etc.
};

export type FeatureDescriptor = {
  name: string;              // 'catalog'
  version: string;           // '1.4.2'
  compatibleHostApi: string; // '^1.0.0'
  routes: Array<{
    name: string;            // 'CatalogHome'
    path?: string;           // 'catalog'
  }>;
  getRoot: (api: PlatformAPI) => React.ComponentType<unknown>;
};

8.2. Implementación de una feature como paquete aislado

// packages/feature-catalog/src/index.ts
import type { FeatureDescriptor, PlatformAPI } from '@app/platform-contracts';
import React from 'react';
import { CatalogRoot } from './ui/CatalogRoot';

export const descriptor: FeatureDescriptor = {
  name: 'catalog',
  version: '1.4.2',
  compatibleHostApi: '^1.0.0',
  routes: [{ name: 'CatalogHome', path: 'catalog' }],
  getRoot: (api: PlatformAPI) => {
    // Inyecta api vía contexto interno para que la feature no importe cosas del host.
    return function CatalogEntrypoint() {
      return <CatalogRoot api={api} />;
    };
  },
};
// packages/feature-catalog/src/ui/CatalogRoot.tsx
import React, { useEffect } from 'react';
import { View, Text, Button } from 'react-native';
import type { PlatformAPI } from '@app/platform-contracts';

export function CatalogRoot({ api }: { api: PlatformAPI }) {
  useEffect(() => {
    api.analytics.screen('CatalogHome', { feature: 'catalog', version: '1.4.2' });
  }, [api]);

  async function onPress() {
    const logged = await api.auth.isLoggedIn();
    api.logger.info('CTA pressed', { logged });
  }

  return (
    <View style={{ padding: 16 }}>
      <Text style={{ fontSize: 20, fontWeight: '600' }}>Catálogo</Text>
      <Button title="Acción" onPress={onPress} />
    </View>
  );
}

8.3. Host app: registro de features y montaje con navegación

// apps/host/src/features/registry.ts
import type { FeatureDescriptor } from '@app/platform-contracts';

// Composición estática: import directo (pero la feature está aislada por contrato)
import { descriptor as catalog } from '@app/feature-catalog';
import { descriptor as profile } from '@app/feature-profile';

export const features: FeatureDescriptor[] = [catalog, profile];
// apps/host/src/platform/createPlatformApi.ts
import type { PlatformAPI } from '@app/platform-contracts';

export function createPlatformApi(): PlatformAPI {
  return {
    host: {
      appVersion: '6.12.0',
      buildNumber: '1234',
      rnVersion: '0.74.3',
      hermes: true,
      platform: 'ios',
    },
    analytics: {
      track: (event, props) => {
        // integrar con Segment/GA/etc
      },
      screen: (name, props) => {
        // ...
      },
    },
    logger: {
      info: (msg, meta) => console.log(msg, meta),
      warn: (msg, meta) => console.warn(msg, meta),
      error: (msg, meta) => console.error(msg, meta),
    },
    flags: {
      get: (key) => {
        // remote config / feature flags
        return false;
      },
    },
    auth: {
      getAccessToken: async () => 'token',
      isLoggedIn: async () => true,
    },
  };
}
// apps/host/src/App.tsx
import React, { useMemo } from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { features } from './features/registry';
import { createPlatformApi } from './platform/createPlatformApi';

const Stack = createNativeStackNavigator();

export default function App() {
  const api = useMemo(() => createPlatformApi(), []);

  // Host monta pantallas a partir del descriptor.
  // En un caso real, mapearías routes a componentes dentro de cada feature.
  const CatalogRoot = features.find(f => f.name === 'catalog')!.getRoot(api);

  return (
    <NavigationContainer>
      <Stack.Navigator>
        <Stack.Screen name="Catalog" component={CatalogRoot} />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

8.4. Lazy load interno (composición diferida) sin romper contratos

// packages/feature-catalog/src/ui/CatalogRoot.tsx
import React, { Suspense } from 'react';
import { ActivityIndicator, View } from 'react-native';
import type { PlatformAPI } from '@app/platform-contracts';

const CatalogHome = React.lazy(() => import('./screens/CatalogHome'));

export function CatalogRoot({ api }: { api: PlatformAPI }) {
  return (
    <Suspense
      fallback={
        <View style={{ flex: 1, justifyContent: 'center' }}>
          <ActivityIndicator />
        </View>
      }
    >
      <CatalogHome api={api} />
    </Suspense>
  );
}

Esto no crea bundles separados por sí mismo (depende del bundler), pero sí establece la intención arquitectónica: pantallas pesadas no deberían ejecutarse al montar el root.

8.5. Loader conceptual para bundle remoto (JS-only) con manifiesto firmado

Este ejemplo es deliberadamente conceptual, porque la ejecución de bundles remotos en RN requiere implementación específica (y cuidado con políticas). La idea: descargar un manifiesto, validar firma, descargar el bundle, verificar hash, cachear y cargar.

// apps/host/src/remote/RemoteFeatureLoader.ts
import type { PlatformAPI, FeatureDescriptor } from '@app/platform-contracts';

type Manifest = {
  feature: string;
  version: string;
  compatibleHostApi: string;
  bundleUrl: string;
  sha256: string;
  signature: string; // firma del manifiesto
};

async function fetchManifest(url: string): Promise<Manifest> {
  const res = await fetch(url);
  if (!res.ok) throw new Error('Manifest download failed');
  return res.json();
}

function verifyManifestSignature(m: Manifest, publicKey: string): boolean {
  // Implementar verificación real (Ed25519/ECDSA).
  // Aquí se deja como placeholder.
  return Boolean(publicKey) && m.signature.length > 10;
}

async function downloadBundle(url: string): Promise<ArrayBuffer> {
  const res = await fetch(url);
  if (!res.ok) throw new Error('Bundle download failed');
  return res.arrayBuffer();
}

function verifySha256(_data: ArrayBuffer, _expected: string): boolean {
  // Calcular hash real y comparar.
  return true;
}

export async function loadRemoteFeature(
  api: PlatformAPI,
  manifestUrl: string,
  publicKey: string
): Promise<FeatureDescriptor> {
  const manifest = await fetchManifest(manifestUrl);

  if (!verifyManifestSignature(manifest, publicKey)) {
    api.logger.error('Invalid manifest signature', { feature: manifest.feature });
    throw new Error('Invalid signature');
  }

  // Gating por compatibilidad del contrato del host
  // (en real: usa semver.satisfies(manifest.compatibleHostApi, hostApiVersion))
  if (!manifest.compatibleHostApi.startsWith('^1.')) {
    throw new Error('Incompatible host API');
  }

  const bundle = await downloadBundle(manifest.bundleUrl);
  if (!verifySha256(bundle, manifest.sha256)) {
    throw new Error('Corrupted bundle');
  }

  // Aquí necesitarías un mecanismo RN para "evaluar" el bundle y obtener exports.
  // En arquitecturas reales se usa un runtime/bridge específico.
  // Se devuelve placeholder para ilustrar contrato.
  return {
    name: manifest.feature,
    version: manifest.version,
    compatibleHostApi: manifest.compatibleHostApi,
    routes: [],
    getRoot: () => () => null,
  };
}

El mensaje importante: la carga remota no es “hacer fetch y eval”. Necesitas: firma, hash, cache, gating por versión, reporting, y un runtime de carga compatible con RN/Hermes.

9) Comparativa de pros y contras

Pros (cuando se implementa bien):

1) Escalado de equipos: ownership por dominio reduce bloqueos y conflictos. Cada equipo entrega valor sin pisar a otros.

2) Mantenibilidad: límites claros reducen deuda accidental. Refactors dentro de una feature afectan menos al resto.

3) Time-to-market: con contratos y CI, integrar cambios se vuelve más mecánico, menos heroico.

4) Observabilidad por dominio: si etiquetas métricas por feature, puedes priorizar optimizaciones y detectar regresiones con precisión.

5) Independencia parcial de despliegue (JS-only): con OTA/carga remota, puedes corregir bugs sin esperar al ciclo de store (aceptando restricciones).

Contras (costes reales):

1) Complejidad de plataforma: necesitas tooling, reglas de dependencia, versionado de contratos y soporte interno. Sin platform team, se degrada rápido.

2) Independencia de despliegue limitada: cualquier cambio nativo obliga a release de binario. Muchas features reales dependen de SDKs nativos.

3) Riesgo de duplicación: librerías repetidas, estilos inconsistentes, patrones divergentes. Requiere governance fuerte.

4) Rendimiento: composición dinámica mal hecha aumenta latencia, consumo y fallos por red. Necesitas estrategia de cache/prefetch.

5) Debugging y soporte: con bundles remotos y múltiples versiones en campo, el soporte se complica: necesitas correlación y capacidad de rollback inmediata.

6) Compliance: OTA y cambios dinámicos pueden chocar con políticas de Apple según el caso de uso. No lo asumas como “gratis”.

10) Conclusión

Sí, micro-frontends en mobile con React Native son posibles, pero no con la misma semántica “pura” que en web. Si tu objetivo principal es escalar equipos y código, la vía sólida es micro-frontends como modularización estricta: paquetes por dominio, contratos de plataforma, reglas de dependencia, y un host que actúa como shell. Esto aporta beneficios inmediatos sin pelearte contra el modelo de distribución móvil.

Si además necesitas independencia de despliegue, puedes aproximarte mediante OTA y/o carga remota de bundles JS-only, pero entonces el problema deja de ser “frontend” y se convierte en ingeniería de plataforma: seguridad criptográfica de artefactos, compatibilidad por versiones, observabilidad por release, rollouts controlados, fallbacks y cumplimiento.

Mi recomendación práctica: empieza por composición estática bien gobernada (contratos + boundaries + CI), mide cuellos de botella, y sólo después evalúa carga remota en un subconjunto de features con cambios principalmente JS. La arquitectura micro-frontend en React Native no es un fin: es una inversión para que el producto pueda crecer sin que el binario y el equipo se conviertan en un monolito inmanejable.