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 són propietat d’un servidor i les que són propietat de 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 guanya una superfície de runtime
Fins ara @pokedex/contracts només ha tingut tipus. Cada export s’esborrava en build, així que res seu arribava a cap bundle. Ara guanya 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. El host és propietari del store, el store és propietari del cache, i un objecte d’API al costat del 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 el store del host va cablejar, 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 el 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({
results: z.array(z.object({ name: z.string(), url: z.string() })),
});
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+)\/?$/);
return match ? Number(match[1]) : 0;
}
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’única etiqueta per la 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. 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 superfície de runtime surt com a 3.0.0. packages/contracts/package.json:
{
"name": "@pokedex/contracts",
"version": "3.0.0",
"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.0
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.0 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 guanya 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 de l’arxiu. El 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 el store viu al paquet de contractes, per la raó d’identitat de dalt. El host és propietari del cablejat; el contracte és propietari de 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 del 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 a l’arxiu 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 del final d’aquest post no rep cap error.
El 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 creen ni importen un store. Es renderitzen dins d’aquest arbre i arriben al 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 compartits 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'],
},
Els dos paquets de Redux declaren version a mà, i @pokedex/contracts no. El post 5 va trobar la regla: rspack llegeix la versió del paquet que està compartint, excepte amb un paquet resolt a través d’un mapa exports, on en lloc d’això omet el provide en silenci. @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.0"
"@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 el sentit és exactament aquest: una sola instància.
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.0 )
( cd apps/list && npm install @reduxjs/toolkit@^2.12.0 react-redux@^9.3.0 @pokedex/contracts@^3.0.0 )
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 arxiu 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 l’etiqueta per la qual el host invalidarà. El shell no sabia res d’aquest endpoint quan es va construir; el remote l’afegeix al store en marxa el primer cop que el seu codi carrega.
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ó vola, 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 del stack propi d’aquest remote. El que va canviar és d’on surten les files: un cache al 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 es queda en 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 pertanyen al domini amo de les 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 el 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 arxiu d’endpoint al domini amo de les dades, un major honest a la vista, i un contenidor de vuit 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ó del 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. El store del host va cablejar 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 cablejar mai, així que els endpoints existeixen, els hooks corren, i els fetch que haurien de disparar no van enlloc. Cada còpia és internament consistent. L’error és entre elles, i res en runtime no és propietari 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 anomena l’arxiu a 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 el stack és al detail, i la mostra tan bon punt el 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. El contracte segueix donant fruit en llocs que el post anterior mai no va predir.
RefreshButton necessita Pressable i Text afegits a l’import de react-native, més dues entrades petites d’estil; l’arxiu 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 una etiqueta. invalidateTags(['PokemonList']) recorre el cache compartit, troba cada query que va proveir aquesta etiqueta, 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. Una etiqueta, declarada al contracte, proveïda per una feature, despatxada 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.
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 és propietari de l’únic baseApi en què cada costat injecta, 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 és propietari del 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 una etiqueta. 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