Una paleta de design system que alimenta tres panells d'apps federades

El design system com a singleton federat a React Native

El post 10 va acabar amb una promesa: el design system es converteix en un singleton federat, un sol paquet d’UI compartit en runtime, perquè cada remote renderitzi els mateixos components sense carregar amb la seva pròpia còpia. Aquest post la compleix.

El deute és real. Des del post 4, cada pantalla ha definit els seus propis estils amb StyleSheet.create i hexadecimals escrits a mà, i la duplicació ja abasta tres apps i el paquet de vista: els mateixos grisos de plantilla es repeteixen a la list, a la party i a la vista de detall, i el mateix blau d’accent apareix tant al botó de Refresh del host com al d’Add del detail. Quatre bases de codi amb quatre còpies de les mateixes decisions de disseny, i cap lloc on tocar-les una sola vegada.

La solució aplica als píxels dos patrons que la sèrie ja coneix. El post 3 va compartir biblioteques com a singletons perquè una sola còpia serveixi totes les apps. Els posts 5 i 6 van portar paquets a la frontera perquè cada costat n’instal·li una única definició.

@pokedex/ui fa les dues funcions: empaqueta els components copy-in de gluestack-ui, els aplica estils amb NativeWind 4.2.6 i els publica al registre local de Verdaccio. Després, Module Federation lliura a cada remote la còpia del host en runtime. Aquesta build fa servir @gluestack-ui/core a ^3.0.25, @gluestack-ui/utils a ^3.0.21 i Tailwind 3.4.19.

A la frontera ja hi ha tres paquets, i cadascun la creua d’una manera. Els contractes i el design system es resolen a una única còpia en runtime, i així ho representa el diagrama: és el host qui els proporciona. El paquet detail no es comparteix d’aquesta manera, i la decisió és deliberada: cada consumidor conserva la seva pròpia versió fixada, que és exactament per a això que es va dissenyar al post 5. Aquest contrast és el nucli d’aquest post.

Un paquet per als píxels

Comença des del 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

Val la pena ser sincer abans d’entrar en l’anatomia: el codi del design system no cap en un post. Són tretze components de gluestack copiats, mapes de tokens per a divuit tipus i onze components compostos. Per això els paquets d’aquest post es copien del tag final en lloc d’escriure’s a mà, i el mateix val per a les versions noves de contracts i detail, que aquest post també amplia (El detail estrena disseny explica en què consisteixen). Descarrega una sola vegada la còpia de referència (Executa-ho la torna a fer servir per als recursos natius) i copia els tres paquets:

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 segueix l’anatomia que van fixar els posts 5 i 6: react-native-builder-bob genera un lib/ en ESM, "react-native": "./src/index.ts" exposa el codi sense compilar perquè Re.Pack compili exactament els fitxers que hi ha al repo, publishConfig apunta al Verdaccio local i la llista files incorpora dues entrades que cap paquet anterior no havia necessitat: tailwind.preset.js i global.css.

La CLI de gluestack copia el codi dels components a src/components/ui/. Copy-in vol dir que el codi passa a ser teu: és el teu paquet qui el publica, i les actualitzacions d’upstream no arriben soles. El repo d’acompanyament conserva les tretze primitives que fan servir aquestes pantalles, de box a vstack.

Una d’aquestes còpies arrossega una dependència que ningú no declara. heading importa @expo/html-elements, i el paquet generat no l’inclou a dependencies. Afegeix-lo tu: si no, la instal·lació es completa sense queixar-se i el problema apareix més tard, quan el bundler del consumidor arriba a aquest import i no troba el mòdul.

Per sobre de les primitives hi ha els tokens i els components compostos. tailwind.preset.js defineix els colors de marca i els neutres, a més de type.<name> per als divuit tipus de Pokémon. Aquests mateixos valors també són a src/tokens/ per al codi de runtime. Els fan servir onze components: ScreenContainer, PokemonCard, TypeBadge, LoadingState, ErrorState, InfoRow, StatBar, EmptySlot, BackPill, ThemeToggle i Toaster. Un barrel, primitives.ts, fa que els consumidors importin tot des d’un únic punt.

Hi ha dues decisions que corresponen als tokens, i deixar-les en mans dels components provoca una fallada silenciosa.

Només es generen les classes escrites com a strings literals. Un `bg-type-${t}` construït al vol és invisible per a l'escàner de Tailwind, i el color no arriba mai a l'app sense que salti cap avís. El fitxer de tokens declara cada classe en mapes literals (TYPE_BG_CLASS i companyia) i exposa funcions de consulta, de manera que tota la paleta queda en un únic lloc auditable.

La segona és el contrast del text. Els colors de tipus necessiten un color de text mesurat, no triat a ull. El blanc sobre el blau del tipus aigua mesura 3,48:1, i sobre el rosa del tipus psíquic, 3,41:1: per sota del mínim de 4,5:1 que les WCAG demanen per a text petit. El negre supera 6:1 en tots dos casos. textOnTypeClass() calcula una sola vegada quin dels dos contrasta més, i cada superfície acolorida per tipus fa servir el resultat. Amb un negre fixat a mà, el hero del tipus lluita es quedaria a 1,7:1.

Les imatges són l’excepció a tota aquesta còpia de codi font. La lluna i el sol del ThemeToggle i la fletxa del BackPill són fitxers PNG a src/assets, que van arribar amb la còpia, i la llista files els inclou al paquet publicat.

Publica’l igual que als posts 5 i 6, sense saltar-te el npm install:

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

La versió 1.0.2 és l’API 1.0.0 que estava prevista, més els pedaços que van afegir les revisions posteriors.

El detail estrena disseny

La versió definitiva del detail necessita dades que el contracte anterior no incloïa. Contracts 3.2.2 afegeix heightM, weightKg, abilities, stats i un flavourText opcional a PokemonDetail. També afegeix types a PartyMember, perquè la party pugui acolorir les seves targetes sense fer una altra petició. parsePokemonDetail converteix les unitats de la PokéAPI i normalitza el flavour text una sola vegada, a la frontera.

Els dos remotes demanen ara el payload d’espècie juntament amb el del Pokémon, i la list envia les dades de tipus en afegir un membre. La passada de fitxers de Configurar un runtime d’estils en una build federada incorpora aquests canvis. El codi dels paquets nous ja venia a la còpia de referència, així que publica contracts i detail abans que les apps els instal·lin:

( 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 refà la vista sobre el design system: un hero plegable amb el color del tipus, l’sprite dins d’un anell, el flavour text, les files d’Info, les Base Stats i el botó d’Add. És una major perquè afegeix peer dependencies: @pokedex/ui, NativeWind i Reanimated passen a ser obligatoris per a qualsevol consumidor.

Les props no canvien. La vista continua acceptant { pokemon, loading, error, onRetry, onAddToParty, addDisabled, addLabel }, així que als dos contenidors els basta amb actualitzar la versió. El canvi visual més gran de la sèrie s’aplica amb un npm install.

La pantalla de detall de Pokémon acabada: un hero verd herba amb l'sprite en un anell translúcid sobre un número 001 semitransparent al fons, el nom de Bulbasaur i les seves insígnies de tipus, després el flavour text en una targeta de cita, files d'Info i barres de Base Stats en targetes flotants

El plegat és el gest característic de la pantalla, i val la pena veure’l abans de la secció de configuració, perquè tots els seus colors són classes de token que es resolen a la frontera:

Animat: el hero verd herba del detail desapareix a mesura que es fa scroll i el contingut puja, i s'assenta en una barra de títol compacta que diu Bulbasaur

Configurar un runtime d’estils en una build federada

La documentació de NativeWind cobreix Metro i situa Re.Pack entre les solucions de la comunitat. Callstack, per la seva banda, manté un plugin propi, @callstack/repack-plugin-nativewind, amb la versió lligada al Re.Pack instal·lat (aquí, la 5.2.5). Reanimated necessita el mateix tracte: Re.Pack detecta el paquet i avisa quan falta ReanimatedPlugin, així que @callstack/repack-plugin-reanimated hi entra al costat amb la mateixa versió. Totes dues coses són certes, i la configuració que ve a continuació és la que documenta el plugin mateix.

Les tres apps reben les dependències d’estils. Els dos remotes instal·len a més detail 4.0.2 i contracts 3.2.2. Totes declaren gluestack core i utils de manera explícita, perquè el codi copiat els importa. I totes fixen react i react-dom a la seva pròpia versió de React, la 19.2.3, perquè la resolució de peers no acabi instal·lant un segon 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 bloc overrides és la raó que aquests installs comencin amb npm pkg set. gluestack arrossega react-native-web i react-dom com a peers transitius, i els seus rangs de react xoquen amb el react@19.2.3 fixat de l'app. Fixar-los tots dos a overrides ho resol. Recórrer a --legacy-peer-deps amaga justament els desajustos de versió que un singleton compartit necessita que vegis.

Els últims quatre pins actualitzen @module-federation/enhanced i el trio de la CLI de React Native (el paquet central i els dos de plataforma) a les seves versions actuals, amb la qual cosa desapareixen els avisos de npm audit que aquest stack pot resoldre; el README del repo documenta els que queden i per què. react-native-reanimated i react-native-worklets inclouen codi natiu, així que cal actualitzar els pods del host abans de la build d’Executa-ho: ( cd apps/host/ios && bundle install && bundle exec pod install ), la mateixa convenció que segueix el README del repo. A Qui comparteix què s’explica per què hi ha react-native-reanimated i react-native-worklets. Fixa tailwindcss per sota de la 4: el runtime d’estils de NativeWind 4, react-native-css-interop, declara una dependència de Tailwind 3, i el latest d’npm és un 4.x que aquí no funciona.

La configuració de cada app, els fitxers d’entrada i les pantalles refetes surten de la còpia de referència. Amb aquesta passada, el teu arbre 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 l’origen JSX de NativeWind, la configuració de Rspack incorpora el plugin, i un global.css de tres línies conté les directives de Tailwind. El fitxer interessant és 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: [],
};

Aquest fitxer i els que l’envolten concentren tres advertències, i passar per alt qualsevol d’elles provoca una fallada silenciosa.

Escaneja el codi dels paquets. Tailwind només genera una classe quan veu l'string literal, i les classes del design system són a node_modules. Els globs de content han d'incloure el codi publicat de @pokedex/ui i @pokedex/detail, localitzat amb require.resolve. Si t'ho saltes, el problema sembla intermitent: les classes que l'app també fa servir continuen funcionant, i només desapareixen les que són exclusives de la biblioteca.

Importa global.css a cada mòdul exposat, no només a l'entrada. La federació carrega el mòdul exposat, mai l'index.js propi del remote, així que un global.css importat només a l'entrada funciona en standalone i, federat, no fa res ni avisa de res. Per això ListStack.tsx l'importa al principi del fitxer, i la party hi arriba a través del mòdul que descriu Els estils es registren a l'arrencada.

darkMode: 'class' no és decoratiu. Sense ell, el setColorScheme de NativeWind llança una excepció, i el canvi de tema d'Un toggle, tres bundles depèn d'aquesta crida.

Els estils es registren a l’arrencada

L’import de la party s’ha avançat un pas, i val la pena explicar per què.

Registrar un full d’estils escriu al runtime d’estils que llegeixen tots els components muntats. Quan el mòdul de la pestanya party arriba durant un render diferit, aquest registre d’estils es produeix mentre les pantalles del Pokédex són a la vista, i React ho diu en desenvolupament: Can’t perform a React state update on a component that hasn’t mounted yet, amb un stack de components que apunta al bundle de la list. Tres avisos, el primer cop que s’obre la pestanya. No falla res. La pantalla es renderitza correctament, l’avís és només un log de desenvolupament, i qui no obre mai la segona pestanya no arriba a veure’l.

Tot i així val la pena arreglar-ho, perquè ara la causa és concreta i no un misteri. El post 8 va registrar un avís amb aquesta mateixa forma a l’arrencada i va dir clarament que no l’havia rastrejat. En aquest cas sí que s’ha identificat la causa: import '../global.css' al principi d’un mòdul exposat s’executa durant el render que carrega aquest mòdul. Un bisect ho confirma. Comenta l’import i la pestanya s’obre en silenci.

Així que els estils de la party van passar a ser un mòdul propi:

// 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';

S’afegeix al mapa d’exposes al costat del mòdul d’estat, i PartyStack.tsx importa ./styles en lloc del full d’estils directament, perquè la build en standalone els continuï registrant:

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

El host el carrega en el mateix efecte que ja carrega el mòdul d’estat, una línia més amunt:

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

Això amplia la regla del post 8 amb una segona meitat. Tot el que un remote ha de registrar abans de renderitzar, el seu reducer i els seus estils, es carrega durant l’arrencada; les pantalles es continuen carregant sota demanda. La pestanya Pokédex no necessita aquest canvi per un motiu que val la pena dir: és el primer que renderitza el host, així que els seus estils es registren quan encara no hi ha res més muntat que se’n pugui veure afectat.

Qui comparteix què

La decisió sobre el mapa shared és el test d’identitat dels posts 3 i 6, plantejat tres vegades i amb tres respostes diferents.

nativewind entra al mapa shared perquè el seu runtime manté un registre d’estils a nivell de mòdul i l’estat de l’esquema de color. Amb dues còpies, un remote registra els seus estils en un registre que el host no llegeix mai. La discussió #1032 de Re.Pack recull aquesta mateixa fallada. La resposta acceptada proposa compartir la ruta profunda react-native-css-interop/, hi ha respostes posteriors que continuen informant del problema, i la PR #1242 es va tancar sense que la seva proposta d’acumulació arribés a fusionar-se.

El host proporciona l’únic runtime d’estils amb estat. Amb NativeWind compartit com a singleton eager i el plugin de Re.Pack configurat a cada app, els dos remotes registren els seus estils al registre que llegeix el host. Aquesta build ho comprova als dos remotes i als dos esquemes de color.

@pokedex/ui s’hi afegeix en les mateixes condicions, i també el proporciona el host com a singleton eager: un provider, un tema i un únic conjunt d’instàncies de components. Les eines de build es queden fora del mapa de runtime. Tailwind i PostCSS no arriben al bundle, i Zod exporta valors, no una identitat de procés. Aquestes són les quatre entrades 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 i Worklets registren vistes natives i un runtime de worklets una sola vegada per procés. Quan la list va incloure la seva pròpia còpia per al plegat del detail, l’app va morir en carregar-se amb el mateix nom de vista nativa registrat dues vegades. Ara el host proporciona una única còpia a tots els consumidors.

I @pokedex/detail es queda fora a propòsit. El versionat per consumidor és el seu punt fort: l’escala de versions del post 5 continua funcionant després d’aquest post, i que la list vagi amb detail 4.0.2 mentre la party continua en 3.x és una configuració compatible, no un bug. El mateix criteri dona la resposta contrària, i totes dues són correctes.

Els grisos s’unifiquen

Les pantalles mantenen la seva estructura. La list renderitza PokemonCard en una FlatList de tres columnes, i la party fa servir aquesta mateixa targeta al costat de l’EmptySlot del design system. Els colors literals de cada app passen a resoldre’s mitjançant tokens compartits; només conserven valors en cru les definicions de tokens i les capes translúcides del detail:

Abans, per appDesprés, des de la frontera
#2a75bb als botons de dues appsbg-pokemonGreen, un token
#6b7280, #9ca3af, #f3f4f6, els grisos de plantillatext-darkGrey, text-midGrey, bg-offGrey
#eef2ff / #4338ca, els colors d’insígnia de la listTYPE_BG_CLASS per tipus real
Pantalles #fffVariants de ScreenContainer: bg-offWhite en clar, bg-navy en fosc

La conversió comporta dos canvis menors més. El detail s’obre ara com un push dins de l’stack de cada pestanya, amb el BackPill del design system flotant sobre el hero, de manera que la tab bar es queda fixa mentre hi ha un detall obert:

Animat: un toc a la targeta de Bulbasaur, a la graella de tres columnes, obre la pantalla de detall acabada, amb el seu hero verd herba entrant sobre la mateixa tab bar

I el botó de Refresh de la capçalera, el del post 6, passa a ser un gest d’estirar cap avall que gestiona la list mateixa. El dispatch no canvia (un sol tag invalidat, que proveeixen els dos endpoints del post 10), així que les files REST i les insígnies GraphQL es recarreguen juntes des d’un gest en lloc d’un botó:

Animat: la graella del Pokédex estirada cap avall, l'spinner de recàrrega girant sobre les targetes, i la graella assentant-se després

Un toggle, tres bundles

ThemeToggle és un component del design system sense props: en prémer-lo escriu l’esquema de color al singleton compartit de nativewind, i cada remote el munta a la dreta de la seva pròpia capçalera. El que és interessant és el que no fa: no parla amb cap remote. L’esquema és estat a nivell de mòdul dins del runtime d’estils compartit; cada classe dark: del runtime es torna a resoldre tan bon punt canvia l’esquema, i tres bundles publicats per separat es repinten alhora. Cap documentació no ho promet; està comprovat en aquesta build.

Aquest mateix canvi d’esquema arriba també a la navegació nativa. Tot surt del mateix observable: el host obté de useColorScheme() el tema que aplica al seu NavigationContainer, la mateixa subscripció que fan servir les classes. En una versió anterior d’aquesta build, el host guardava a més el seu propi estat, i les barres de navegació natives canviaven gairebé mig segon abans que els estils del contingut. Amb una única font de veritat, totes les superfícies s’actualitzen al mateix cicle.

Animat: la llista del Pokédex en mode clar, un toc al toggle de tema de la capçalera, la mateixa llista repintada en blau marí amb insígnies de tipus tonals, i després la pestanya Party igual de fosca: tres bundles publicats per separat canviant de tema amb una sola premuda

El feedback passa pel mateix singleton

El feedback de l’equip fa servir aquest mateix paquet compartit. toast() és una funció normal de @pokedex/ui, i el host munta l’únic Toaster del runtime per sobre de la tab bar. Després de despatxar l’acció d’afegir, la list consulta l’store i mostra el resultat que ha calculat el reducer. Amb cinc membres, dos tocs ràpids produeixen un missatge de «joined your party» i un altre de «Your party is full».

La list no arriba a muntar el component que dibuixa la càpsula. Crida l’API de presentació compartida, i el Toaster del host renderitza el resultat amb l’sprite del Pokémon i el color del seu tipus.

Animat: un toc al botó Add to party del color del tipus, i una càpsula blau marí pujant sobre la tab bar amb l'sprite de Bulbasaur i les paraules Bulbasaur joined your party

L’eliminació des de la party es confirma per la mateixa via, que és la regla de propietat del post 6 aplicada a la presentació: la confirmació va a càrrec de qui modifica l’estat. A més, és el toast() mateix qui anuncia el missatge al lector de pantalla, així que qui el crida no pot mostrar la confirmació visual sense oferir també la confirmació accessible.

Animat: un toc a la insígnia d'eliminar d'un membre de l'equip, la graella tancant-se al voltant dels buits dibuixats, i una càpsula que confirma Bulbasaur left your party

Ara trenca-ho

Cada entrada del mapa està justificada; comprova-ho traient-ne una. A apps/list/rspack.config.mjs, esborra l’entrada nativewind de shared, reinicia el dev server de la list i recarrega.

L’app no falla. Arrenca, la llista es renderitza, els sprites es carreguen i el layout queda malament sense fer soroll: els contenidors de columna de la graella perden l’amplada, així que cada targeta s’ajusta al seu propi contingut i les tres columnes deixen de quadrar entre files. Els colors aguanten, i també les insígnies de tipus i la tipografia, i per això la fallada passa tan desapercebuda. El remote ja inclou el seu propi runtime d’estils. La seva build compila les classes del remote en un registre que els components compartits no llegeixen mai, i el que es perd a la pantalla és l’amplada. Els components de @pokedex/ui no es veuen afectats, perquè les seves classes les va compilar l’escaneig del host dins del runtime del host.

La fallada del post 6 era sorollosa, la del post 8 era silenciosa, i aquesta és silenciosa i parcial: la pantalla continua funcionant, només que la seva graella deixa de ser la del design system. Restaura l’entrada, reinicia i la graella torna al seu lloc. Un runtime d’estils és estat compartit, i l’estat compartit sota federació o és un singleton o és un bug.

Executa-ho

Els recursos natius continuen incloent-se al binari del host. Copia del repo d’acompanyament les icones de les pestanyes, la icona de l’app, la bola de la pantalla d’arrencada, les fonts Nunito i la configuració nativa. La còpia d’ios/Host inclou el catàleg d’icones, l’storyboard d’arrencada i l’app delegate. Copia també project.pbxproj, perquè Xcode només renderitza les imatges soltes de l’splash si el projecte les declara com a recursos. L’última ordre enllaça les tipografies:

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 continua actiu des dels passos de publicació, i els tres paquets ja estan publicats. Arrenca els tres dev servers, cadascun al seu propi terminal:

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

Després compila i llança l’app del host una vegada, perquè la icona i el catàleg quedin inclosos a la build:

( cd apps/host && npm run ios )

Obre la pestanya Pokédex i veuràs la graella de targetes a tres columnes. Toca un Pokémon per obrir el detall acabat, afegeix-lo a l’equip i observa la confirmació. Després prem el toggle de la capçalera: amb una sola premuda canvien d’esquema totes les superfícies, els dos remotes i la navegació nativa inclosos. El cicle complet, de l’splash a l’equip buit, es veu així:

Recorregut animat: l'splash d'arrencada, la graella del Pokédex en clar, el detall de Bulbasaur que s'obre i es plega, el toast de joined your party, el canvi de tema que mostra la graella en mode fosc, la pestanya Party fosca, i la confirmació de l'eliminació sobre els buits dibuixats

El que has construït, i el que ve

Un sol design system i un sol runtime d’estils serveixen ja tots els remotes. Els remotes importen @pokedex/ui com un paquet qualsevol, i la federació el resol a la còpia del host. Una release del shell pot canviar-los l’aspecte sense reconstruir cada remote, sempre que l’API dels components continuï sent compatible.

Queda la frontera amb el natiu. react-native-svg inclou un mòdul natiu, i el codi natiu no pot arribar per federació. Les fonts topen amb aquesta mateixa frontera per un altre motiu: el binari ha de contenir cada tipografia i registrar-la en temps de build.

El repo d’acompanyament deixa els dos casos a la vista. Les icones de les pestanyes són imatges planes tenyides, les variants de Nunito van enllaçades al binari del host, i la bola de la pantalla d’arrencada és un recurs de l’storyboard.

A continuació: l’accessibilitat creua la frontera, amb un paquet de testing compartit que comprova àrees tàctils, contrast i ordre de focus en remotes que el host només coneix en runtime.

Fonts

Warren de Leon
Warren de Leon

Software Engineering Manager. Recentment he liderat l'equip de Mobile Platform a Hargreaves Lansdown. Escric sobre lideratge tècnic, React Native i com construir bons equips.

Veure perfil