Ang nakaraang post ay nagtapos sa isang pangungusap na may butas: isang screen na nagre-render ng “Opened from the ” at wala nang kasunod, dahil ang isang type na na-check sa build ay walang magagawa laban sa isang value na lumilitaw lang sa runtime. Nagtapos din ito sa isang pangako: totoong data, isang store, shared sa mga remote. Tinutupad ng post na ito ang pangako, at makukuha ng butas ang tunay nitong sagot sa daan.
Nakasandal ito sa server state at client state mula sa maikling pahinga sa serye. Ang hatian sa pagitan ng data na pag-aari ng server at data na pag-aari ng app ay ipinapalagay na dito, hindi na muling ipinapaliwanag. Ang post na ito ay tungkol sa isang kalahati nito, ang server state, sa ilalim ng federation: isang Redux Toolkit (RTK) store sa host, isang RTK Query cache, at ang Pokédex domain na pumupuno rito gamit ang dalawa nitong endpoint, habang ang installed na view ng @pokedex/detail ay nagre-render ng ibinibigay dito. Papalitan ng live na data mula sa PokéAPI ang bawat hardcoded na Pokémon sa app, kasama ang dalawang nag-hiwalay nang kopya.
Ang hugis na binubuo natin, bago ang anumang code:
Ang nag-iisang bagay na dapat tandaan: ang baseApi ay iisang object, at bawat panig ay nag-i-import ng eksaktong iisa. Iyon ang dahilan kung bakit shared ang cache. Sirain iyon, at masisira ang app sa paraang sulit makita, kaya sadya natin itong sisirain malapit sa dulo.
Magpatuloy mula sa sarili mong post 5 code kung sumunod ka sa build. Kung hindi, magsimula mula sa tapos nitong estado:
git clone https://github.com/warrendeleon/react-native-module-federation
cd react-native-module-federation
git checkout post-05-contracts
Nagkakaroon ng runtime seam ang contract package
Hanggang ngayon, puro types lang ang laman ng @pokedex/contracts. Bawat export ay binubura sa build, kaya walang umaabot mula rito sa anumang bundle. Ngayon makukuha nito ang una nitong runtime export: ang RTK Query API object na pinagdadaanan ng buong app sa pag-fetch.
Mukhang natural na lugar nito ang host. Pag-aari ng host ang store, pag-aari ng store ang cache, at ang isang API object sa tabi ng store na pinapakain nito ang lugar kung saan ito ilalagay ng isang ordinaryong app. Sa isang ordinaryong app, tama ang instinct na iyon. Sa ilalim ng federation, bumabagsak ito sa identity: ang isang consumer ay makakapagdagdag lang ng mga endpoint sa parehong baseApi instance na ikinabit ng store ng host, at ang isang instance na nasa loob ng source code ng host ay hindi maii-import ng iba. Ang contract package ang nag-iisang module na nire-resolve na ng bawat panig sa iisang kopya, dahil isa itong Module Federation singleton. Ilagay ang instance doon at ang baseApi.injectEndpoints({...}) ng isang consumer ay magrerehistro laban sa iisang cache at middleware na pinapatakbo na ng store. Ang iisang instance ay nangangahulugan ng isang HTTP cache, isang deduplication pipeline, isang tag graph sa lahat ng feature, kasama ang mga feature na ilalabas matagal pagkatapos ng 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)),
};
}
Ang createApi na walang endpoints ay bumubuo ng isang walang lamang balangkas: isang reducer, kaunting middleware, at isang injectEndpoints method na tatawagin ng mga consumer. Ang fetchBaseQuery ay isang maliit na wrapper sa ibabaw ng fetch na naglalagay ng base URL sa unahan at nagpa-parse ng JSON. Pinapangalanan ng tagTypes ang nag-iisang label na ginagamit ng app na ito sa pag-invalidate; wala pa itong ginagawa, at may tunay itong trabaho sa huling seksyon.
Ang dalawang parse function ang bahaging itinuturo ng sirang pangungusap ng post 5. Ang isang type ay isang compile-time na pangako, at wala na ito pagdating ng aktwal na response. Ang isang na-rename na field o isang null sa dating kinaroroonan ng string ay dadaan nang diretso sa isang hand-written na cast at babagsak pagkalampas ng tatlong screen. Kaya ang bawat raw na response ay vina-validate gamit ang isang Zod schema mismo sa seam, at ang isang maling hugis ay nagiging isang value na kayang hawakan ng screen sa halip na isang crash. Ang detail schema ay nagtatabi lang ng nire-render ng detail screen; ang buong PokéAPI payload para sa isang Pokémon ay umaabot nang halos 300 KB ng JSON, at ang pag-parse ng mga field na walang nagpapakita ay dagdag na surface lang na masisiraan. May sarili itong post ang runtime validation mamaya sa serye.
Ang isang runtime export at dalawang bagong peer dependency ay isang breaking change, kaya tataas ang version ng major. Ang mismong numero ay nangangailangan muna ng sulyap sa sarili mong registry: nai-publish na ng hagdan ng post 5 ang 1.1.0 at 2.0.0, at ang isang nai-publish na numero ay gastado na habang-buhay, dahil tumatanggi ang registry na gamitin itong muli at ang bawat install ay umaasa sa pagtangging iyon. Kaya lalabas ang runtime seam bilang 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"
}
}
Ang zod ay isang tunay na runtime dependency, kaya sumasama ito sa loob ng package. Ang @reduxjs/toolkit at react-redux ay mga peer: ang mga app ang nag-i-install sa kanila, at ginagamit ng contract ang kanilang mga kopya sa halip na mag-bundle ng sarili. I-install ang mga dev dependency ng package at i-publish:
cd packages/contracts
npm install
npm publish
+ @pokedex/contracts@3.0.0
Ginagawa rito ng caret ang ipinakita ng post 5 na ginagawa nito. Bawat consumer ay nasa ^1.0.0, kaya iniiwan ng npm install ang tatlong app sa eksaktong kinaroroonan nila kahit na nasa registry na ang 3.0.0. Ang pagkuha ng bagong contract ay isang sadyang hakbang, isa-isang app, at ang isa sa tatlo ay sadyang hindi gagalaw.
Isang store sa host
Makakakuha ang host ng 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;
Ang hatian ng pagmamay-ari ang punto ng file. Nasa host ang store: ang isang store bawat app ay trabaho ng shell, tulad ng tab bar. Ang API instance na pinagbubuuan ng store ay nasa contract package, dahil sa identity na dahilan sa itaas. Pag-aari ng host ang pagkakabit; pag-aari ng seam ang instance.
Ang baseApi.middleware ay may bigat na dala. Pinapatakbo nito ang cache lifecycle: fetching, deduplication, tag invalidation, cache eviction. Iwanan ito sa labas ng store at ang unang screen na magpapatakbo ng query hook ay magtatapon ng full-screen na red box sa development, kung saan diretsong pinapangalanan ng RTK ang pagkakamali:
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!
Sulit tingnan ang component stack sa capture na iyon: ang pinangyarihan ng crash ay ang PokedexScreen sa loob ng __federation_expose_ListStack.chunk.bundle. Ang screen ng isang remote, sa isang chunk na na-download ng host sa runtime, ay tumama sa isang error na dulot ng isang nawawalang linya sa store file mismo ng host. Tumatawid ang pagkabigo sa module boundary sa kabaligtarang direksyon ng lahat ng iba pa sa seryeng ito. Isang maingay na pagkabigo, kahit papaano. Tandaan ito, dahil ang sabotahe sa dulo ng post na ito ay walang matatanggap na anumang error.
Papasok ang store sa ibabaw ng buong tree, para mabasa ng lahat ng naka-federate dito ang cache. apps/host/App.tsx, ang wrapper:
import { Provider } from 'react-redux';
import { store } from './src/store';
export default function App() {
return (
<Provider store={store}>
<SafeAreaProvider>
<NavigationContainer>
{/* ang tab navigator mula sa post 4 */}
</NavigationContainer>
</SafeAreaProvider>
</Provider>
);
}
Hindi kailanman gumagawa o nag-i-import ng store ang mga remote. Nagre-render sila sa loob ng tree na ito at naaabot ang store sa pamamagitan ng shared na react-redux singleton, katulad ng pag-abot nila sa shared na safe-area context noong post 3.
I-share ang state trio
Para makapag-inject ang kahit ano sa labas ng host sa shared na baseApi, tatlong package ang kailangang mag-resolve sa iisang kopya sa runtime: @reduxjs/toolkit, react-redux, at ang mismong @pokedex/contracts. Sasali sila sa shared map ng host bilang mga eager singleton, at ang pinaghirapang aral ng post 5 ay agad na naa-apply sa dalawa sa kanila. apps/host/rspack.config.mjs, ang mga dagdag:
import rtkPkg from '@reduxjs/toolkit/package.json' with { type: 'json' };
import reactReduxPkg from 'react-redux/package.json' with { type: 'json' };
// ...sa shared map:
'@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'],
},
Ang dalawang Redux package ay nagdedeklara ng version nang mano-mano, at ang @pokedex/contracts ay hindi. Natagpuan ng post 5 ang panuntunan: binabasa ng rspack ang version mula sa package na ini-share nito, maliban sa isang package na nire-resolve sa pamamagitan ng isang exports map, kung saan tahimik nitong nilalaktawan ang provide sa halip. Ang @reduxjs/toolkit at react-redux ay parehong may dalang exports map, kaya kung wala ang tahasang version ay walang darating sa share scope mula sa alinman sa dalawa. Walang exports map ang contract package, kaya wala itong kailangan. Kinukumpirma ng bundle-grep mula sa post 5 na nagrerehistro ang lahat ng tatlong provide, kasama ang contract:
"@pokedex/contracts", version: "3.0.0"
"@reduxjs/toolkit", version: "2.12.0"
"react-redux", version: "9.3.0"
Sinasalamin ng list remote ang parehong tatlong entry nang walang eager: ang host ang nagbibigay ng mga kopya, ang remote ang kumukonsumo. Ito ang unang pagkakataon na lumitaw ang @pokedex/contracts sa anumang shared map. Hanggang ngayon, puro types lang ito, binubura sa build, kaya walang maisha-share. Ngayon dala nito ang baseApi, at ang iisang instance ang buong punto.
Dalawa sa mga app ang magdadagdag ng mga package at magtataas ng contract:
( 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 )
Ang party app ang pangatlo, at wala itong matatanggap. Walang Redux package, walang bagong shared entry, at ang dalawang installed na package nito ay nananatili sa kinaroroonan: ang contract sa ^1.0.0 (dalawang major sa likod ng seam na tina-type nito) at ang detail screen sa ^1.0.0, ang static na bersyon. Ligtas iyon dahil sa isang tiyak na dahilan: puro types lang ang kinukonsumo ng party app mula sa contract, at ang types ay binubura sa build. Binabago ng isang runtime major ang laman ng JavaScript ng package; ang isang consumer na hindi kailanman nag-i-import ng kahit ano sa JavaScript na iyon ay walang masisira. Ang mga remote ay sumasali sa shared state nang kusa; walang ipinipilit ang shell sa kanila.
Ang list remote: totoong data
Ngayon ang unang injection. apps/list/src/listApi.ts, isang bagong file:
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;
Idinadagdag ng injectEndpoints ang getPokemonList sa shared na baseApi at nagbabalik ng isang typed na hook. Kinukuha ng endpoint ang unang 151 Pokémon sa isang request, ibinibigay ang raw na body sa parsePokemonList, at nagbabalik ng alinman sa mga hinugisang row o isang nahuling error. Ang providesTags: ['PokemonList'] ay nagtatatak sa resulta ng label na gagamitin ng host sa pag-invalidate. Walang alam ang shell tungkol sa endpoint na ito noong binuo ito; idinadagdag ito ng remote sa tumatakbong store sa unang pagkakataong mag-load ang code nito.
Bibitawan ng screen ang hardcoded nitong array na limang row at babasahin ang 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>
)}
/>
);
}
Tatlong state sa halip na isa: isang spinner habang lumilipad ang request, isang error screen na may retry kapag hindi maabot ang PokéAPI, at ang listahan. Buo pa rin ang navigation mula sa post 5: ang pag-tap sa isang row ay nagpu-push pa rin ng PokemonDetail na may { id } sa loob ng sariling stack ng remote na ito. Ang nagbago ay kung saan nanggagaling ang mga row: isang cache sa store ng host, pinupunan ng isang endpoint na in-inject ng remote na ito, binabasa sa pamamagitan ng isang hook na wala pa noong nai-ship ang host.
Nabubuhay ang detail; nananatiling view ang library
Nag-iwan ang post 5 ng sadyang amoy sa detail package: ang sarili nitong pribadong kopya ng Pokémon data, lumihis na mula sa kopya ng list, na may komentong nangangakong bubura rito ang live na data. Ito na ang sandaling iyon, at dumarating ang dalawang release na gumuguhit ng isang linya.
Ang linya: ang isang component library ay nagshi-ship ng pixels, at ang mga depinisyon ng data ay pag-aari ng domain na may-ari ng data. Ipagtatanggol nang buo ng susunod na post ang linyang iyan; inilalapat ito ng post na ito. Ang Pokémon data ay sa Pokédex domain, kaya ang pangalawang endpoint ay lalapag sa tabi ng una, sa list app. 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;
Isang query na tumatanggap ng id bilang argument, kumukuha ng isang Pokémon, at nagpa-parse nito sa seam. Walang tags: wala pang nag-i-invalidate ng iisang Pokémon.
Aakyat ang @pokedex/detail sa 3.0.0: isang major, dahil ganap na nagbabago ng hugis ang props. Wala na ang static na kopya at ang lookup nito; ang natitira ay isang view: tumatanggap ang PokemonDetailView ng Pokémon at ng tatlong state bilang props, nire-render ang mga ito, at wala nang ibang ginagawa. Kinokompos ng list app ang hook at view sa isang maliit na container sa detail route nito:
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} />;
}
Kung saan nanggagaling ang data ay trabaho ng app; kung ano ang itsura nito ay trabaho ng library. Walang anuman sa package ang gumagalaw sa store, at walang naidagdag na kahit isang dependency ang pagiging live: ang parehong apat na peer na hawak ng static na bersyon, at wala ang Redux sa mga ito.
Bilangin ang gastos ng pagkabuhay: isang endpoint file sa domain na may-ari ng data, isang tapat na major sa view, at isang walong-linyang container. At ang mas tahimik na gantimpala: nawala na kapwa ang kopya ng data ng list at ang kopya ng package, kaya nawala na rin ang paglihis sa pagitan nila. Ang dalawang screen ay nagre-render na ngayon ng anumang sabihin ng PokéAPI, sa pamamagitan ng iisang cache, at ang pagsalungat sa isa’t isa ay hindi na nila alam gawin.
Ngayon, sirain natin
Ang claim ay ito: ang iisang shared na instance ang bumubuklod sa lahat. Ang pinakamabilis na paraan para paniwalaan iyon ay tanggalin ito at manood.
Burahin ang @pokedex/contracts entry mula sa shared map sa dalawang config na may dala nito (host at list), at iwanang nakalagay ang @reduxjs/toolkit at react-redux. I-restart ang mga dev server at i-relaunch.
Umiikot. Habang-buhay. Walang crash, walang red box, at sa pagkakataong ito ay wala ring anuman sa console. Makalipas ang dalawampung segundo, ang tanging linyang nai-log ng dev server ay ang pag-start ng app. Panoorin ito hangga’t gusto mo: wala nang darating pa.
Ang babala ng middleware mula sa seksyon ng store ang mukhang natural na pagkabigo rito, at ang dahilan kung bakit hindi ito lumalabas kailanman ang buong aral. Kapag hindi na shared ang contract, ang host ay magba-bundle ng sarili nitong kopya ng @pokedex/contracts at ang list remote ay magba-bundle ng ibang kopya. Ang dalawang kopya ay nangangahulugan ng dalawang baseApi object. Ikinabit ng store ng host ang reducer at middleware ng sarili nitong kopya, kaya mula sa pananaw ng RTK ay kumpleto at malusog ang setup: walang kulang, walang dapat ibabala. Ang dalawang endpoint ng domain ay nagrehistro sa kopya ng list, isang kopyang hindi kailanman ikinabit ng kahit anong store, kaya nandiyan ang mga endpoint, tumatakbo ang mga hook, at ang mga fetch na dapat nilang patakbuhin ay walang pupuntahan. Bawat kopya ay consistent sa sarili nito. Ang pagkakamali ay nasa pagitan nila, at walang anuman sa runtime ang may-ari ng “pagitan” na iyon.
Ito ang tahimik na pagkabigong paulit-ulit na binabalikan ng mga shared-singleton post, at mayroon na itong kumpletong hagdan. Ang dalawang React ay bumabagsak sa paglunsad. Ang nawawalang middleware ay nagtatapon ng red box na nagpapangalan sa file na dapat ayusin. Ang dalawang baseApi ay nagbibigay sa iyo ng spinner sa ibabaw ng isang cache na hindi kailanman napupuno, at ang tanging diagnostic ay ang kawalan ng lahat ng iba pa. Mura ang maingay na pagkabigo; ang tahimik ang kumakain ng hapon. Ibalik ang mga shared entry, i-restart, at mapupuno muli ang listahan.
Mag-invalidate sa kabila ng seam
Hindi pa nagagamit ang tag graph mula nang ideklara ito ng contract. Panahon nang hilahin ito.
Itinago ng post 4 ang lahat ng header gamit ang headerShown: false sa tab navigator, at binigyan ng post 5 ang bawat stack ng remote ng sarili nitong mga header sa loob ng tab. Bubuksan ngayong muli ng host ang header ng Pokédex tab, dahil maglalagay ito roon ng isang bagay na pag-aari nito. 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 />,
})}
/>
May isang detalye rito na sulit tingnang mabuti. Ang detail route ay may dala nitong sariling stack header na may back button, at ang isang tab header na nakapatong sa ibabaw nito ay maglalagay ng dalawang bar sa screen. Kaya itinatago ng host ang bar nito habang nasa detail ang stack, at ipinapakita ito sa sandaling bumalik ang stack. Ang pag-check laban sa 'PokemonDetail' ay mukhang pakikialam ng host sa mga internal na pangalan ng ruta ng remote, at iyon nga mismo ang mangyayari kung nasa remote ang pangalan. Pero hindi. Ito ang pangalan ng ruta mula sa DetailParamList sa @pokedex/contracts, ang parehong kasunduang pinagmumulan ng params. Patuloy na nagbabayad ang contract sa mga lugar na hindi kailanman inasahan ng nakaraang post.
Kailangan ng RefreshButton na maidagdag ang Pressable at Text sa import ng react-native, kasama ang dalawang maliit na style entry; ang kumpletong file ay nasa companion tag.
Hindi kailanman dinefine ng host ang getPokemonList. Wala itong hawak na reference sa endpoint ng list remote, sa hook nito, o sa query nito. Ang tanging dine-dispatch nito ay isang tag. Nilalakad ng invalidateTags(['PokemonList']) ang shared na cache, hinahanap ang bawat query na nagbigay ng tag na iyon, at muling kinukuha ang mga may subscriber na nasa screen. Nagre-reload ang listahan; ang endpoint ng detail, na walang ibinibigay na tags, ay hindi nagagalaw. Isang label, dineklara sa contract, ibinigay ng isang feature, dine-dispatch ng host, at ang refetch ay bumabagsak sa code na hindi pa nakikita kailanman ng nag-dispatch.
Sa isang totoong app, ang invalidation ay nakasabit sa invalidatesTags ng isang mutation sa halip na isang button, pero pareho ang abot nito sa kabila ng module boundary. Ang mga team ay nagkakasundo sa mga pangalan ng tag sa contract katulad ng pagkakasundo nila sa types, at ang kasunduang iyon ang buong refresh protocol sa pagitan nila.
Patakbuhin
Nai-publish at nai-install na ang mga package, kaya hindi na kailangan ang Verdaccio para sa pagpapatakbo. Tatlong dev server, isa bawat app, tapos ang simulator build:
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
Magpapakita ang Pokédex tab ng spinner nang sandali, tapos mapupuno ng unang 151 Pokémon, kasama ang opisyal na artwork, diretso mula sa PokéAPI. I-tap ang isang row at kukunin ng detail route ang Pokémon nito sa pamamagitan ng parehong cache. I-tap ang Refresh sa header at magre-reload ang listahan sa kabila ng seam:
Nagre-render pa rin ang Party tab ng walang lamang grid nito, contract na dalawang major sa likod, detail screen na nasa static na 1.0.0 pa rin, walang store na natatanaw, gumagana nang eksaktong tulad ng bago ang post na ito. Kasama sa kalayaan ang kalayaang hindi sumali.
Ang naitayo mo, at ang susunod
Pag-aari ng contract package ang nag-iisang baseApi na iniinjectan ng bawat panig, kaya ang cache, ang deduplication, at ang tag graph ay shared sa mga feature na magkakahiwalay na binuo at inilabas. Pag-aari ng host ang store na binuo sa paligid nito. Pinupuno ng Pokédex domain ang cache gamit ang dalawa nitong endpoint, nire-render ng installed na view ang ibinibigay ng mga container, nababantayan ang boundary gamit ang isang schema, at nire-refresh ng host ang data na hindi nito alam pangalanan sa pamamagitan ng pag-dispatch ng isang tag. Kapag tinanggal ang pag-share, ang pagkabigo ay isang tahimik na spinner; nakita mo na ito nang minsan dito, kaya makikilala mo ito kapag mahal na.
Lahat ng tumatawid sa seam ay server state pa rin: data na pag-aari ng server at hinahawakan ng cache ang isang kopya. Wala pang pag-aari mismo ng app ang nakakatawid ng module boundary, at nagsisimula nang mag-ipon ang app ng mga bagay na pag-aari nito. Walang laman pa rin ang Party grid dahil walang anumang ginagawa mo sa Pokédex ang nakakaabot dito. Isara ang app at buksang muli, at walang nakakaligtas sa anumang ginawa mo: walang party, walang mga paborito, walang alaala. Ang client state ang kabilang kalahati ng hatiang pinagsimulan ng post na ito, at babalikan iyon ng build-along pagkalipas ng dalawang post. Una, aatras muna ang serye at ipagtatanggol ang mga desisyon sa pagmamay-ari na paisa-isang ginawa ng huling tatlong post: kung saan pupunta ang isang boundary, kung ano ang puwedeng malaman ng isang shared component, kung saan naroon ang isang depinisyon ng data.
Mga Sanggunian
- Redux Toolkit: code splitting — ang
injectEndpointsat pagdaragdag ng mga endpoint sa isang API na nandiyan na sa runtime - RTK Query — ang cache, ang mga tag, at ang mga generated na hook
- Zod — ang schema library na nagbabantay sa runtime boundary
- PokéAPI — ang libreng REST API na pinagkukunan ng fetch ng app
- react-native-module-federation — ang companion repo, sa tag na
post-06-shared-store