Nagtapos ang post 8 sa pangako ng post na ito: ang parehong app na muling binuo sa TanStack Query at Zustand, ang stack na unang inaabot ng karamihan sa mga React Native team. Nandiyan na ang build na iyon, sa isang branch na ginawa mula sa tag ng post 8.
Ang mga patakarang itinakda ko sa sarili: parehong tatlong app, parehong naka-install na detail package, parehong mga feature (ang Pokédex list, ang detail na may naka-cap na Add button, ang party grid na may kontrol sa pagtanggal, ang live na bilang na “My Party 3/6”), at walang pagbabagong hindi ipinilit ng state stack. Magkamukha ang tapos na kambal sa simulator, sa bawat screen, at iyon ang control na kailangan ng paghahambing na ito.
Ang hatol, bago ang patunay. Mahusay na binubuo ng dalawang stack ang app na ito. Mas kaunti ang gumagalaw na piyesa ng bersiyong TanStack at Zustand, at para sa pagdaragdag ng server state mula sa isang remote, ito ang mas maginhawa sa dalawa pagdating sa federation: walang irerehistro, walang i-inject. Dalawang tiyak na bagay ang binibili ng Redux Toolkit: isang store na lumalaki sa runtime, at isang saradong hanay ng mga write. Mahalaga lang ang mga iyon kapag ang mga team na hiwalay na nagde-deploy ay nagbabahagi ng live na client state na may mga patakarang dapat tuparin. Kung hindi kailanman aabot ang app mo sa puntong iyon, mananalo ang mas magaan na stack at hindi na aangkop sa iyo ang karamihan ng argumentong ito.
Pumapasok ang mga arrow sa contract sa panig ng Redux at lumalabas mula rito sa kambal: hinahayaan ng Redux ang mga remote na magdagdag sa seam sa runtime, at sa branch na ito, kinokonsumo na lang nila ang dala ng seam. Nasa parehong kalahati ang @pokedex/detail na may parehong bersiyon, dahil iisang package ito, walang pagbabago.
Server state: walang dapat i-inject
Desentralisado ang modelo ng TanStack Query, at iyon ang pinakamagandang argumento nito rito. Walang central na API object, walang hakbang ng pagrerehistro, walang build-time na listahan ng kung ano ang mayroon. Anumang component sa anumang bundle ay tumatawag ng useQuery gamit ang anumang key laban sa shared na client. Ang isang remote na inilabas isang taon pagkatapos ng shell ay maaaring magdagdag ng server state sa tumatakbong app sa pamamagitan ng pagtawag ng hook.
Kinailangan ng post 8 ang baseApi.injectEndpoints, isang shared na instance, at ang middleware na nagpapatakbo ng cache nito. Ang pag-access ng data ng list app sa branch na ito ay isang fetch at isang hook:
// apps/list/src/listApi.ts
async function fetchPokemonList(): Promise<PokemonSummary[]> {
const res = await fetch('https://pokeapi.co/api/v2/pokemon?limit=151');
if (!res.ok) {
throw new Error(`PokéAPI responded ${res.status}`);
}
return parsePokemonList(await res.json());
}
export function usePokemonList() {
return useQuery({ queryKey: pokemonKeys.list(), queryFn: fetchPokemonList });
}
Ini-import ang parsePokemonList mula sa contract at byte-for-byte na kapareho ng sa post 8. Ang pag-parse gamit ang Zod, ang modelo, ang mga hugis: lahat ng iyon ay nakaligtas sa palit, dahil ang data model ay pag-aari ng domain at hindi ng kung anong library ang humahawak ng cache ngayong linggo.
Suportado ang pagbabahagi ng isang client sa mga bundle na hiwalay na binuo, pero may isang paalala. Ang sagot ng v5 migration guide para sa mga micro-frontend ay ang pagpapasa ng queryClient sa isang hook o provider, kapalit ng custom context ng v4. Isang talata iyon sa isang migration guide, hindi isang kabanata tungkol sa federation. Ang mga failure mode sa React Native ay ang mga singleton na nakilala na ng seryeng ito: dalawang kopya ng @tanstack/react-query ay magbibigay sa iyo ng “No QueryClient set”, dalawang kopya ng React ay magbibigay ng null na mga hook.
Sa ibang lugar lumalapag ang gastos. Pinapalawak ng mga injected na endpoint ng RTK ang isang typed na surface, kaya nakikita ng team na nagdaragdag ng endpoint ang buong API at ang tag graph na ibinabahagi nito. Ang koordinasyon ng TanStack ay isang array ng mga string. Ang sagot ko ay ang contract package na naman: kasama ng client ang key factory, kaya ang pagpapalit ng pangalan ay isang version bump na nakikita ng lahat ng consumer.
// packages/contracts/src/query.ts
export const queryClient = new QueryClient();
export const pokemonKeys = {
all: ['pokemon'] as const,
list: () => [...pokemonKeys.all, 'list'] as const,
detail: (id: number) => [...pokemonKeys.all, 'detail', id] as const,
};
Rekomendasyon iyon, hindi dokumentadong practice. Ang mga key factory ang dokumentadong sagot sa kalat ng mga key sa scale; walang anuman sa docs ang tumatalakay sa dalawang team na nagmamay-ari ng mga key sa ilalim ng iisang prefix.
May mas tahimik na salamin dito ang banggaan ng pangalan ng endpoint sa post 8. Sa RTK, dalawang app na nag-i-inject ng endpoint na tinatawag na getPokemonDetail ay nagbabahagi ng definition na unang nag-load: isang console error sa development, katahimikan sa production. Sa TanStack, dalawang app na tumatawag ng useQuery gamit ang pokemonKeys.detail(1) ay nagbabahagi ng iisang cache entry, dahil kinukumpara ang mga key ayon sa value. Walang nirerehistro, kaya walang nagbabanggaan. Buksan si Bulbasaur sa Pokédex at buksan siyang muli mula sa Party tab, at ang pangalawang screen ay magre-render mula sa cache nang walang request. Ang masamang bersiyon niyon ay dalawang team na nag-iba na ang mga queryFn, kung saan nakadepende ang data sa kung aling screen ang unang binuksan ng user.
Isang maliit na bagay ang isinuko ng shell. Dala ng baseApi ng post 8 ang base URL para sa lahat ng consumer. Dito, nagfe-fetch ang bawat queryFn ng kahit ano, at walang say ang host sa kung saan nanggagaling ang data ng isang remote.
Invalidation: ang prefix ay isang pangako
Ang Refresh button ng host ay bahagi ng interface ng host, na umaabot sa data ng isang remote nang hindi nag-i-import ng anuman mula rito. Nag-dispatch ang post 8 ng invalidateTags(['PokemonList']). Ang kambal:
<Pressable onPress={() => queryClient.invalidateQueries({ queryKey: pokemonKeys.all })}>
Tumutugma ang mga query filter ayon sa prefix maliban kung hihingin mo ang exact, kaya lahat ng binubuo ng factory sa ilalim ng ['pokemon'] ay sabay na nagiging stale at nagre-refetch ang anumang naka-mount na query. Ang prefix ang buong kasunduan.
Ikinumpara ko ang dalawang modelong ito sa tags laban sa query keys, at totoo pa rin ang argumentong iyon dito. Isang bagay ang idinaragdag ng federation: ang dalawang panig ay mga bundle na hiwalay na dine-deploy at hindi kailanman nagsama sa iisang build. Ang tag ay isang value na idineklara sa isang shared na listahan; ang key prefix ay pagkakapareho ng string sa code na inilalabas sa magkakaibang araw. Ang paglalagay ng factory sa versioned na contract ay nagbabalik sa kombensiyon sa isang bagay na nakikita ng compiler at ng proseso ng release.
Client state: bumabalik ang store sa shell
Dito pinaka-nagkakaiba ang dalawang disenyo.
Sa post 8, ang party app ang lumikha ng state ng party, siyang nagmamay-ari nito, at nag-inject nito sa isang store na nabuo na ng host. Para mismo doon ang combineSlices, at dumarating ang slice sa runtime mula sa isang bundle na wala man lang alam ang shell tungkol dito.
Walang katumbas ang Zustand. Walang nagdaragdag ng store sa isang tumatakbong application pagkatapos ng paglikha, at hindi ko iyon binabasa bilang pagkukulang: ang natural na federated na sagot ng Zustand ay ilang independent na store, isa bawat remote, na walang anumang makinarya. Gumagana ang sagot na iyon hanggang sa may piraso ng state na kailangang tumawid ng hangganan ng module, na siyang kaso ng buong seryeng ito.
Kaya nasa contract package ang store:
// packages/contracts/src/partyStore.ts
export const partyStore = createStore<PartyState>((set, get) => ({
members: [],
add: member => {
if (get().members.length >= MAX_PARTY) return; // the cap lives with the owner's action
set(state => ({ members: [...state.members, { ...member, uid: uid() }] }));
},
remove: id => set(state => ({ members: state.members.filter(m => m.uid !== id) })),
}));
createStore mula sa zustand/vanilla, dahil walang React ang contract. Nagsu-subscribe ang mga consumer gamit ang useStore(partyStore, selector).
Ang pagbaligtad ng pagmamay-ari ang tapat na gastos. Hindi na isinusulat ng party app ang mga patakaran ng party; nagbabasa ito ng state na dumarating sa isang package na ini-install nito. Burado na ang partySlice.ts nito, pati ang entry na ./partySlice sa Module Federation config nito, kaya iisang module na naman ang inilalantad nito sa halip na dalawa. Sa panig ng host, wala na ang src/store.ts, wala na ang boot import na naglo-load ng state module ng party, at sumama sa kanila ang ambient declaration na nagbibigay ng type nito.
Ang mga pagbuburang iyon ang kapalit. Naglaan ang post 8 ng isang seksiyon sa isang dispatch na naglalaho dahil hindi pa nag-lo-load ang may-ari ng slice, at naroon ang boot import ng shell para pigilan iyon. Dito, hindi maaaring mangyari ang pagkabigong iyon: nandiyan na ang state sa sandaling may mag-import ng contract. Nabayaran na ang presyo nang mas maaga, bilang release. Ang bawat bagong piraso ng shared state ay isang bagong bersiyon ng @pokedex/contracts na ini-install ng lahat ng app: isang deploy sa web, isang ikot ng App Store sa mobile. Walang stack sa dalawa ang nag-aalis ng koordinasyon; parehong inililipat lang ito sa ibang araw.
Ang component package ang control group. Hindi nagalaw ang @pokedex/detail 3.1.0: parehong bersiyon, parehong file, naka-install sa dalawang app, nakakabit sa isang Zustand action sa isang branch at sa isang Redux action creator sa kabila. Ang view na nagre-render ng iniaabot dito ay walang pakialam kung aling library ang nagkalkula nito. Iyon ang bunga ng patakaran ng library mula sa post 6.
Pagmamay-ari: setState mula kahit saan
Ang cap ay patakarang pag-aari ng party app, at sa branch na ito, nasa loob ito ng add. Bawat write na dumadaan sa add ay saklaw ng cap. Public ang setState sa bawat Zustand store, at hindi ito dumadaan sa add.
Ang buong demonstrasyon ay nasa detail container ng list app, sa tabi ng lehitimong pagkakabit:
// apps/list/src/ListStack.tsx — a module with no business writing this state
const bypassTheCap = () =>
data &&
partyStore.setState(s => ({
members: [
...s.members,
{ uid: `bypass-${s.members.length}`, id: data.id, name: data.name, spriteUri: data.spriteUri },
],
}));
Walang type error. Walang babala. Walang mare-review ang team na may-ari. Napupuno ang party hanggang anim, kusang nagdi-disable ang Add button, at ang isang long-press sa parehong screen ay naglalagay ng pampitong Pokémon sa isang party na may anim na puwesto:
Nandiyan lang ang long-press para makunan ang bypass. Ang tawag mismo ang punto.
Panoorin ang header. Anim na puwesto ang nire-render ng party grid, kaya walang mapupuntahan ang pampitong miyembro, at ang bilang na 7/6 ang tanging surface na nagpapakita nito. Hindi maipapakita ng sariling UI ng team na may-ari ang state na kasusulat lang ng isang banyagang module.
Pinapayagan din ng Redux ang mga banyagang write: kahit anong module ay maaaring mag-dispatch ng kahit ano. Ang naiiba, idineklara ng slice ang bawat write na igagalang ng may-ari, kaya ang action na hindi kailanman idineklara ng may-ari ay walang binabago, at nababasa sa iisang file ang mga lehitimong mutation. Inilalantad ng Zustand store ang state mismo, at ang patakarang nasa loob ng isang function ay umaabot lamang sa mga tumatawag ng function na iyon.
Sa loob ng iisang team, hindi ito problema. Alam ng lahat na naroon ang setState, nahuhuli ito ng code review, at tumatagal ang kombensiyon dahil magkakatabi ang mga humahawak nito. Sa sandaling magkaibang team na may magkaibang iskedyul ng release ang sumusulat at ang may-ari, kombensiyon ang hawak mo sa halip na hangganan.
Pagpili
Walang ganap na panalo. Ang nagpapasya ay kung sino ang may pahintulot na sumulat ng ano.
- Iisang team ang may-ari ng lahat ng state. TanStack Query at Zustand. Mas kaunting konsepto, mas kaunting seremonya, walang i-inject, at ang
setStateay isang ginhawa sa halip na butas. - Mga independent na team, shared na server cache, walang shared na client state. TanStack pa rin. Ibahagi ang client bilang singleton, ilagay ang key factory sa isang versioned na contract, at nasa iyo na ang koordinasyong kailangan.
- Mga independent na team na sumusulat sa client state ng isa’t isa, na may mga patakarang dapat tuparin. Para sa kasong ito ang bigat ng Redux Toolkit: isang store na lumalaki sa runtime at isang saradong hanay ng mga write bawat slice.
- Mobile, na may mabagal na ikot ng release. Timbangin ang hugis ng pagbabago. Sa branch ng Zustand, ang bawat bagong piraso ng shared state ay isang release ng contract, at sa mobile, naghihintay ang release sa review ng app store.
Ang hudyat na dapat bantayan ay ang araw na kailangan ng pangalawang team na sumulat ng state na pag-aari ng una. Pagkatapos ng araw na iyon, ang tanong ay kung mas gusto mong mag-coordinate sa pamamagitan ng type o ng kombensiyon.
Nasa sarili nitong branch ang kambal, ginawa mula sa tag ng post 8, sa post-09-tanstack-zustand. Isang fork ito, hindi hakbang pasulong: nagpapatuloy ang serye mula sa post 8 sa main.
Susunod, ang kabilang kalahati ng paghahambing. Mananatili ang stack at mahahati sa dalawa ang backend: REST para sa list, GraphQL para sa mga type badge, isang client, isang cache, dalawang team.
Mga Sanggunian
- TanStack Query: paglipat sa v5 — ang sagot para sa mga micro-frontend, pagpapasa ng
queryClientkapalit ng custom context - TanStack Query: mga query filter — pagtutugma ayon sa prefix, at
exact - TanStack Query: query keys at effective query keys — mga key factory sa scale
- Zustand:
createStoreatuseStore— ang vanilla store at ang subscription nito mula sa React - Redux Toolkit:
combineSlicesat RTK Query: code splitting — ang sinasandigan ng kabilang branch - react-native-module-federation — ang kasamang repo, sa tag na
post-09-tanstack-zustand