Una paleta de design system que alimenta tres paneles de apps federadas

El design system como singleton federado en React Native

El post 10 terminó con una promesa: el design system se convierte en un singleton federado, un solo paquete de UI compartido en runtime, para que cada remote renderice los mismos componentes sin cargar con su propia copia. Este post la cumple.

La deuda es real. Desde el post 4, cada pantalla ha definido sus propios estilos con StyleSheet.create y hexadecimales escritos a mano, y la duplicación ya abarca tres apps y el paquete de vista: los mismos grises de plantilla se repiten en la list, en la party y en la vista de detalle, y el mismo azul de acento aparece tanto en el botón de Refresh del host como en el de Add del detail. Cuatro bases de código con cuatro copias de las mismas decisiones de diseño, y ningún sitio donde tocarlas una sola vez.

La solución aplica a los píxeles dos patrones que la serie ya conoce. El post 3 compartió librerías como singletons para que una sola copia sirva a todas las apps. Los posts 5 y 6 llevaron paquetes a la frontera para que cada lado instale una única definición.

@pokedex/ui cumple las dos funciones: empaqueta los componentes copy-in de gluestack-ui, les aplica estilos con NativeWind 4.2.6 y los publica en el registro local de Verdaccio. Después, Module Federation entrega a cada remote la copia del host en runtime. Esta build usa @gluestack-ui/core en ^3.0.25, @gluestack-ui/utils en ^3.0.21 y Tailwind 3.4.19.

En la frontera ya hay tres paquetes, y cada uno la cruza de una manera. Los contratos y el design system se resuelven a una única copia en runtime, y así lo representa el diagrama: es el host quien los proporciona. El paquete detail no se comparte de esa forma, y la decisión es deliberada: cada consumidor conserva su propia versión fijada, que es justo para lo que se diseñó en el post 5. Ese contraste es el núcleo de este post.

Un paquete para los píxeles

Empieza desde el tag del post 10:

git clone https://github.com/warrendeleon/react-native-module-federation
cd react-native-module-federation
git checkout post-10-two-backends

Conviene ser sincero antes de entrar en la anatomía: el código del design system no cabe en un post. Son trece componentes de gluestack copiados, mapas de tokens para dieciocho tipos y once componentes compuestos. Por eso los paquetes de este post se copian del tag final en lugar de escribirse a mano, y lo mismo vale para las versiones nuevas de contracts y detail, que este post también amplía (El detail estrena diseño explica en qué consisten). Descarga una sola vez la copia de referencia (Ejecútalo vuelve a usarla para los recursos nativos) y copia los tres paquetes:

npx degit@3.8.0 --force warrendeleon/react-native-module-federation#post-11-design-system /tmp/pokedex-ref
rm -rf packages/ui packages/contracts packages/detail
cp -R /tmp/pokedex-ref/packages/ui /tmp/pokedex-ref/packages/contracts /tmp/pokedex-ref/packages/detail packages/

packages/ui sigue la anatomía que fijaron los posts 5 y 6: react-native-builder-bob genera un lib/ en ESM, "react-native": "./src/index.ts" expone el código sin compilar para que Re.Pack compile exactamente los archivos que hay en el repo, publishConfig apunta al Verdaccio local y la lista files incorpora dos entradas que ningún paquete anterior había necesitado: tailwind.preset.js y global.css.

La CLI de gluestack copia el código de los componentes en src/components/ui/. Copy-in significa que el código pasa a ser tuyo: es tu paquete el que lo publica, y las actualizaciones de upstream no llegan solas. El repo de acompañamiento conserva las trece primitivas que usan estas pantallas, de box a vstack.

Una de esas copias arrastra una dependencia que nadie declara. heading importa @expo/html-elements, y el paquete generado no lo incluye en dependencies. Añádelo tú: si no, la instalación se completa sin quejarse y el fallo aparece más tarde, cuando el bundler del consumidor llega a ese import y no encuentra el módulo.

Por encima de las primitivas están los tokens y los componentes compuestos. tailwind.preset.js define los colores de marca y los neutros, además de type.<name> para los dieciocho tipos de Pokémon. Esos mismos valores están también en src/tokens/ para el código de runtime. Los usan once componentes: ScreenContainer, PokemonCard, TypeBadge, LoadingState, ErrorState, InfoRow, StatBar, EmptySlot, BackPill, ThemeToggle y Toaster. Un barrel, primitives.ts, hace que los consumidores importen todo desde un único punto.

Hay dos decisiones que corresponden a los tokens, y dejarlas en manos de los componentes provoca un fallo silencioso.

Solo se generan las clases escritas como strings literales. Un `bg-type-${t}` construido al vuelo es invisible para el escáner de Tailwind, y el color no llega nunca a la app sin que salte ningún aviso. El archivo de tokens declara cada clase en mapas literales (TYPE_BG_CLASS y compañía) y expone funciones de consulta, de modo que toda la paleta queda en un único sitio auditable.

La segunda es el contraste del texto. Los colores de tipo necesitan un color de texto medido, no elegido a ojo. El blanco sobre el azul del tipo agua mide 3,48:1, y sobre el rosa del tipo psíquico, 3,41:1: por debajo del mínimo de 4,5:1 que las WCAG piden para texto pequeño. El negro supera 6:1 en los dos casos. textOnTypeClass() calcula una sola vez cuál de los dos contrasta más, y cada superficie coloreada por tipo utiliza el resultado. Con un negro fijado a mano, el hero del tipo lucha se quedaría en 1,7:1.

Las imágenes son la excepción a todo este copiado de código fuente. La luna y el sol del ThemeToggle y la flecha del BackPill son archivos PNG en src/assets, que llegaron con la copia, y la lista files los incluye en el paquete publicado.

Publícalo igual que en los posts 5 y 6, sin saltarte el npm install:

( cd packages/ui && npm install && npm run build && npm publish )

La versión 1.0.2 es la API 1.0.0 que estaba prevista, más los parches que añadieron las revisiones posteriores.

El detail estrena diseño

La versión definitiva del detail necesita datos que el contrato anterior no incluía. Contracts 3.2.2 añade heightM, weightKg, abilities, stats y un flavourText opcional a PokemonDetail. También añade types a PartyMember, para que la party pueda colorear sus tarjetas sin lanzar otra petición. parsePokemonDetail convierte las unidades de la PokéAPI y normaliza el flavour text una sola vez, en la frontera.

Los dos remotes piden ahora el payload de especie junto con el del Pokémon, y la list envía los datos de tipo al añadir un miembro. La pasada de archivos de Configurar un runtime de estilos en una build federada incorpora esos cambios. El código de los paquetes nuevos ya venía en la copia de referencia, así que publica contracts y detail antes de que las apps los instalen:

( cd packages/contracts && npm install && npm run build && npm publish )
( cd packages/detail && npm install && npm run build && npm publish )

@pokedex/detail 4.0.2 rehace la vista sobre el design system: un hero plegable con el color del tipo, el sprite dentro de un anillo, el flavour text, las filas de Info, las Base Stats y el botón de Add. Es una major porque añade peer dependencies: @pokedex/ui, NativeWind y Reanimated pasan a ser obligatorios para cualquier consumidor.

Las props no cambian. La vista sigue aceptando { pokemon, loading, error, onRetry, onAddToParty, addDisabled, addLabel }, así que a los dos contenedores les basta con actualizar la versión. El cambio visual más grande de la serie se aplica con un npm install.

La pantalla de detalle de Pokémon terminada: un hero verde hierba con el sprite en un anillo translúcido sobre un número 001 semitransparente al fondo, el nombre de Bulbasaur y sus insignias de tipo, luego el flavour text en una tarjeta a modo de cita, filas de Info y barras de Base Stats en tarjetas flotantes

El plegado es el gesto característico de la pantalla, y vale la pena verlo antes de la sección de configuración, porque todos sus colores son clases de token que se resuelven en la frontera:

Animado: el hero verde hierba del detail se va con el scroll mientras el contenido sube, y se asienta en una barra de título compacta que dice Bulbasaur

Configurar un runtime de estilos en una build federada

La documentación de NativeWind cubre Metro y sitúa Re.Pack entre las soluciones de la comunidad. Callstack, por su parte, mantiene un plugin propio, @callstack/repack-plugin-nativewind, con la versión ligada al Re.Pack instalado (aquí, la 5.2.5). Reanimated necesita el mismo trato: Re.Pack detecta el paquete y avisa cuando falta ReanimatedPlugin, así que @callstack/repack-plugin-reanimated entra a su lado con la misma versión. Las dos cosas son ciertas, y la configuración que viene a continuación es la que documenta el propio plugin.

Las tres apps reciben las dependencias de estilos. Los dos remotes instalan además detail 4.0.2 y contracts 3.2.2. Todas declaran gluestack core y utils de forma explícita, porque el código copiado los importa. Y todas fijan react y react-dom a su propia versión de React, la 19.2.3, para que la resolución de peers no acabe instalando un segundo React.

( cd apps/host && npm pkg set overrides.react=19.2.3 overrides.react-dom=19.2.3 && npm install nativewind@4.2.6 @gluestack-ui/core@3.0.25 @gluestack-ui/utils@3.0.21 react-native-reanimated@4.6.0 react-native-worklets@0.12.1 @pokedex/ui@1.0.2 @pokedex/contracts@3.2.2 && npm install -D tailwindcss@3.4.19 @callstack/repack-plugin-nativewind@5.2.5 @callstack/repack-plugin-reanimated@5.2.5 postcss@8.4.47 postcss-loader@8.1.1 autoprefixer@10.4.20 @module-federation/enhanced@2.9.0 @react-native-community/cli@20.2.0 @react-native-community/cli-platform-android@20.2.0 @react-native-community/cli-platform-ios@20.2.0 )
( cd apps/list && npm pkg set overrides.react=19.2.3 overrides.react-dom=19.2.3 && npm install nativewind@4.2.6 @gluestack-ui/core@3.0.25 @gluestack-ui/utils@3.0.21 react-native-reanimated@4.6.0 react-native-worklets@0.12.1 @pokedex/ui@1.0.2 @pokedex/contracts@3.2.2 @pokedex/detail@4.0.2 && npm install -D tailwindcss@3.4.19 @callstack/repack-plugin-nativewind@5.2.5 @callstack/repack-plugin-reanimated@5.2.5 postcss@8.4.47 postcss-loader@8.1.1 autoprefixer@10.4.20 @module-federation/enhanced@2.9.0 @react-native-community/cli@20.2.0 @react-native-community/cli-platform-android@20.2.0 @react-native-community/cli-platform-ios@20.2.0 )
( cd apps/party && npm pkg set overrides.react=19.2.3 overrides.react-dom=19.2.3 && npm install nativewind@4.2.6 @gluestack-ui/core@3.0.25 @gluestack-ui/utils@3.0.21 react-native-reanimated@4.6.0 react-native-worklets@0.12.1 @pokedex/ui@1.0.2 @pokedex/contracts@3.2.2 @pokedex/detail@4.0.2 && npm install -D tailwindcss@3.4.19 @callstack/repack-plugin-nativewind@5.2.5 @callstack/repack-plugin-reanimated@5.2.5 postcss@8.4.47 postcss-loader@8.1.1 autoprefixer@10.4.20 @module-federation/enhanced@2.9.0 @react-native-community/cli@20.2.0 @react-native-community/cli-platform-android@20.2.0 @react-native-community/cli-platform-ios@20.2.0 )

El bloque overrides es la razón de que esos installs empiecen con npm pkg set. gluestack arrastra react-native-web y react-dom como peers transitivos, y sus rangos de react chocan con el react@19.2.3 fijado de la app. Fijar los dos en overrides lo resuelve. Recurrir a --legacy-peer-deps esconde justo los desajustes de versión que un singleton compartido necesita que veas.

Los últimos cuatro pins actualizan @module-federation/enhanced y el trío de la CLI de React Native (el paquete central y los dos de plataforma) a sus versiones actuales, con lo que desaparecen los avisos de npm audit que este stack puede resolver; el README del repo documenta los que quedan y por qué. react-native-reanimated y react-native-worklets incluyen código nativo, así que hay que actualizar los pods del host antes de la build de Ejecútalo: ( cd apps/host/ios && bundle install && bundle exec pod install ), la misma convención que sigue el README del repo. En Quién comparte qué se explica por qué están aquí react-native-reanimated y react-native-worklets. Fija tailwindcss por debajo de la 4: el runtime de estilos de NativeWind 4, react-native-css-interop, declara una dependencia de Tailwind 3, y el latest de npm es un 4.x que aquí no funciona.

La configuración de cada app, los archivos de entrada y las pantallas rehechas salen de la copia de referencia. Con esta pasada, tu árbol queda igual que el del tag:

for app in host list party; do
  cp /tmp/pokedex-ref/apps/$app/babel.config.js \
     /tmp/pokedex-ref/apps/$app/global.css \
     /tmp/pokedex-ref/apps/$app/index.js \
     /tmp/pokedex-ref/apps/$app/jest.config.js \
     /tmp/pokedex-ref/apps/$app/rspack.config.mjs \
     /tmp/pokedex-ref/apps/$app/tailwind.config.js \
     /tmp/pokedex-ref/apps/$app/tsconfig.json apps/$app/
done
cp /tmp/pokedex-ref/apps/host/App.tsx /tmp/pokedex-ref/apps/host/mf-modules.d.ts apps/host/
cp /tmp/pokedex-ref/apps/host/__mocks__/react-native-reanimated.js /tmp/pokedex-ref/apps/host/__mocks__/partyApp-styles.js apps/host/__mocks__/
cp /tmp/pokedex-ref/apps/host/__tests__/RemoteBoundary.test.tsx apps/host/__tests__/
cp /tmp/pokedex-ref/apps/list/src/ListStack.tsx /tmp/pokedex-ref/apps/list/src/PokedexScreen.tsx /tmp/pokedex-ref/apps/list/src/detailApi.ts /tmp/pokedex-ref/apps/list/src/typesApi.ts apps/list/src/
mkdir -p apps/list/__mocks__
cp /tmp/pokedex-ref/apps/list/__mocks__/react-native-reanimated.js /tmp/pokedex-ref/apps/list/__mocks__/styleMock.js apps/list/__mocks__/
cp /tmp/pokedex-ref/apps/list/__tests__/addGate.test.tsx /tmp/pokedex-ref/apps/list/__tests__/doubleTapAdd.test.tsx /tmp/pokedex-ref/apps/list/__tests__/parsePokemonDetail.test.ts /tmp/pokedex-ref/apps/list/__tests__/parsePokemonTypes.test.ts apps/list/__tests__/
cp /tmp/pokedex-ref/apps/party/src/PartyScreen.tsx /tmp/pokedex-ref/apps/party/src/PartyStack.tsx /tmp/pokedex-ref/apps/party/src/detailApi.ts /tmp/pokedex-ref/apps/party/src/styles.ts apps/party/src/
cp /tmp/pokedex-ref/apps/party/__tests__/partyStateReady.test.ts /tmp/pokedex-ref/apps/party/__tests__/partySelfRecovery.test.tsx /tmp/pokedex-ref/apps/party/__tests__/removeAnimatesBeforeCommit.test.tsx apps/party/__tests__/
mkdir -p apps/party/__mocks__
cp /tmp/pokedex-ref/apps/party/__mocks__/react-native-reanimated.js apps/party/__mocks__/
cp -R /tmp/pokedex-ref/scripts .
cp /tmp/pokedex-ref/README.md .

Babel incorpora el origen JSX de NativeWind, la configuración de Rspack incorpora el plugin, y un global.css de tres líneas contiene las directivas de Tailwind. El archivo interesante es tailwind.config.js:

module.exports = {
  presets: [require('nativewind/preset'), require('@pokedex/ui/tailwind.preset.js')],
  darkMode: 'class',
  content: [
    './App.tsx',
    './src/**/*.{js,jsx,ts,tsx}',
    path.join(path.dirname(require.resolve('@pokedex/ui/package.json')), 'src/**/*.{js,jsx,ts,tsx}'),
    path.join(path.dirname(require.resolve('@pokedex/detail/package.json')), 'src/**/*.{js,jsx,ts,tsx}'),
  ],
  theme: { extend: {} },
  plugins: [],
};

Ese archivo y los que lo rodean concentran tres advertencias, y pasar por alto cualquiera de ellas provoca un fallo silencioso.

Escanea el código de los paquetes. Tailwind solo genera una clase cuando ve el string literal, y las clases del design system están en node_modules. Los globs de content tienen que incluir el código publicado de @pokedex/ui y @pokedex/detail, localizado con require.resolve. Si te lo saltas, el fallo parece intermitente: las clases que la app también usa siguen funcionando, y solo desaparecen las que son exclusivas de la librería.

Importa global.css en cada módulo expuesto, no solo en la entrada. La federación carga el módulo expuesto, nunca el index.js propio del remote, así que un global.css importado solo en la entrada funciona en standalone y, federado, no hace nada ni avisa de nada. Por eso ListStack.tsx lo importa al principio del archivo, y la party llega a él a través del módulo que describe Los estilos se registran en el arranque.

darkMode: 'class' no es decorativo. Sin él, el setColorScheme de NativeWind lanza una excepción, y el cambio de tema de Un toggle, tres bundles depende de esa llamada.

Los estilos se registran en el arranque

El import de la party se adelantó un paso, y vale la pena explicar por qué.

Registrar una hoja de estilos escribe en el runtime de estilos que leen todos los componentes montados. Cuando el módulo de la pestaña party llega durante un render diferido, esa escritura ocurre mientras las pantallas de la Pokédex están a la vista, y React lo dice en desarrollo: Can’t perform a React state update on a component that hasn’t mounted yet, con un stack de componentes que apunta al bundle de la list. Tres avisos, la primera vez que se abre la pestaña. No falla nada. La pantalla se renderiza correctamente, el aviso es solo un log de desarrollo, y quien no abre nunca la segunda pestaña no llega a verlo.

Aun así vale la pena arreglarlo, porque ahora la causa es concreta y no un misterio. El post 8 registró un aviso con esta misma forma en el arranque y dijo con todas las letras que no lo había rastreado. En este caso sí se ha identificado la causa: import '../global.css' al principio de un módulo expuesto se ejecuta durante el render que carga ese módulo. Un bisect lo confirma. Comenta el import y la pestaña se abre en silencio.

Así que los estilos de la party pasaron a ser un módulo propio:

// apps/party/src/styles.ts
// The remote's styles as a module the host can preload, for the same reason partySlice is one:
// registering them is a side effect the styling runtime shares with every app already mounted.
import '../global.css';

Se suma al mapa de exposes junto al módulo de estado, y PartyStack.tsx importa ./styles en vez de la hoja de estilos directamente, para que la build en standalone los siga registrando:

exposes: {
  './PartyStack': './src/PartyStack.tsx',
  './partySlice': './src/partySlice.ts',
  './styles': './src/styles.ts',
},

El host lo carga en el mismo efecto que ya carga el módulo de estado, una línea más arriba:

import('partyApp/styles').catch(err => console.warn('party styles failed to load', err));

Eso amplía la regla del post 8 con una segunda mitad. Todo lo que un remote debe registrar antes de renderizar, su reducer y sus estilos, se carga durante el arranque; las pantallas siguen cargando bajo demanda. La pestaña Pokédex no necesita ese cambio por un motivo que vale la pena decir: es lo primero que renderiza el host, así que sus estilos se registran cuando todavía no hay nada montado a lo que puedan afectar.

Quién comparte qué

La decisión sobre el mapa shared es el test de identidad de los posts 3 y 6, planteado tres veces y con tres respuestas distintas.

nativewind entra en el mapa shared porque su runtime mantiene un registro de estilos a nivel de módulo y el estado del esquema de color. Con dos copias, un remote registra sus estilos en un registro que el host no lee nunca. La discusión #1032 de Re.Pack recoge ese mismo fallo. La respuesta aceptada propone compartir la ruta profunda react-native-css-interop/, hay respuestas posteriores que siguen informando del problema, y la PR #1242 se cerró sin que su propuesta de acumulación llegara a fusionarse.

El host proporciona el único runtime de estilos con estado. Con NativeWind compartido como singleton eager y el plugin de Re.Pack configurado en cada app, los dos remotes registran sus estilos en el registro que lee el host. Esta build lo comprueba en los dos remotes y en los dos esquemas de color.

@pokedex/ui se añade en las mismas condiciones, y también lo proporciona el host como singleton eager: un provider, un tema y un único conjunto de instancias de componentes. Las herramientas de build se quedan fuera del mapa de runtime. Tailwind y PostCSS no llegan al bundle, y Zod exporta valores, no una identidad de proceso. Estas son las cuatro entradas del host:

'@pokedex/ui': {
  singleton: true,
  eager: true,
  requiredVersion: pkg.dependencies['@pokedex/ui'],
},
nativewind: {
  singleton: true,
  eager: true,
  requiredVersion: pkg.dependencies.nativewind,
},
'react-native-reanimated': {
  singleton: true,
  eager: true,
  requiredVersion: pkg.dependencies['react-native-reanimated'],
},
'react-native-worklets': {
  singleton: true,
  eager: true,
  requiredVersion: pkg.dependencies['react-native-worklets'],
},

Reanimated y Worklets registran vistas nativas y un runtime de worklets una sola vez por proceso. Cuando la list incluyó su propia copia para el plegado del detail, la app murió al cargar con el mismo nombre de vista nativa registrado dos veces. Ahora el host proporciona una única copia a todos los consumidores.

Y @pokedex/detail se queda fuera a propósito. El versionado por consumidor es su punto fuerte: la escalera de versiones del post 5 sigue funcionando después de este post, y que la list vaya con detail 4.0.2 mientras la party sigue en 3.x es una configuración compatible, no un bug. El mismo criterio da la respuesta contraria, y ambas son correctas.

Los grises se unifican

Las pantallas mantienen su estructura. La list renderiza PokemonCard en una FlatList de tres columnas, y la party usa esa misma tarjeta junto al EmptySlot del design system. Los colores literales de cada app pasan a resolverse mediante tokens compartidos; solo conservan valores en crudo las definiciones de tokens y las capas translúcidas del detail:

Antes, por appDespués, desde la frontera
#2a75bb en los botones de dos appsbg-pokemonGreen, un token
#6b7280, #9ca3af, #f3f4f6, los grises de plantillatext-darkGrey, text-midGrey, bg-offGrey
#eef2ff / #4338ca, los colores de insignia de la listTYPE_BG_CLASS por tipo real
Pantallas #fffVariantes de ScreenContainer: bg-offWhite en claro, bg-navy en oscuro

La conversión trae además dos cambios menores. El detail se abre ahora como un push dentro del stack de cada pestaña, con el BackPill del design system flotando sobre el hero, de modo que la tab bar se queda fija mientras hay un detalle abierto:

Animado: un toque en la tarjeta de Bulbasaur, en la cuadrícula de tres columnas, abre la pantalla de detalle terminada, con su hero verde hierba entrando sobre la misma tab bar

Y el botón de Refresh de la cabecera, el del post 6, pasa a ser un gesto de tirar hacia abajo que gestiona la propia list. El dispatch no cambia (un solo tag invalidado, que proveen los dos endpoints del post 10), así que las filas REST y las insignias GraphQL se recargan juntas desde un gesto en lugar de un botón:

Animado: la cuadrícula de la Pokédex arrastrada hacia abajo, el spinner de recarga girando encima de las tarjetas, y la cuadrícula asentándose después

Un toggle, tres bundles

ThemeToggle es un componente del design system sin props: al pulsarlo escribe el esquema de color en el singleton compartido de nativewind, y cada remote lo monta a la derecha de su propia cabecera. Lo interesante es lo que no hace: no habla con ningún remote. El esquema es estado a nivel de módulo dentro del runtime de estilos compartido; cada clase dark: del runtime se vuelve a resolver en cuanto cambia el esquema, y tres bundles publicados por separado se repintan a la vez. No hay documentación que lo prometa; está comprobado en esta build.

Ese mismo cambio de esquema alcanza también la navegación nativa. Todo sale del mismo observable: el host obtiene de useColorScheme() el tema que aplica a su NavigationContainer, la misma suscripción que usan las clases. En una versión anterior de esta build, el host guardaba además su propio estado, y las barras de navegación nativas cambiaban casi medio segundo antes que los estilos del contenido. Con una única fuente de verdad, todas las superficies se actualizan en el mismo ciclo.

Animado: la lista de la Pokédex en modo claro, un toque en el toggle de tema de la cabecera, la misma lista repintada en azul marino con insignias de tipo tonales, y luego la pestaña Party igual de oscura: tres bundles publicados por separado cambiando de tema con una sola pulsación

El feedback pasa por el mismo singleton

El feedback del equipo usa ese mismo paquete compartido. toast() es una función normal de @pokedex/ui, y el host monta el único Toaster del runtime por encima de la tab bar. Después de despachar la acción de añadir, la list consulta el store y muestra el resultado que ha calculado el reducer. Con cinco miembros, dos toques rápidos producen un mensaje de «joined your party» y otro de «Your party is full».

La list no llega a montar el componente que dibuja la cápsula. Llama a la API de presentación compartida, y el Toaster del host renderiza el resultado con el sprite del Pokémon y el color de su tipo.

Animado: un toque en el botón Add to party del color del tipo, y una cápsula azul marino subiendo sobre la tab bar con el sprite de Bulbasaur y las palabras Bulbasaur joined your party

La eliminación desde la party se confirma por la misma vía, que es la regla de propiedad del post 6 aplicada a la presentación: la confirmación corre a cargo de quien modifica el estado. Además, es el propio toast() el que anuncia el mensaje al lector de pantalla, así que quien lo llama no puede mostrar la confirmación visual sin ofrecer también la confirmación accesible.

Animado: un toque en la insignia de eliminar de un miembro del equipo, la cuadrícula cerrándose alrededor de los huecos vacíos dibujados, y una cápsula que confirma Bulbasaur left your party

Ahora rómpelo

Cada entrada del mapa está justificada; compruébalo quitando una. En apps/list/rspack.config.mjs, borra la entrada nativewind de shared, reinicia el dev server de la list y recarga.

La app no falla. Arranca, la lista se renderiza, los sprites se cargan y el layout queda mal sin hacer ruido: los contenedores de columna de la cuadrícula pierden el ancho, así que cada tarjeta se ajusta a su propio contenido y las tres columnas dejan de cuadrar entre filas. Los colores aguantan, y también las insignias de tipo y la tipografía, y por eso el fallo pasa tan desapercibido. El remote ya incluye su propio runtime de estilos. Su build compila las clases del remote en un registro que los componentes compartidos no leen nunca, y lo que se pierde en pantalla es el ancho. Los componentes de @pokedex/ui no se ven afectados, porque sus clases las compiló el escaneo del host en el runtime del host.

El fallo del post 6 era ruidoso, el del post 8 era silencioso, y este es silencioso y parcial: la pantalla sigue funcionando, solo que su cuadrícula deja de ser la del design system. Restaura la entrada, reinicia y la cuadrícula vuelve a su sitio. Un runtime de estilos es estado compartido, y el estado compartido bajo federación o es un singleton o es un bug.

Ejecútalo

Los recursos nativos siguen incluyéndose en el binario del host. Copia del repo de acompañamiento los iconos de las pestañas, el icono de la app, la bola de la pantalla de arranque, las fuentes Nunito y la configuración nativa. La copia de ios/Host incluye el catálogo de iconos, el storyboard de arranque y el app delegate. Copia también project.pbxproj, porque Xcode solo renderiza las imágenes sueltas del splash si el proyecto las declara como recursos. El último comando enlaza las tipografías:

cp -R /tmp/pokedex-ref/apps/host/src/assets apps/host/src/
cp -R /tmp/pokedex-ref/assets .
cp /tmp/pokedex-ref/apps/host/react-native.config.js apps/host/
cp /tmp/pokedex-ref/apps/list/react-native.config.js apps/list/
cp /tmp/pokedex-ref/apps/party/react-native.config.js apps/party/
cp -R /tmp/pokedex-ref/apps/host/ios/Host apps/host/ios/
cp /tmp/pokedex-ref/apps/host/ios/Host.xcodeproj/project.pbxproj apps/host/ios/Host.xcodeproj/
cp -R /tmp/pokedex-ref/apps/host/android/app/src/main/res apps/host/android/app/src/main/
( cd apps/host && npx react-native-asset@2.3.0 )

Verdaccio sigue activo desde los pasos de publicación, y los tres paquetes ya están publicados. Arranca los tres dev servers, cada uno en su propio terminal:

( cd apps/list && npm run start:remote )
( cd apps/party && npm run start:remote )
( cd apps/host && npm start )

Después compila y lanza la app del host una vez, para que el icono y el catálogo queden incluidos en la build:

( cd apps/host && npm run ios )

Abre la pestaña Pokédex y verás la cuadrícula de tarjetas a tres columnas. Toca un Pokémon para abrir el detalle terminado, añádelo al equipo y observa la confirmación. Después pulsa el toggle de la cabecera: con una sola pulsación cambian de esquema todas las superficies, los dos remotes y la navegación nativa incluidos. El ciclo completo, del splash al equipo vacío, se ve así:

Recorrido animado: el splash de arranque, la cuadrícula de la Pokédex en claro, el detalle de Bulbasaur que se abre y se pliega, el toast de joined your party, el cambio de tema que muestra la cuadrícula en modo oscuro, la pestaña Party oscura, y la confirmación de la eliminación sobre los huecos vacíos

Lo que has construido, y lo que viene

Un solo design system y un solo runtime de estilos sirven ya a todos los remotes. Los remotes importan @pokedex/ui como un paquete cualquiera, y la federación lo resuelve a la copia del host. Una release del shell puede cambiarles el aspecto sin reconstruir cada remote, siempre que la API de los componentes siga siendo compatible.

Queda la frontera con lo nativo. react-native-svg incluye un módulo nativo, y el código nativo no puede llegar por federación. Las fuentes topan con esa misma frontera por otro motivo: el binario tiene que contener cada tipografía y registrarla en tiempo de build.

El repo de acompañamiento deja los dos casos a la vista. Los iconos de las pestañas son imágenes planas tintadas, las variantes de Nunito van enlazadas en el binario del host, y la bola de la pantalla de arranque es un recurso del storyboard.

Lo siguiente: la accesibilidad cruza la frontera, con un paquete de testing compartido que comprueba áreas táctiles, contraste y orden de foco en remotes que el host solo conoce en runtime.

Fuentes

Warren de Leon
Warren de Leon

Software Engineering Manager. Recientemente lideré el equipo de Mobile Platform en Hargreaves Lansdown. Escribo sobre liderazgo técnico, React Native y cómo construir buenos equipos.

Ver perfil