El post anterior va acabar amb una frase amb un forat: una pantalla que renderitzava “Opened from the ” i res més, perquè un tipus comprovat en build no serveix de res davant d’un valor que només apareix en runtime. També va acabar amb una promesa: dades reals, un sol store, compartit entre els remotes. Aquest post compleix la promesa, i el forat rep la seva resposta de debò pel camí.
Es recolza en estat de servidor i estat de client, del breu desviament de la sèrie. La divisió entre les dades que pertanyen a un servidor i les que pertanyen a l’app es dona aquí per sabuda, no es torna a explicar. Aquest post tracta d’una de les seves meitats, l’estat de servidor, sota federació: un sol store de Redux Toolkit (RTK) al host, un sol cache d’RTK Query que el domini Pokédex omple amb els seus dos endpoints, mentre la vista instal·lada de @pokedex/detail renderitza el que li donen. Dades en directe de PokéAPI substitueixen cada Pokémon hardcodejat de l’app, incloses les dues còpies desviades.
Això és el que construirem, abans de cap línia de codi:
L’única cosa que has de retenir: baseApi és un sol objecte, i cada costat n’importa exactament el mateix. Això és el que fa que el cache sigui compartit. Trenca-ho, i l’app es trenca d’una manera que val la pena veure, així que ho trencarem a propòsit cap al final.
Continua amb el teu propi codi del post 5 si ho vas construir pas a pas. Si no, parteix del seu estat final:
git clone https://github.com/warrendeleon/react-native-module-federation
cd react-native-module-federation
git checkout post-05-contracts
El paquet de contractes estrena superfície de runtime
Fins ara @pokedex/contracts només ha tingut tipus. Cada export s’esborrava en build, així que res seu no arribava a cap bundle. Ara incorpora el seu primer export de runtime: l’objecte d’API d’RTK Query a través del qual fa fetch tota l’app.
El host sembla el lloc natural per a ell. L’store viu al host, el cache viu a l’store, i un objecte d’API al costat de l’store que alimenta és on el posaria una app normal. En una app normal aquest instint és correcte. Sota federació falla per identitat: un consumidor només pot afegir endpoints a la mateixa instància de baseApi que l’store del host va muntar, i una instància que viu dins del codi font del host és una que res més no pot importar. El paquet de contractes és l’únic mòdul que cada costat ja resol a una sola còpia, perquè és un singleton de Module Federation. Posa-hi la instància i el baseApi.injectEndpoints({...}) d’un consumidor es registra contra l’únic cache i l’únic middleware que l’store ja executa. Una instància vol dir un cache HTTP, una deduplicació de peticions, un graf de tags entre totes les features, incloses les que es publiquin molt després del shell. packages/contracts/src/api.ts:
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
import { z } from 'zod';
export const baseApi = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: 'https://pokeapi.co/api/v2/' }),
tagTypes: ['PokemonList'],
endpoints: () => ({}),
});
export interface PokemonSummary {
id: number;
name: string;
spriteUri: string;
}
export interface PokemonDetail {
id: number;
name: string;
spriteUri: string;
types: string[];
}
const PokemonListResponseSchema = z.object({
// La url no és una string qualsevol: l'id de la fila se'n deriva, així que l'esquema
// vigila l'id numèric final que porta una url de recurs de PokéAPI.
results: z.array(z.object({ name: z.string().min(1), url: z.string().regex(/\/\d+\/?$/) })),
});
const PokemonDetailResponseSchema = z.object({
id: z.number(),
name: z.string(),
types: z.array(z.object({ type: z.object({ name: z.string() }) })),
});
export function artworkUri(id: number): string {
return `https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/${id}.png`;
}
export function idFromResourceUrl(url: string): number {
const match = url.match(/\/(\d+)\/?$/);
if (!match) {
throw new Error(`PokéAPI resource URL has no trailing id: ${url}`);
}
const id = Number(match[1]);
if (!Number.isSafeInteger(id) || id < 1) {
throw new Error(`PokéAPI resource URL id is out of range: ${url}`);
}
return id;
}
function formatName(name: string): string {
return name
.split('-')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
}
export function parsePokemonList(raw: unknown): PokemonSummary[] {
const { results } = PokemonListResponseSchema.parse(raw);
return results.map(entry => {
const id = idFromResourceUrl(entry.url);
return { id, name: formatName(entry.name), spriteUri: artworkUri(id) };
});
}
export function parsePokemonDetail(raw: unknown): PokemonDetail {
const parsed = PokemonDetailResponseSchema.parse(raw);
return {
id: parsed.id,
name: formatName(parsed.name),
spriteUri: artworkUri(parsed.id),
types: parsed.types.map(entry => formatName(entry.type.name)),
};
}
createApi sense endpoints construeix una carcassa buida: un reducer, una mica de middleware, i un mètode injectEndpoints que els consumidors cridaran. fetchBaseQuery és un embolcall petit sobre fetch que anteposa la URL base i parseja el JSON. tagTypes anomena l’únic tag pel qual aquesta app invalida; encara no fa res, i fa feina de debò a l’última secció.
Les dues funcions de parseig són la part que assenyalava la frase trencada del post 5. Un tipus és una promesa de temps de compilació, i ja no existeix quan una resposta aterra de veritat. Un camp reanomenat o un null on abans hi havia un string es cola directe per un cast escrit a mà i rebenta tres pantalles més enllà. Així que cada resposta crua es valida amb un esquema de Zod just a la frontera, i una forma incorrecta es converteix en un valor que la pantalla pot gestionar en lloc d’un crash.
La frontera ha de vigilar tot allò de què depèn, i per això l’esquema de la llista comprova l’id final de la url en lloc de conformar-se amb z.string(): l’id es deriva d’aquesta url, i una comprovació laxa aquí deixaria passar un payload malformat com un Pokémon fantasma número 0. idFromResourceUrl llança pel mateix motiu: un helper que s’inventa un id en silenci davant d’una entrada dolenta és un parser que deixa passar dades malformades en lloc de rebutjar-les. La comprovació de rang també es guanya les seves dues línies, perquè una tirallonga de dígits al final encara no és un id. /pokemon/0/ no anomena res, i una cadena de dígits prou llarga com per sortir del rang d’enters segurs ha deixat de ser el número imprès a la url.
L’esquema del detail conserva només el que renderitza la pantalla de detail; el payload complet de PokéAPI per a un Pokémon s’acosta als 300 KB de JSON, i parsejar camps que res no mostra només seria més superfície on trencar-se. La validació en runtime té el seu propi post més endavant a la sèrie.
Un export de runtime i dues peer dependencies noves són un canvi que trenca, així que la versió puja de major. El número en si necessita primer un cop d’ull al teu propi registre: l’escala del post 5 ja va publicar 1.1.0 i 2.0.0, i un número publicat queda gastat per sempre, perquè el registre es nega a reutilitzar-lo i cada install depèn d’aquesta negativa. Així que la frontera de runtime surt com a 3.0.3. El major és el que explica la història; el patch final és l’enduriment del parser que va arribar després de la primera publicació. packages/contracts/package.json:
{
"name": "@pokedex/contracts",
"version": "3.0.3",
"dependencies": {
"zod": "^3.25.76"
},
"peerDependencies": {
"@reduxjs/toolkit": ">=2.10.0",
"react": "*",
"react-redux": ">=9"
}
}
zod és una dependència real de runtime, així que viatja dins del paquet. @reduxjs/toolkit i react-redux són peers: les apps els instal·len, i el contracte usa les seves còpies en lloc d’empaquetar-ne una de pròpia. Instal·la les dependències de desenvolupament del paquet i publica:
cd packages/contracts
npm install
npm publish
+ @pokedex/contracts@3.0.3
El caret fa aquí el que el post 5 va mostrar que fa. Cada consumidor és a ^1.0.0, així que npm install deixa les tres apps exactament on són encara que 3.0.3 ja existeixi al registre. Adoptar el contracte nou és un moviment deliberat, app per app, i una de les tres es queda quieta a propòsit.
Un sol store al host
El host suma un store. apps/host/src/store.ts:
import { combineSlices, configureStore } from '@reduxjs/toolkit';
import { baseApi } from '@pokedex/contracts';
export const store = configureStore({
reducer: combineSlices(baseApi),
middleware: getDefaultMiddleware => getDefaultMiddleware().concat(baseApi.middleware),
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
El repartiment de propietat és el sentit del fitxer. L’store viu al host: un store per app és feina del shell, com la barra de pestanyes. La instància d’API al voltant de la qual es construeix l’store viu al paquet de contractes, per la raó d’identitat de dalt. El host munta la connexió; el contracte posa la instància.
baseApi.middleware és una peça estructural. Executa el cicle de vida del cache: fetching, deduplicació, invalidació per tags, expulsió del cache. Deixa’l fora de l’store i la primera pantalla que executi un hook de query llança un red box a pantalla completa en desenvolupament, i RTK anomena l’error sense embuts:
Warning: Middleware for RTK-Query API at reducerPath "api" has not been added to the store.
You must add the middleware for RTK-Query to function correctly!
Val la pena aturar-se a la pila de components d’aquesta captura: el punt del crash és PokedexScreen dins de __federation_expose_ListStack.chunk.bundle. La pantalla d’un remote, en un chunk que el host va descarregar en runtime, xoca amb un error causat per una línia que falta al fitxer de store del mateix host. La fallada creua la frontera de mòduls en la direcció contrària a tota la resta d’aquesta sèrie. Una fallada sorollosa, almenys. Tingues-la present, perquè el sabotatge d’Ara trenca-ho no rep cap error.
L’store entra per sobre de tot l’arbre, perquè tot el que està federat a dins pugui llegir el cache. apps/host/App.tsx, l’embolcall:
import { Provider } from 'react-redux';
import { store } from './src/store';
export default function App() {
return (
<Provider store={store}>
<SafeAreaProvider>
<NavigationContainer>
{/* el navegador de pestanyes del post 4 */}
</NavigationContainer>
</SafeAreaProvider>
</Provider>
);
}
Els remotes mai no creen ni importen un store. Es renderitzen dins d’aquest arbre i arriben a l’store a través del singleton compartit de react-redux, igual que arribaven al context compartit de safe-area al post 3.
Comparteix el trio d’estat
Perquè res de fora del host injecti al baseApi compartit, tres paquets s’han de resoldre a una sola còpia en runtime: @reduxjs/toolkit, react-redux, i el mateix @pokedex/contracts. S’uneixen al mapa de shared del host com a singletons eager, i la lliçó que tant va costar al post 5 s’aplica a dos d’ells immediatament. apps/host/rspack.config.mjs, les addicions:
import rtkPkg from '@reduxjs/toolkit/package.json' with { type: 'json' };
import reactReduxPkg from 'react-redux/package.json' with { type: 'json' };
// ...al mapa shared:
'@reduxjs/toolkit': {
singleton: true,
eager: true,
version: rtkPkg.version,
requiredVersion: pkg.dependencies['@reduxjs/toolkit'],
},
'react-redux': {
singleton: true,
eager: true,
version: reactReduxPkg.version,
requiredVersion: pkg.dependencies['react-redux'],
},
'@pokedex/contracts': {
singleton: true,
eager: true,
requiredVersion: pkg.dependencies['@pokedex/contracts'],
},
Un paquet resolt a través d’un mapa exports necessita declarar la seva version a mà. Els dos paquets de Redux ho fan, i @pokedex/contracts no. El post 5 ho va trobar en el mateix muntatge: rspack llegeix la versió del paquet que està compartint, i omet el provide per a un paquet resolt a través d’un mapa exports. Es va sostenir en aquestes versions, en aquesta configuració, que és el que s’afirma aquí. @reduxjs/toolkit i react-redux porten tots dos un mapa exports, així que sense el version explícit cap dels dos no aterraria al share scope. El paquet de contractes no té mapa exports, així que no necessita res. El grep del bundle del post 5 confirma que els tres provides es registren, contracte inclòs:
"@pokedex/contracts", version: "3.0.3"
"@reduxjs/toolkit", version: "2.12.0"
"react-redux", version: "9.3.0"
El remote list reflecteix les mateixes tres entrades sense eager: el host proveeix les còpies, el remote les consumeix. És la primera vegada que @pokedex/contracts apareix en cap mapa shared. Fins ara era només tipus, esborrats en build, així que no hi havia res a compartir. Ara porta baseApi, i una sola instància és tot el sentit.
Dues de les apps afegeixen els paquets i avancen el contracte:
( cd apps/host && npm install @reduxjs/toolkit@2.12.0 react-redux@9.3.0 @pokedex/contracts@3.0.3 )
( cd apps/list && npm install @reduxjs/toolkit@2.12.0 react-redux@9.3.0 @pokedex/contracts@3.0.3 )
L’app party és la tercera, i no rep res. Ni paquets de Redux, ni entrades shared noves, i els seus dos paquets instal·lats es queden on són: el contracte a ^1.0.0 (dos majors per darrere del contracte contra el qual tipa) i la pantalla de detail a ^1.0.0, la versió estàtica. Això és segur per una raó precisa: l’app party consumeix només tipus del contracte, i els tipus s’esborren en build. Un major de runtime canvia el que viatja al JavaScript del paquet; un consumidor que mai no importa res d’aquest JavaScript no té res a trencar. Els remotes entren a l’estat compartit per decisió pròpia; el shell no els imposa res.
El remote list: dades reals
Ara la primera injecció. apps/list/src/listApi.ts, un fitxer nou:
import { baseApi, parsePokemonList, type PokemonSummary } from '@pokedex/contracts';
const listApi = baseApi.injectEndpoints({
endpoints: build => ({
getPokemonList: build.query<PokemonSummary[], void>({
async queryFn(_arg, _api, _extra, baseQuery) {
const res = await baseQuery('pokemon?limit=151');
if (res.error) {
return { error: res.error };
}
try {
return { data: parsePokemonList(res.data) };
} catch (err) {
return {
error: {
status: 'CUSTOM_ERROR',
error: err instanceof Error ? err.message : 'Invalid PokéAPI response',
},
};
}
},
providesTags: ['PokemonList'],
}),
}),
});
export const { useGetPokemonListQuery } = listApi;
injectEndpoints afegeix getPokemonList al baseApi compartit i retorna un hook tipat. L’endpoint porta els primers 151 Pokémon en una sola petició, passa el cos cru a parsePokemonList, i retorna o les files amb forma o un error capturat. providesTags: ['PokemonList'] estampa el resultat amb el tag pel qual el host invalidarà. El shell no sabia res d’aquest endpoint quan es va construir; el remote l’afegeix a l’store en marxa el primer cop que es carrega el seu codi.
La pantalla es desfà del seu array hardcodejat de cinc files i llegeix el hook. apps/list/src/PokedexScreen.tsx:
export default function PokedexScreen() {
const insets = useSafeAreaInsets();
const navigation = useNavigation<NativeStackNavigationProp<ListParamList>>();
const { data, isLoading, isError, refetch } = useGetPokemonListQuery();
if (isLoading) {
return (
<View style={styles.centre}>
<ActivityIndicator size="large" />
</View>
);
}
if (isError || !data) {
return (
<View style={styles.centre}>
<Text style={styles.error}>Couldn't reach PokéAPI.</Text>
<Pressable style={styles.retry} onPress={() => refetch()}>
<Text style={styles.retryText}>Try again</Text>
</Pressable>
</View>
);
}
return (
<FlatList
data={data}
keyExtractor={p => String(p.id)}
contentContainerStyle={{ paddingBottom: insets.bottom + 8 }}
renderItem={({ item }) => (
<Pressable
style={styles.row}
onPress={() => navigation.navigate('PokemonDetail', { id: item.id })}>
<Image source={{ uri: item.spriteUri }} style={styles.sprite} />
<Text style={styles.number}>#{String(item.id).padStart(3, '0')}</Text>
<Text style={styles.name}>{item.name}</Text>
</Pressable>
)}
/>
);
}
Tres estats en lloc d’un: un spinner mentre la petició és en curs, una pantalla d’error amb reintent quan PokéAPI no respon, i la llista. La navegació del post 5 està intacta: tocar una fila segueix empenyent PokemonDetail amb { id } dins de l’stack propi d’aquest remote. El que va canviar és d’on surten les files: un cache a l’store del host, omplert per un endpoint que aquest remote va injectar, llegit a través d’un hook que no existia quan el host es va publicar.
El detail pren vida; la biblioteca continua sent una vista
El post 5 va deixar una olor deliberada al paquet de detail: la seva pròpia còpia privada de les dades de Pokémon, desviada de la còpia del list, amb un comentari prometent que les dades en directe l’esborrarien. Aquest és aquell moment, i arriben dues releases que tracen una línia.
La línia: una biblioteca de components publica píxels, i les definicions de dades viuen amb el domini d’aquestes dades. El proper post defensa aquesta línia a fons; aquest l’aplica. Les dades de Pokémon són del domini Pokédex, així que el segon endpoint aterra al costat del primer, a l’app list. apps/list/src/detailApi.ts:
import { baseApi, parsePokemonDetail, type PokemonDetail } from '@pokedex/contracts';
const detailApi = baseApi.injectEndpoints({
endpoints: build => ({
getPokemonDetail: build.query<PokemonDetail, number>({
async queryFn(id, _api, _extra, baseQuery) {
const res = await baseQuery(`pokemon/${id}`);
if (res.error) {
return { error: res.error };
}
try {
return { data: parsePokemonDetail(res.data) };
} catch (err) {
return {
error: {
status: 'CUSTOM_ERROR',
error: err instanceof Error ? err.message : 'Invalid PokéAPI response',
},
};
}
},
}),
}),
});
export const { useGetPokemonDetailQuery } = detailApi;
Una query que pren l’id com a argument, porta un Pokémon, i el parseja a la frontera. Sense tags: res no invalida encara un sol Pokémon.
@pokedex/detail puja a 3.0.0: un major, perquè les props canvien de forma del tot. La còpia estàtica i la seva cerca han marxat; el que queda és una vista: PokemonDetailView pren el Pokémon i els tres estats com a props, els renderitza, i no fa res més. L’app list compon hook i vista en un contenidor petit a la seva ruta de detail:
import { PokemonDetailView } from '@pokedex/detail';
import { useGetPokemonDetailQuery } from './detailApi';
function PokemonDetailRoute({ route }: { route: { params: DetailParams } }) {
const { data, isLoading, isError, refetch } = useGetPokemonDetailQuery(route.params.id);
return <PokemonDetailView pokemon={data} loading={isLoading} error={isError} onRetry={refetch} />;
}
D’on surten les dades és cosa de l’app; quin aspecte tenen és cosa de la biblioteca. Res al paquet no toca l’store, i passar a dades en directe no li va afegir ni una dependència: els mateixos quatre peers que tenia la versió estàtica, i Redux no apareix entre ells.
Suma el cost de prendre vida: un fitxer d’endpoint al domini d’aquestes dades, un major honest a la vista, i un contenidor de quatre línies. I la recompensa més silenciosa: la còpia de dades del list i la còpia del paquet han marxat totes dues, així que la deriva entre elles també ha marxat. Les dues pantalles renderitzen ara el que digui PokéAPI, a través d’un sol cache, i contradir-se l’una a l’altra ja no és una cosa que sàpiguen fer.
Ara trenca-ho
L’afirmació és que una sola instància compartida ho sosté tot. La manera més ràpida de fiar-te’n és treure-la i mirar.
Esborra l’entrada @pokedex/contracts del mapa shared a les dues configs que la porten, host i list, i deixa @reduxjs/toolkit i react-redux al seu lloc. Reinicia els servidors de desenvolupament i rellança.
L’spinner gira. Per sempre. Sense crash, sense red box, i aquest cop tampoc res a la consola. Vint segons després, l’única línia que el servidor de desenvolupament ha registrat és l’arrencada de l’app. Mira-ho l’estona que vulguis: no arriba res més.
L’avís del middleware de la secció de l’store semblaria la fallada natural aquí, i la raó que no salti mai és la lliçó sencera. Amb el contracte sense compartir, el host empaqueta la seva pròpia còpia de @pokedex/contracts i el remote list n’empaqueta una de diferent. Dues còpies volen dir dos objectes baseApi. L’store del host va muntar el reducer i el middleware de la seva còpia, així que des d’on mira RTK el muntatge està complet i sa: no falta res, res de què avisar. Els dos endpoints del domini es van registrar a la còpia del list, una que cap store no va muntar mai, així que els endpoints existeixen, els hooks corren, i els fetch que haurien de disparar no van enlloc. Cada còpia és internament coherent. L’error és entre elles, i res en runtime no es fa càrrec d’aquest «entre».
Aquesta és la fallada silenciosa a la qual els posts del singleton compartit tornen una vegada i una altra, i ja té una escala completa. Dos Reacts rebenten en arrencar. Un middleware que falta llança un red box que et diu quin fitxer has d’arreglar. Dos baseApi et donen un spinner sobre un cache que mai no s’omple, i l’únic diagnòstic és l’absència de tota la resta. Una fallada sorollosa és barata; la silenciosa costa la tarda. Torna les entrades shared, reinicia, i la llista s’omple una altra vegada.
La invalidació creua la frontera
El graf de tags no s’ha usat des que el contracte el va declarar. Toca estirar-lo.
El post 4 va amagar totes les capçaleres amb headerShown: false al navegador de pestanyes, i el post 5 va donar a cada stack de remote les seves pròpies capçaleres dins de la pestanya. El host ara torna a encendre la capçalera de la pestanya Pokédex, perquè hi posarà una cosa que és seva. apps/host/App.tsx:
import { useDispatch } from 'react-redux';
import { baseApi } from '@pokedex/contracts';
function RefreshButton() {
const dispatch = useDispatch();
return (
<Pressable
style={styles.refresh}
onPress={() => dispatch(baseApi.util.invalidateTags(['PokemonList']))}
hitSlop={12}
accessibilityRole="button"
accessibilityLabel="Refresh Pokédex">
<Text style={styles.refreshText}>Refresh</Text>
</Pressable>
);
}
import { getFocusedRouteNameFromRoute } from '@react-navigation/native';
<Tab.Screen
name="Pokédex"
component={PokedexTab}
options={({ route }) => ({
headerShown: getFocusedRouteNameFromRoute(route) !== 'PokemonDetail',
headerRight: () => <RefreshButton />,
})}
/>
Un detall d’aquí mereix mirar-se de prop. La ruta de detail porta la seva pròpia capçalera de stack amb botó de tornar, i una capçalera de pestanya apilada a sobre posaria dues barres en pantalla. Així que el host amaga la seva barra mentre l’stack és al detail, i la mostra tan bon punt l’stack torna enrere.
Comprovar contra 'PokemonDetail' sembla que el host es fica en els noms de ruta interns del remote, i això és exactament el que seria si el nom visqués al remote. Però no hi viu. És el nom de ruta de DetailParamList a @pokedex/contracts, el mateix acord del qual surten els params. Sigues honest sobre fins on arriba això: el host escriu la cadena 'PokemonDetail' a mà, i TypeScript no connecta una cadena solta amb la clau del contracte. Reanomena la ruta al contracte i aquesta comparació continua compilant mentre deixa de coincidir en silenci. L’acord hi és; el compilador no està imposant aquest extrem.
RefreshButton necessita Pressable i Text afegits a l’import de react-native, més dues entrades petites d’estil; el fitxer complet és al tag d’acompanyament.
El host mai no va definir getPokemonList. No guarda cap referència a l’endpoint del remote list, ni al seu hook, ni a la seva query. L’única cosa que despatxa és un tag. invalidateTags(['PokemonList']) recorre el cache compartit, troba cada query que va proveir aquest tag, i torna a demanar les que tenen un subscriptor en pantalla. La llista es recarrega; l’endpoint del detail, que no proveeix tags, queda intacte. Un tag, declarat al contracte, proveït per una feature, despatxat pel host, i el refetch aterra en codi que qui despatxa no ha vist mai.
En una app real la invalidació penja de l’invalidatesTags d’una mutation en lloc d’un botó, però l’abast a través de la frontera de mòduls és el mateix. Els equips acorden noms de tags al contracte igual que acorden tipus, i aquest acord és el protocol sencer de refresc entre ells.
Acaba exactament a l’estat final d’aquest post. El recorregut imprimeix els fitxers que sostenen el build; els manifests, les configs, els tests i els canvis menors viuen al tag. Per acabar amb un arbre idèntic byte a byte al del tag, aboca-hi a sobre la còpia de referència. Un fitxer que has escrit bé se sobreescriu amb ell mateix, i l’abocada omple el que la prosa no ha imprès:
npx degit@3.8.0 --force warrendeleon/react-native-module-federation#post-06-shared-store /tmp/pokedex-ref-06
rm packages/detail/src/PokemonDetailScreen.tsx
cp -R /tmp/pokedex-ref-06/. .
L’rm importa: aquest post esborra aquesta pantalla, i una abocada sobre l’arbre afegeix i sobreescriu, però no elimina mai.
L’escombrada porta a més la configuració del transform de Jest que necessita l’stack de l’store, els tests de regressió del parser, els canvis de stack per pestanya i el refactor del detail que aquest recorregut ha descrit en prosa.
Posa’l en marxa
Els paquets estan publicats i instal·lats, així que Verdaccio no cal per a l’execució. Tres servidors de desenvolupament, un per app, i la build del simulador:
cd apps/list && npm run start:remote # :8082
cd apps/party && npm run start:remote # :8083
cd apps/host && npm start # :8081
cd apps/host && npm run ios
La pestanya Pokédex ensenya un spinner un moment, i després s’omple amb els primers 151 Pokémon, artwork oficial inclòs, directes de PokéAPI. Toca una fila i la ruta de detail porta el seu Pokémon a través del mateix cache. Toca Refresh a la capçalera i la llista es recarrega a l’altra banda de la frontera:
La pestanya Party segueix renderitzant la seva graella buida, amb el contracte dos majors per darrere, la pantalla de detail encara a la 1.0.0 estàtica, sense store a la vista, funcionant exactament igual que abans d’aquest post. La independència inclou la llibertat de no participar.
El que has construït, i el que ve
El paquet de contractes defineix l’únic baseApi en què injecta cada costat, així que el cache, la deduplicació i el graf de tags es comparteixen entre features que es van construir i publicar pel seu compte. El host munta l’store construït al seu voltant. El domini Pokédex omple el cache amb els seus dos endpoints, la vista instal·lada renderitza el que els contenidors li donen, la frontera queda protegida amb un esquema, i el host refresca dades que no sap anomenar despatxant un tag. Sense les entrades shared, la fallada és un spinner silenciós; ja l’has vist una vegada aquí, així que el reconeixeràs quan sigui car.
Tot el que creua la frontera segueix sent estat de servidor, això sí: dades que són propietat d’un servidor i de les quals el cache guarda una còpia. Res del que és propietat de la mateixa app no ha creuat encara cap frontera de mòduls, i l’app comença a acumular coses seves. La graella del Party segueix buida perquè res del que fas a la Pokédex no li pot arribar. Tanca l’app i torna-la a obrir, i res del que vas fer no sobreviu: ni party, ni preferits, ni memòria.
L’estat de client és l’altra meitat de la divisió de la qual va partir aquest post, i el build-along hi torna d’aquí a dos posts. Abans, la sèrie fa un pas enrere i defensa les decisions de propietat que els últims tres posts van anar prenent una a una: on va una frontera, què pot saber un component compartit, on viu una definició de dades.
Fonts
- Redux Toolkit: code splitting — l’
injectEndpointsi afegir endpoints a una API existent en runtime - RTK Query — el cache, els tags i els hooks generats
- Zod — la biblioteca d’esquemes que protegeix la frontera de runtime
- PokéAPI — l’API REST gratuïta de la qual fa fetch l’app
- react-native-module-federation — el repo d’acompanyament, al tag
post-06-shared-store