El post 6 va acabar amb una limitació honesta: tanca l’app, torna-la a obrir, i res del que vas fer no sobreviu. Ni equip, ni preferits, ni memòria. Tot el que ha creuat la frontera fins ara ha estat estat de servidor: dades que són propietat de PokéAPI, guardades en un cache que tots els mòduls comparteixen. L’app en si encara no té res de seu.
Aquest post li dona alguna cosa seva: l’equip de sis. L’estat de client no té cap servidor al darrere ni cap cache per refrescar. Viu en un slice de Redux, i sota federació un slice planteja la pregunta al voltant de la qual gira tota la sèrie: qui n’és el propietari, i com el fan servir les altres features sense dependre’n? L’assaig sobre propietat va respondre amb principis. Aquest post respon amb codi.
Això és el que construirem:
Una idea per tenir al cap: el cache és de tothom, però un slice d’estat de client té exactament un propietari. L’app party és la propietària de l’equip. Tot el que un altre mòdul necessita (una acció, una forma de lectura, un límit) creua a través del contracte, i cap al final fem dispatch contra l’slice abans que s’hagi carregat el seu propietari, a propòsit, per veure què fa Redux amb una acció que ningú no escolta.
Continua des del teu propi codi del post 6 si l’has anat construint. 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-06-shared-store
Qui és el propietari de l’estat de client?
En aquesta app ja hi viuen tres classes d’estat, i cadascuna té un propietari diferent.
El cache de servidor és de tothom. El post 6 el va construir: un baseApi al contracte, un store al host, endpoints injectats per qui respon de les dades. Cap app no és propietària d’una entrada del cache. Les dades són de PokéAPI i el cache només en guarda una còpia, i per això qualsevol mòdul les pot llegir.
L’estat privat té un propietari i es queda a dins. Quins membres té l’equip, com funciona treure’n un, què significa el límit: tot això és cosa de l’app party, en un slice que ningú més no importa.
La interacció que creua és la franja estreta entre tots dos. La Pokédex necessita afegir un membre a un equip que no és seu, i la seva capçalera vol mostrar “My Party 3/6” amb un estat que no pot veure. Aquests creuaments es tipen, s’anomenen, es versionen i es posen al contracte, perquè la regla de l’assaig de propietat també mana aquí: les apps mai no depenen entre si. Les apps depenen de contractes.
Aquesta taxonomia decideix tota la resta del post. El que segueix construeix aquesta taula fila a fila.
El mur, i el reducer es muda
L’slice de l’equip s’ha d’unir a l’store en marxa, i Redux Toolkit (RTK) té una API exactament per a això: combineSlices construeix un reducer amb un mètode inject, i un slice injectat en runtime comença a reduir des d’aquell moment. Així que l’app party necessita cridar inject sobre l’objecte reducer que l’store del host va muntar de veritat.
El host del post 6 construïa aquell reducer inline:
// apps/host/src/store.ts, al post 6
export const store = configureStore({
reducer: combineSlices(baseApi),
middleware: getDefaultMiddleware => getDefaultMiddleware().concat(baseApi.middleware),
});
No hi ha manera d’arribar-hi. El reducer és una expressió dins del codi font del host, mai exportada, i ni tan sols un export ajudaria: les apps no importen apps. Una còpia és pitjor que inútil: combineSlices(baseApi) a l’app party construeix un segon reducer que cap store no executa, i injectar-hi no canvia res a la pantalla.
És l’argument d’identitat del post 6, per tercera vegada. baseApi es va mudar al contracte perquè un consumidor només pot injectar endpoints a la mateixa instància que l’store va muntar. El reducer arrel es muda per la mateixa raó, al fitxer del costat. packages/contracts/src/store.ts:
import { combineSlices } from '@reduxjs/toolkit';
import { baseApi } from './api';
export const rootReducer = combineSlices(baseApi);
I l’store del host es queda en un import:
// apps/host/src/store.ts
import { configureStore } from '@reduxjs/toolkit';
import { baseApi, rootReducer } from '@pokedex/contracts';
export const store = configureStore({
reducer: rootReducer,
middleware: getDefaultMiddleware => getDefaultMiddleware().concat(baseApi.middleware),
});
El host continua sent el propietari de l’store: el middleware, el Provider, tot el muntatge. El contracte ara és el propietari del reducer igual que ho és de la instància d’api. Com que contracts és un singleton de federació, rootReducer és un sol objecte a tot el runtime, i un slice injectat per una app construïda mesos després del shell aterra al reducer que el shell ja executa.
El contracte porta el creuament
Ara la fila central de la taxonomia. Les interaccions que creuen una frontera d’app van a un nou packages/contracts/src/party.ts, curt a propòsit:
import { createAction, nanoid } from '@reduxjs/toolkit';
export const MAX_PARTY = 6;
export interface PartyMember {
uid: string;
id: number;
name: string;
spriteUri: string;
}
export const addToParty = createAction(
'party/add',
(member: Omit<PartyMember, 'uid'>) => ({ payload: { ...member, uid: nanoid() } }),
);
export const partyStateReady = createAction('party/stateReady');
export interface PartySliceShape {
party?: { members: PartyMember[] };
}
addToParty és l’única acció de la qual es fa dispatch des de fora de l’equip. El contracte defineix la forma de l’acció; el reducer de l’app party decideix què significa. El callback prepare estampa cada membre amb un nanoid() (ve dins d’RTK, sense dependència nova) perquè el reducer es mantingui pur i dues còpies del mateix Pokémon continuïn sent distingibles. Els duplicats estan permesos per disseny: un equip de sis Magikarp és una decisió tan vàlida com qualsevol altra, i l’uid és el que els distingeix quan en treus un.
MAX_PARTY seu a la frontera perquè cada superfície que reflecteix la regla ha de llegir el mateix número: un botó desactivat en una app, un comptador en una altra, la guarda al reducer del propietari.
partyStateReady és la raresa del grup: cap case del reducer no l’atén, i res del party no canvia quan es despatxa. Existeix per a la seqüència d’arrencada, i la secció sobre carregar mòduls d’estat ensenya l’única cosa útil que fa.
PartySliceShape és el costat de lectura, i el marcador opcional és el disseny, no una defensa. L’slice l’injecta en runtime un mòdul que qui llegeix no controla, així que en el moment en què un mòdul aliè llegeix, state.party pot no existir encara. Qui llegeix escriu s.party?.members ?? [] i renderitza una cosa honesta en tots dos casos. Existeixen alternatives tipades (withLazyLoadedSlices d’RTK enfila l’slice possiblement absent via declaration merging), però la forma tolerant ensenya la situació en lloc d’amagar-la, i el sabotatge d’Ara trenca-ho depèn que l’entenguis.
Fixa’t en el que falta. L’equip també tindrà una acció remove, i no apareix en cap contracte, perquè ningú més no en fa dispatch. El contracte porta el que creua, res més; una entrada que ningú no consumeix és un passiu amb número de versió.
Els dos fitxers nous són additius, així que la versió és una minor; el tag porta 3.1.3, la minor més els mateixos patches d’enduriment que es va endur la línia 3.0.x. Publica:
cd packages/contracts
npm install && npm publish
+ @pokedex/contracts@3.1.3
El host i la list són tots dos a ^3.0.0, i aquí el lockfile importa més que el caret. Un npm install normal no canvia res: el lockfile fixa 3.0.3, i install respecta el lockfile encara que el caret acceptaria 3.1.3. Avançar un caret té la seva pròpia ordre:
( cd apps/host && npm update @pokedex/contracts )
Sense editar package.json; només es mou una línia del lockfile. El pin només es toca quan un consumidor creua una major. Un consumidor està a punt de fer-ho.
El propietari
L’app party ha estat el grup de control de la sèrie: contracte a ^1.0.0, detail a ^1.0.0, sense accés a l’store, una graella estàtica de sis buits. Fer créixer un slice acaba amb això en tots els fronts, i l’adopció es fa a mà, a propòsit, perquè dues majors són una distància real. apps/party/package.json:
"@pokedex/contracts": "^3.1.0",
"@pokedex/detail": "^3.1.0",
"@reduxjs/toolkit": "^2.12.0",
"react-redux": "^9.3.0"
npm install, i el build es trenca de seguida, al compilador i no en runtime. L’stack de l’app party encara munta la pantalla de detall a la manera 1.0.0: import PokemonDetailScreen from '@pokedex/detail', directe a component=. El paquet 3.x ja no té default export, així que aquell import ara resol a l’objecte namespace del paquet, i el muntatge deixa de passar la comprovació de tipus:
error TS2322: Type 'typeof import(".../@pokedex/detail/dist/index")' is not
assignable to type 'ScreenComponentType<PartyParamList, "PokemonDetail"> | undefined'.
Tres majors del paquet detail li han passat de llarg a aquesta app mentre no mirava. El que 3.x exporta és la PokemonDetailView amb nom, una vista que exigeix les dades com a props, així que l’error de compilació obliga la party a escriure el mateix contenidor de vuit línies que l’app list va escriure al post 6. Un contenidor necessita dades, i això es converteix en un mur propi a L’equip necessita dades pròpies. Cadenes així són el que costa de veritat el retard de versions: no un crash a producció, una pila de murs el dia que per fi adoptes.
L’slice és el cor del post, i cap en una pantalla. apps/party/src/partySlice.ts:
import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
import { addToParty, MAX_PARTY, rootReducer, type PartyMember } from '@pokedex/contracts';
export const partySlice = createSlice({
name: 'party',
initialState: { members: [] as PartyMember[] },
reducers: {
// Private: nobody else dispatches remove, so it ships in no contract.
remove(state, action: PayloadAction<string>) {
state.members = state.members.filter(m => m.uid !== action.payload);
},
},
extraReducers: builder => {
builder.addCase(addToParty, (state, { payload }) => {
if (state.members.length >= MAX_PARTY) return; // the cap lives with the owner
state.members.push(payload);
});
},
});
export const { remove } = partySlice.actions;
// Importing this module is what adds the reducer to the shared store.
rootReducer.inject(partySlice);
Dues coses porten el disseny. L’acció que creua es lliga via extraReducers: l’equip atén l’addToParty del contracte. Val la pena ser precís sobre per què això funciona entre apps construïdes per separat, perquè és fàcil atribuir-ho al que no toca. builder.addCase llegeix actionCreator.type i registra el seu reducer sota aquell string, així que el que ha de coincidir és party/add, no la identitat de l’objecte creator. Dues còpies del contracte també encaixarien. Definir l’string, el límit i la forma de lectura una sola vegada en lloc de reescriure’ls a cada costat és feina del paquet versionat: publicar el contracte és el que evita aquella deriva, hi hagi singleton o no. On la identitat singleton en runtime es guanya el lloc és en els objectes, rootReducer i baseApi: una injecció només aterra a l’store que va connectar el host si tots dos costats sostenen la mateixa instància.
I la guarda del límit viu aquí, al propietari. El contracte publica el número; només el propietari el fa complir. Un despatxador que oblidi desactivar el seu botó continua sense poder ficar un setè membre.
L’última línia és el mecanisme. inject rep l’slice en si (createSlice fa servir name com a reducerPath per defecte, així que l’estat es munta a state.party, encaixant amb PartySliceShape), i importar el mòdul és el que executa la injecció. Això converteix l’slice en un mòdul d’estat: un mòdul federat el valor del qual és el seu efecte secundari. La config del bundler de l’app party l’exposa al costat de l’stack:
exposes: {
'./PartyStack': './src/PartyStack.tsx',
'./partySlice': './src/partySlice.ts',
},
L’app party també s’uneix al trio d’estat al seu mapa shared: @reduxjs/toolkit i react-redux amb el version declarat a mà que tot paquet amb mapa exports necessita en aquesta config, @pokedex/contracts com a singleton, cap d’ells eager, exactament com els declara l’app list. La mateixa regla del post 6: el host proveeix les còpies, els remotes les consumeixen.
Una observació del bucle de desenvolupament, perquè RTK es protegeix d’un perill aquí: sobre el paper, re-executar aquest mòdul intenta colar una funció amb identitat nova en un reducerPath existent, i l’inject d’RTK es nega a reemplaçar-la (un error de consola en desenvolupament; el reducer original continua viu). En aquest muntatge amb Re.Pack el perill es queda en teoria: editar el fitxer de l’slice recarrega l’app en lloc d’intercanviar el mòdul en calent, així que obtens un store nou en comptes d’una doble injecció. Val la pena saber quina de les dues coses fa el teu stack abans de fiar-te de cap.
Amb estat per renderitzar, la pestanya de farciment es converteix en pantalla. Les línies interessants de PartyScreen.tsx:
import { useDispatch, useSelector } from 'react-redux';
import { MAX_PARTY, type PartySliceShape } from '@pokedex/contracts';
import { remove } from './partySlice';
const members = useSelector((s: PartySliceShape) => s.party?.members ?? []);
Els buits plens renderitzen l’sprite, el nom i un control de treure que fa dispatch de remove(member.uid); els que queden per omplir conserven la vora discontínua fins a sis; la capçalera compta {members.length}/{MAX_PARTY}; tocar un membre empeny PokemonDetail amb { id }, el mateix DetailParams del post 5, sense camps nous. Fixa’t que el propietari llegeix el seu propi estat a través de la forma tolerant. Fins i tot aquí l’opcional es guanya el lloc: el primer render pot arribar a l’store abans que la primera acció, i la injecció registra el reducer però deixa state.party en undefined fins que arribi la següent acció.
Els mòduls d’estat es carreguen a l’arrencada
L’slice s’injecta quan el seu mòdul s’importa. De moment només l’importa una cosa: la pantalla de party, que també importa remove. Això vol dir que l’slice existeix només després que l’usuari obri la pestanya Party, i la Pokédex està a punt de fer-hi dispatch molt abans. Algú ha de carregar el mòdul d’estat aviat, i només un participant és viu a l’arrencada en tots els camins: el shell. apps/host/App.tsx, dins d’un efecte:
// Screens load on demand; state modules load at boot.
useEffect(() => {
import('partyApp/partySlice')
.then(() => store.dispatch(partyStateReady()))
.catch(err => console.warn('party state module failed to load', err));
}, []);
El host ja declara partyApp al seu mapa de remotes, així que això no afegeix cap acoblament que no tingués. Dispara l’import i no guarda cap referència al contingut del mòdul: la declaració ambient tipa el mòdul com a buit, perquè el host l’importa per l’efecte secundari i no ha d’anomenar res de dins. Les pantalles continuen sent lazy, perquè una pantalla que l’usuari no ha obert no costa res d’ajornar.
Carregar en arrencar és un avantatge de sortida, no una garantia. El chunk creua la xarxa, i res no impedeix que un dit ràpid arribi a un botó Add abans que ell. Un party/add despatxat en un store que no té reducer per a ell desapareix sense deixar rastre: ni avís ni error, perquè Redux ignora per disseny una acció que ningú no atén. Així que el resolve es fa visible, i aquí és on partyStateReady es guanya el sou. inject() canvia una entrada del mapa de reducers i reconstrueix el reducer combinat, però no despatxa mai, així que state.party continua indefinit fins que la següent acció passa pel reducer nou. El marcador és aquesta acció: el .then la despatxa tan bon punt el mòdul resol, state.party apareix, i cada subscriptor torna a renderitzar amb l’slice al seu lloc.
El costat d’escriptura tanca el cercle just aquí: el contenidor de L’escriptura arriba com a prop deshabilita Add mentre s.party sigui indefinit. Un toc que no pot passar és un dispatch que no es pot perdre.
La col·locació de l'efecte és una observació, no un diagnòstic. A l'àmbit del mòdul, aquest import produïa Can't perform a React state update on a component that hasn't mounted yet en algunes arrencades en fred i en altres no, i moure'l a un efecte va fer desaparèixer l'avís en arrencades en fred repetides. Què produeix aquesta actualització és una cosa que aquest post no ha arribat a fixar:
rootReducer.inject()no en té la culpa, perquè canvia una entrada del mapa de reducers i reconstrueix el reducer combinat sense despatxar res ni cridarreplaceReducer, així que per si sol no avisa ningú. Fins que l'actualització no es rastregi fins al seu origen, tracta la col·locació com una observació local: l'efecte és el muntatge que va fer callar l'avís, i un efecte és on React espera que visqui un efecte secundari.
Res no espera aquell import, així que un servidor de party caigut no pot bloquejar l’arrencada. Aquesta prova, val la pena fer-la en lloc de donar-la per bona. Atura el dev server de party i arrenca l’app en fred: el runtime de federació avisa ben alt per consola en desenvolupament que el fetch del manifest ha fallat ([ Federation Runtime ]: Failed to get manifest. #RUNTIME-003), i el shell continua endavant. La Pokédex renderitza, el comptador llegeix un 0/6 honest a través de la forma tolerant, el botó Add es queda desactivat perquè partyStateReady no s’ha arribat a disparar, i l’app funciona sense l’slice, que és exactament l’estat que PartySliceShape va ser dissenyada per descriure.
Un detall de l’execució observada: l’import es resol sense rebutjar, així que el .catch no arriba a disparar-se mai en aquest error; el runtime el conté i el reporta pel seu compte. El catch es queda: una línia que cobreix el camí del rebuig perquè una càrrega fallida no acabi mai sent un unhandled rejection.
L’escriptura arriba com a prop
El costat Pokédex del creuament comença a la vista de detall, i la vista de detall és una biblioteca de components instal·lada: l’únic lloc on l’escriptura no ha de viure. L’assaig de propietat va traçar aquesta línia: un component compartit renderitza el que li donen; una escriptura que creua una frontera de domini la connecta el consumidor. Així que @pokedex/detail 3.1.0 és una minor additiva amb tres props opcionals:
export interface PokemonDetailViewProps {
pokemon?: PokemonDetail;
loading: boolean;
error: boolean;
onRetry: () => void;
onAddToParty?: () => void;
addDisabled?: boolean;
addLabel?: string;
}
La vista renderitza un botó quan un consumidor li passa onAddToParty i no renderitza res quan no. Continua sense importar store, ni contracte, ni action creator. L’escriptura creua una frontera de domini, així que arriba com a callback i surt com un toc.
El contenidor de l’app list connecta les tres:
function PokemonDetailRoute({ route }: { route: { params: DetailParams } }) {
const { data, isLoading, isError, refetch } = useGetPokemonDetailQuery(route.params.id);
const dispatch = useDispatch();
const members = useSelector((s: PartySliceShape) => s.party?.members);
const partyReady = members !== undefined;
const count = members?.length ?? 0;
const full = count >= MAX_PARTY;
return (
<PokemonDetailView
pokemon={data}
loading={isLoading}
error={isError}
onRetry={refetch}
onAddToParty={() =>
data && dispatch(addToParty({ id: data.id, name: data.name, spriteUri: data.spriteUri }))
}
addDisabled={full || !partyReady}
addLabel={full ? 'Party is full' : 'Add to party'}
/>
);
}
Llegeix què fa aquell toc. Una pantalla de l’equip Pokédex fa dispatch d’una acció creada pel contracte, i aquesta acció acaba en un reducer de l’equip party. Tres parts, i cap no importa el codi d’una altra. L’estat desactivat llegeix el mateix MAX_PARTY que el reducer fa servir com a guarda, així que el botó i el límit no es poden desviar. També manté el botó avall fins que state.party existeix, així que l’únic dispatch que es podria perdre és el que no es pot arribar a fer. I la capçalera de la Pokédex obté el seu comptador amb la lectura idèntica:
const partyCount = useSelector((s: PartySliceShape) => s.party?.members.length ?? 0);
// ...
<Text style={styles.partyCount}>My Party {partyCount}/{MAX_PARTY}</Text>
L’app list no edita cap pin de versió per a res d’això: era a ^3.0.0 en tots dos paquets, i npm update @pokedex/contracts @pokedex/detail avança els dos carets fins a les minors noves. Només l’app endarrerida va tocar el seu package.json. Així es va pensar el caret: una minor arriba en demanar-la; una major exigeix editar a mà.
El contenidor de l’equip és l’altre consumidor de la mateixa vista, i no connecta cap de les tres props. Un Pokémon obert des de dins de l’equip no mostra cap botó d’afegir:
Una vista, dos consumidors, un d’ells connectant una escriptura. Aquesta captura és la regla de components de l’assaig de propietat en plena execució.
L’equip necessita dades pròpies
Aquell contenidor del costat party és el mur promès abans. Tocar un membre de l’equip empeny PokemonDetail dins de l’stack de l’app party, i el contenidor de darrere necessita getPokemonDetail, que viu a l’app list, que l’app party no pot importar. Les apps depenen de contractes, mai entre si, i aquesta és la primera vegada que la regla costa alguna cosa visible.
La sortida temptadora és un paquet de dades compartit: @pokedex/data, propietari de l’endpoint que totes dues apps necessiten. Eliminaria la duplicació, i per a dues features d’un mateix equip fins i tot podria ser el correcte. El preu és el que l’assaig de propietat va posar sobre la taula: un paquet compartit posa l’accés a dades de party al tren de releases d’un altre equip. Les releases compatibles no obliguen ningú a moure’s, però el dia que party necessiti un arranjament en aquell paquet, o hi aterri una versió que trenca, la data de sortida de party espera la revisió i la release d’un altre equip. Vint línies de definició de query no justifiquen aquest acoblament. L’app party escriu el seu propi apps/party/src/detailApi.ts amb el mateix nom d’endpoint, el mateix queryFn i el mateix parsePokemonDetail del contracte, copiat a propòsit:
const detailApi = baseApi.injectEndpoints({
endpoints: build => ({
getPokemonDetail: build.query<PokemonDetail, number>({
// byte for byte, the list app's definition
}),
}),
});
export const { useGetPokemonDetailQuery } = detailApi;
Dues apps injecten ara un endpoint anomenat getPokemonDetail en un sol baseApi, i RTK se n’adona. Executa l’app, obre la pestanya Party, i la consola de desenvolupament imprimeix:
called `injectEndpoints` to override already-existing endpointName getPokemonDetail without specifying `overrideExisting: true`
Si ho deixes així, RTK omet la segona injecció i conserva la primera: amb soroll en desenvolupament, en silenci a producció, on aquella guarda es compila fora. Aquest build declara el duplicat: totes dues apps passen overrideExisting: true, l’error de consola desapareix i guanya la darrera injecció. En tots dos casos decideix l’ordre de càrrega, i cap de les dues apps no controla l’ordre de càrrega. Aquesta és la raó honesta per la qual les dues definicions han de ser idèntiques i no només semblants.
El cache no canvia: un sol nom d’endpoint vol dir un sol conjunt d’entrades, així que obrir Bulbasaur des de la Pokédex i després des del party costa un sol fetch. El parany és la deriva: es declari o no, la còpia que afavoreixi l’ordre de càrrega guanya en silenci per a totes dues, i un cop overrideExisting ha fet callar la consola ja no queda cap avís per ignorar. La regla, doncs: mantén les còpies idèntiques byte a byte, declara la col·lisió perquè es vegi deliberada, o posa-li a l’endpoint un nom teu.
Ara trenca-ho
L’afirmació que cal atacar: la guarda sobre state.party és el que hi ha entre un chunk lent i un toc perdut. Primer treu la guarda (torna l’addDisabled del contenidor a un simple full) i després comenta l’import d’arrencada:
// useEffect(() => {
// import('partyApp/partySlice')
// .then(() => store.dispatch(partyStateReady()))
// .catch(err => console.warn('party state module failed to load', err));
// }, []);
Rellança en fred, i no obris la pestanya Party. Això importa: PartyScreen importa remove del fitxer de l’slice, així que visitar la pestanya injecta l’slice com a efecte secundari i amaga el bug. Un usuari que va directe a la Pokédex és la manera de reproduir-ho.
Toca Bulbasaur. Toca Add to party. El toc aterra, el comptador llegeix 0/6, i no passa res més. Sense avís, sense error, sense línia de consola. El trenca-ho del post 6 almenys penjava un spinner que podies mirar. Aquí el dispatch va arribar a l’store, l’store no va trobar cap reducer registrat per a party/add, i Redux va fer el que Redux fa amb una acció sense destinatari: res, per disseny. El toc de l’usuari va caure en un buit que abans era un error.
Ara diguem la part més esmolada en veu alta. Sense l’import d’arrencada, l’slice acaba existint tard o d’hora: una visita a la pestanya Party l’injecta, i totes les incorporacions d’allà endavant funcionen. Així que l’error té una forma més estreta i més lletja: cada intent d’afegir fet abans que l’usuari obri per casualitat la pestanya del propietari es perd en silenci, i això es reprodueix per a uns usuaris i per a altres mai, segons l’ordre de pestanyes. I l’import d’arrencada per si sol només estreny la finestra, de «fins que l’usuari obri la pestanya Party» a «fins que aterri el chunk». Tancar-la no pot, perquè la xarxa no et deu res.
Tancar-la és feina de la guarda: torna a posar la guarda, deixa l’import comentat, i la mateixa arrencada en fred mostra un botó Add senzillament desactivat. Lleig, visible i honest: un toc que no pot passar en lloc d’un toc que menteix.
Restaura l’import, rellança, i el botó es desperta tan bon punt es dispara partyStateReady; el mateix toc puja el comptador a 1/6. El patró són les tres peces: l’import d’arrencada per a l’avantatge de sortida, el marcador perquè l’slice afiori, i la guarda per a la finestra que cap dels dos no pot tancar.
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-08-client-state /tmp/pokedex-ref-08
cp -R /tmp/pokedex-ref-08/. .
L’abocada és també com el comportament aconsegueix la seva prova: porta els dos tests de regressió que demana el disseny d’aquest post (l’store de party recorregut per la finestra desaparèixer-injectar-marcador-afegir, i la guarda de l’Add del contenidor de list mantinguda avall fins que hi ha readiness), els mocks i el muntatge de Jest sobre el qual corren, i les declaracions ambient de federació que el recorregut va resumir en lloc d’imprimir.
Executa-ho
Els paquets estan publicats i instal·lats, així que l’execució són tres dev servers i un build al 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
L’arrencada en fred aterra a la Pokédex amb el comptador a 0/6. No ho llegeixis com a prova que l’import d’arrencada ha funcionat: la forma tolerant retorna 0/6 tant si l’slice està registrat com si encara falta, que és justament per al que serveix. La prova és que el botó Add estigui habilitat, ja que només es desperta quan partyStateReady ha tret l’slice a la llum. Afegeix des d’un detall, mira el comptador pujar, i troba el membre assegut a la pestanya Party:
Continua afegint fins a sis i el límit arriba pels dos costats alhora: la sisena incorporació canvia en viu el botó del detall obert al seu estat desactivat, perquè el mateix selector que compta per a la capçalera compta per a addDisabled:
Treu un membre a la pestanya Party i el buit alliberat torna a la vora discontínua, el comptador baixa a tot arreu alhora, i la incorporació següent funciona. Un slice, un propietari, tres superfícies d’acord perquè totes llegeixen el mateix estat a través de la mateixa forma.
El que has construït, i el que ve
L’app ja té alguna cosa pròpia. L’equip viu en un slice que només és de l’app party, injectat a l’arrencada en un store que el host va muntar al voltant del rootReducer del contracte. Registrat, no omplert: state.party continua absent fins que la següent acció arriba al reducer combinat. El marcador partyStateReady és aquesta acció, i la forma tolerant de lectura cobreix cada instant abans que es dispari. L’única interacció que creua (addToParty, el seu límit, la seva forma de lectura) viatja versionada al contracte, l’escriptura arriba a la vista de detall compartida com una prop que la connecta el seu consumidor, i la party porta les seves pròpies dades en lloc de manllevar el calendari de releases d’un altre equip. També has vist l’error que aquest disseny existeix per prevenir: un dispatch contra un slice el propietari del qual no es va carregar mai, descartat sense fer soroll.
Queda una limitació honesta: reinicia l’app i l’equip desapareix. L’slice viu en memòria, la persistència és problema d’un altre post, i fingir el contrari seria la classe d’ampliació silenciosa d’abast que aquesta sèrie intenta evitar.
La pregunta més esmolada és la que aquest stack no deixa de plantejar. L’equip són sis elements i una regla, i creuar la frontera amb educació va costar un store, una minor del contracte, un reducer injectat i un import d’arrencada. RTK va fer possible el creuament; no el va fer petit. El següent: la sèrie reconstrueix aquesta mateixa app sobre TanStack Query i Zustand, l’stack que la majoria d’equips de React Native prova primer, i observa què li fa la federació a aquella tria.
Fonts
- Redux Toolkit:
combineSlices— el reducer arrel injectable iinject - Redux Toolkit:
createAction— els callbacks prepare, inanoiddins d’RTK - RTK Query: code splitting —
injectEndpointsi la guardaoverrideExisting - PokéAPI — l’API REST gratuïta de la qual l’app fa fetch
- react-native-module-federation — el repo d’acompanyament, al tag
post-08-client-state