Un remote federat que afegeix el seu propi slice a un store compartit mentre un altre remote hi creua

L'estat de client creua la frontera: remotes que porten els seus propis slices a React Native

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 posseeix res.

Aquest post li dona una cosa per posseir: 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 toquen les altres features sense tocar el propietari? L’assaig sobre propietat va respondre amb principis. Aquest post respon amb codi.

Això és el que construirem:

host el shellrootReducer.inject(partySlice)una sola instànciaimport('partyApp/partySlice')dispatch(addToParty(...))el comptador, amb tolerànciacableja el botó d'afegirno cableja cap escripturastore de Reduximport d'arrencada@pokedex/contracts 3.1.0rootReducer · addToParty ·MAX_PARTYremote listfa dispatch d'addToParty ·llegeix el comptadorremote partyposseeix partySlice · l'exposacom a mòdul@pokedex/detail 3.1.0onAddToParty opcional

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 el seu propietari hagi carregat, 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 posseeix les dades. Ningú no és propietari d’una entrada del cache; PokéAPI ho és.

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 posseeix, 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 al 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 el store del host va cablejar 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 el store va cablejar. 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 el 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 del store: el middleware, el Provider, el cablejat. 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 interface PartySliceShape {
  party?: { members: PartyMember[] };
}

addToParty és l’única acció de la qual es fa dispatch des de fora de l’equip. El contracte és el propietari de la forma de l’acció; el reducer de l’equip és el propietari del que 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.

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 del final d’aquest post 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. Publica:

cd packages/contracts
npm install && npm publish
+ @pokedex/contracts@3.1.0

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.0, i install respecta el lockfile encara que el caret acceptaria 3.1.0. 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 al 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, de manera útil, davant de la càmera. L’stack de l’equip 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 d’aquí a dues seccions. 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, i aquesta correspondència funciona entre apps construïdes per separat perquè tots dos costats sostenen el mateix objecte action creator. Contracts és un singleton, així que party/add és un sol creator, no dos que casualment comparteixen un string. 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’equip 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 al 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 carreguen a l’arrencada

L’slice s’injecta quan el seu mòdul s’importa. De moment l’importen dues coses: la pantalla de l’equip (que també importa remove), i res més. 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, una línia a nivell de mòdul:

// Screens load on demand; state modules load at boot.
import('partyApp/partySlice').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 referència al resultat: 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. L’estat carrega a l’arrencada, perquè l’estat ha d’existir abans del primer dispatch que hi apunti.

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ó reporta el fetch del manifest fallit ben alt en desenvolupament ([ 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, 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 guarda 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 llibreria 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 cableja 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 cableja les tres:

function PokemonDetailRoute({ route }: { route: { params: DetailParams } }) {
  const { data, isLoading, isError, refetch } = useGetPokemonDetailQuery(route.params.id);
  const dispatch = useDispatch();
  const count = useSelector((s: PartySliceShape) => s.party?.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}
      addLabel={full ? 'Party is full' : 'Add to party'}
    />
  );
}

Llegeix què fa aquell toc. Una pantalla de l’equip Pokédex fa dispatch d’un creator del contracte, i l’acció aterra 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. 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 cableja cap de les tres props. Un Pokémon obert des de dins de l’equip no mostra cap botó d’afegir:

La pantalla de detall oberta des de la pestanya Party: la mateixa vista compartida sense botó d'afegir, perquè el contenidor de l'equip no cableja cap escriptura

Una vista, dos consumidors, un d’ells cablejant 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’equip, 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: cada consumidor d’un paquet compartit es mou al compàs de les seves releases, així que els canvis d’endpoint de l’equip Pokédex passarien a frenar les dates de l’equip party. 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`

La segona injecció s’omet: amb soroll en desenvolupament, en silenci a producció, on aquella guarda es compila fora. Totes dues apps comparteixen la definició que va carregar primer, i amb ella les seves entrades de cache: obre Bulbasaur des de la Pokédex i després des de l’equip, i la segona obertura és un encert de cache, un fetch en total. Definicions idèntiques fan que el salt sigui inofensiu; aquí gairebé és la gràcia. La trampa és la deriva: si les dues còpies divergeixen algun dia, la que carregui primer guanya en silenci per a totes dues, i el soroll de consola que vas aprendre a ignorar era l’únic avís. La regla, doncs: injecta una definició idèntica i deixa que el salt dedupliqui, passa overrideExisting: true només com a acte deliberat, o posa-li al teu endpoint un nom propi.

Ara trenca-ho

L’afirmació que cal atacar: l’import d’arrencada del shell és el que fa fiable l’slice de l’equip, i no només educat. Comenta’l:

// import('partyApp/partySlice').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 al store, el 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 cada afegir d’allà endavant funciona. Així que l’error té una forma més estreta i més lletja: cada 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. Aquesta classe de bug és la raó que el patró sigui un import d’arrencada i no una esperança: les pantalles carreguen sota demanda, els mòduls d’estat carreguen a l’arrencada, i la diferència és si la forma del store depèn de l’historial de rutes de l’usuari.

Restaura la línia, rellança, i el mateix toc puja el comptador a 1/6.

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 ja a 0/6: l’import d’arrencada ha fet la seva feina abans que res de l’equip hagi renderitzat. Afegeix des d’un detall, mira el comptador pujar, i troba el membre assegut a la pestanya Party:

El flux d'afegir a iOS: la capçalera de la Pokédex llegeix My Party 0/6, un detall s'obre amb el botó Add to party, el toc aterra, el comptador puja a 1/6, i la pestanya Party mostra Bulbasaur al primer buit

Continua afegint fins a sis i el límit arriba pels dos costats alhora: el sisè afegir 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:

La pantalla de detall amb l'equip al límit: el botó d'afegir desactivat i reetiquetat com a Party is full

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 el següent afegir 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 posseeix alguna cosa. L’equip viu en un slice del qual només l’app party és propietària, injectat a l’arrencada en un store que el host va cablejar al voltant del rootReducer del contracte. 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 cableja 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 mai no va carregar, 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

Warren de Leon
Warren de Leon

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

Veure perfil