Isang federated remote na nagdadagdag ng sarili nitong slice sa isang shared store habang tumatawid dito ang isa pang remote

Client state sa kabila ng seam: mga remote na may dalang sariling slice sa React Native

Nagtapos ang post 6 sa isang tapat na limitasyon: isara ang app, buksan ulit, at walang natitira sa lahat ng ginawa mo. Walang party, walang favourites, walang alaala. Server state ang lahat ng tumawid sa seam hanggang ngayon: data na pag-aari ng PokéAPI, nakatago sa isang cache na pinaghahatian ng lahat ng module. Ang app mismo, wala pa ring pag-aari.

Bibigyan ito ng post na ito ng mapag-aari: ang party ng anim. Ang client state ay walang server sa likod at walang cache na maire-refetch. Nasa isang Redux slice ito, at sa ilalim ng federation, ang slice ang nagtatanong ng tanong na paikot-ikot sa buong serye: sino ang may-ari nito, at paano ito hahawakan ng ibang feature nang hindi hinahawakan ang may-ari? Sumagot ang sanaysay tungkol sa pagmamay-ari gamit ang mga prinsipyo. Sasagot ang post na ito gamit ang code.

Ang hugis na bubuuin natin:

host ang shellrootReducer.inject(partySlice)iisang instanceimport('partyApp/partySlice')dispatch(addToParty(...))ang bilang, nangmapagparayanagkakabit ng Add buttonwalang ikinakabit na writeRedux storeboot import@pokedex/contracts 3.1.0rootReducer · addToParty ·MAX_PARTYlist remotenagdi-dispatch ng addToParty· nagbabasa ng bilangparty remotemay-ari ng partySlice ·inilalantad bilang module@pokedex/detail 3.1.0opsyonal na onAddToParty

Isang ideya ang hawakan sa isip: sa lahat ang cache, pero ang isang client-state slice ay may eksaktong isang may-ari. Ang party app ang may-ari ng party. Lahat ng kailangan ng ibang module (isang action, isang hugis ng pagbasa, isang cap) ay tumatawid sa contract, at malapit sa dulo magdi-dispatch tayo papunta sa slice bago pa mag-load ang may-ari nito, sinasadya, para makita kung ano ang gagawin ng Redux sa isang action na walang nakikinig.

Magpatuloy mula sa sarili mong code ng post 6 kung sinabayan mo ang pagbuo. Kung hindi, magsimula sa huling estado nito:

git clone https://github.com/warrendeleon/react-native-module-federation
cd react-native-module-federation
git checkout post-06-shared-store

Sino ang may-ari ng client state?

Tatlong klase ng state na ang nasa app na ito, at magkakaiba ang may-ari ng bawat isa.

Ang server cache ay sa lahat. Binuo ito ng post 6: isang baseApi sa contract, isang store sa host, mga endpoint na ini-inject ng may-ari ng data. Walang may-ari ng isang cache entry; ang PokéAPI ang may-ari.

Ang private state ay may isang may-ari at nananatili sa loob nito. Kung sinong miyembro ang hawak ng party, paano gumagana ang pagtanggal, ano ang ibig sabihin ng cap: lahat iyan ay usapin ng party app, sa isang slice na walang ibang nag-i-import.

Ang interaksiyong tumatawid ang makitid na guhit sa pagitan nila. Kailangan ng Pokédex na magdagdag ng miyembro sa isang party na hindi nito pag-aari, at gusto ng header nitong ipakita ang “My Party 3/6” para sa state na hindi nito nakikita. Ang mga pagtawid na iyon ay tina-type, pinapangalanan, binibigyan ng bersiyon at inilalagay sa contract, dahil totoo rin dito ang patakaran ng sanaysay sa pagmamay-ari: hindi kailanman nagdedepende ang mga app sa isa’t isa. Nagdedepende ang mga app sa mga contract.

Ang taxonomy na iyan ang nagpapasya ng lahat ng iba pa sa post. Ang susunod ay bawat hilera nito, binuo.

Ang pader, at lumipat ang reducer

Kailangang sumali ng slice ng party sa tumatakbong store, at may API ang Redux Toolkit (RTK) para mismo dito: gumagawa ang combineSlices ng reducer na may inject method, at ang slice na na-inject sa runtime ay nagsisimulang mag-reduce mula sa sandaling iyon. Kaya kailangang tawagin ng party app ang inject sa mismong reducer object na ikinabit ng store ng host.

Inline na binuo ng host ng post 6 ang reducer na iyon:

// apps/host/src/store.ts, sa post 6
export const store = configureStore({
  reducer: combineSlices(baseApi),
  middleware: getDefaultMiddleware => getDefaultMiddleware().concat(baseApi.middleware),
});

Walang maaabot. Ang reducer ay isang expression sa loob ng source ng host, hindi kailanman na-export, at kahit may export hindi rin makakatulong: hindi nag-i-import ng app ang mga app. Ang kopya ay mas masama pa sa walang silbi: ang combineSlices(baseApi) sa party app ay gumagawa ng pangalawang reducer na walang store na nagpapatakbo, at ang pag-inject doon ay walang binabago sa screen.

Ito ang argumento ng identity mula sa post 6, sa pangatlong pagkakataon. Lumipat ang baseApi sa contract dahil ang consumer ay makakapag-inject lang ng mga endpoint sa parehong instance na ikinabit ng store. Lumilipat ang root reducer sa parehong dahilan, isang file lang ang layo. packages/contracts/src/store.ts:

import { combineSlices } from '@reduxjs/toolkit';
import { baseApi } from './api';

export const rootReducer = combineSlices(baseApi);

At pumapayat ang store ng host sa isang 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),
});

Ang host pa rin ang may-ari ng store: ang middleware, ang Provider, ang pagkakabit. Ang contract na ngayon ang may-ari ng reducer tulad ng pagiging may-ari nito ng api instance. Dahil singleton ng federation ang contracts, ang rootReducer ay iisang object sa buong runtime, at ang slice na na-inject ng isang app na binuo buwan pagkatapos ng shell ay lumalapag sa reducer na pinapatakbo na ng shell.

Dala ng contract ang pagtawid

Ngayon ang gitnang hilera ng taxonomy. Ang mga interaksiyong tumatawid ng hangganan ng app ay pupunta sa bagong packages/contracts/src/party.ts, sinadyang maikli:

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[] };
}

Ang addToParty ang nag-iisang action na dini-dispatch ng anumang nasa labas ng party. Ang contract ang may-ari ng hugis ng action; ang reducer ng party ang may-ari ng ibig sabihin nito. Ang prepare callback ang nagtatatak sa bawat miyembro ng nanoid() (kasama na sa RTK, walang bagong dependency) para manatiling pure ang reducer at manatiling magkakaiba ang dalawang kopya ng parehong Pokémon. Pinapayagan ang mga duplicate sa disenyo: ang party ng anim na Magikarp ay isang lehitimong desisyon sa buhay, at ang uid ang nagbubukod sa kanila kapag tinanggal ang isa.

Nakaupo ang MAX_PARTY sa seam dahil bawat surface na sumasalamin sa patakaran ay kailangang magbasa ng parehong numero: isang naka-disable na button sa isang app, isang bilang sa isa pa, ang guwardiya sa reducer ng may-ari.

Ang PartySliceShape ang panig ng pagbasa, at ang opsyonal na marka ang disenyo, hindi depensa. Ang slice ay ini-inject sa runtime ng isang module na hindi kontrolado ng nagbabasa, kaya sa sandaling magbasa ang isang banyagang module, maaaring wala pa ang state.party. Sumusulat ang nagbabasa ng s.party?.members ?? [] at nagre-render ng tapat na sagot sa parehong kaso. May mga typed na alternatibo (kaya ng withLazyLoadedSlices ng RTK na ihabi ang posibleng-wala na slice sa pamamagitan ng declaration merging), pero itinuturo ng mapagparayang hugis ang sitwasyon sa halip na itago, at ang sabotahe sa dulo ng post na ito ay nakadepende sa pag-unawa mo rito.

Pansinin ang wala. Magkakaroon din ang party ng remove action, at hindi ito lumalabas sa anumang contract, dahil walang ibang nagdi-dispatch nito. Dala ng contract ang tumatawid, wala nang iba; ang entry na walang kumokonsumo ay isang pananagutan na may bersiyon.

Parehong additive ang dalawang bagong file, kaya minor ang bersiyon. I-publish:

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

Parehong nasa ^3.0.0 ang host at ang list, at dito mas mahalaga ang lockfile kaysa sa caret. Walang binabago ang karaniwang npm install: nakapako ang lockfile sa 3.0.0, at iginagalang ng install ang lockfile kahit tatanggapin ng caret ang 3.1.0. Ang pagpapasulong ng caret ay may sariling utos:

( cd apps/host && npm update @pokedex/contracts )

Walang pag-edit ng package.json, isang linya ng lockfile ang gumalaw. Ang pin ay ginagalaw lang kapag tumawid ang consumer ng major. May isang consumer na malapit nang gumawa niyan.

Ang may-ari

Ang party app ang naging control group ng serye: contract sa ^1.0.0, detail sa ^1.0.0, walang access sa store, isang static na grid ng anim na bakanteng puwesto. Tinatapos ng pagpapalaki ng slice ang lahat ng iyan, at ang pag-adopt ay isang sinadyang pag-edit dahil ang dalawang major ay totoong layo. 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, at agad nasisira ang build, kapaki-pakinabang, sa harap ng kamera. Minomount pa rin ng stack ng party ang detail screen sa paraang 1.0.0: import PokemonDetailScreen from '@pokedex/detail', diretso sa component=. Wala nang default export ang 3.x na package, kaya ang import na iyon ay nagre-resolve na ngayon sa namespace object ng package, at hindi na pumapasa sa type check ang mount:

error TS2322: Type 'typeof import(".../@pokedex/detail/dist/index")' is not
assignable to type 'ScreenComponentType<PartyParamList, "PokemonDetail"> | undefined'.

Tatlong major ng detail package ang lumipas sa paligid ng app na ito habang hindi ito nakatingin. Ang ine-export ng 3.x ay ang may pangalang PokemonDetailView, isang view na humihingi ng data bilang props, kaya pinipilit ng compile error ang party na isulat ang parehong walong-linyang container na isinulat ng list app sa post 6. Nangangailangan ng data ang container, at magiging sarili nitong pader iyon pagkalipas ng dalawang seksiyon. Ang mga ganitong kadena ang totoong halaga ng pagkahuli sa bersiyon: hindi crash sa production, kundi salansan ng mga pader sa araw na sa wakas ay nag-adopt ka.

Ang slice ang puso ng post, at kasya ito sa isang screen. 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);

Dalawang bagay ang may dala ng disenyo. Ang tumatawid na action ay nakatali sa pamamagitan ng extraReducers: itinutugma ng party ang addToParty ng contract, at gumagana ang pagtutugma sa pagitan ng magkahiwalay na binuong app dahil hawak ng dalawang panig ang parehong action creator object. Singleton ang contracts, kaya ang party/add ay iisang creator, hindi dalawang nagkataong magkapareho ng string. At ang guwardiya ng cap ay narito, sa may-ari. Inilalathala ng contract ang numero; ang may-ari lang ang nagpapatupad nito. Ang dispatcher na nakalimutang i-disable ang button nito ay hindi pa rin makakapagpasok ng pampitong miyembro.

Ang huling linya ang mekanismo. Tinatanggap ng inject ang slice mismo (ginagamit ng createSlice ang name bilang default na reducerPath, kaya ang state ay nakamount sa state.party, kasya sa PartySliceShape), at ang pag-import ng module ang nagsasagawa ng injection. Ginagawa nitong state module ang slice: isang federated module na ang halaga ay ang side effect nito. Inilalantad ito ng bundler config ng party katabi ng stack:

exposes: {
  './PartyStack': './src/PartyStack.tsx',
  './partySlice': './src/partySlice.ts',
},

Sumasali rin ang party sa state trio sa shared map nito: ang @reduxjs/toolkit at react-redux na may version na idineklarang mano-mano na kailangan ng bawat package na may exports map sa config na ito, ang @pokedex/contracts bilang singleton, walang eager, eksaktong tulad ng deklarasyon ng list app. Parehong patakaran ng post 6: ang host ang nagbibigay ng mga kopya, ang mga remote ang kumokonsumo.

Isang obserbasyon sa dev loop, dahil may binabantayang panganib ang RTK dito: sa papel, ang muling pagpapatakbo ng module na ito ay sumusubok magpasok ng function na may bagong identity sa dati nang reducerPath, at tumatangging magpalit ang inject ng RTK (console error sa development; buhay pa rin ang orihinal na reducer). Sa setup na ito gamit ang Re.Pack, nananatiling teorya ang panganib: ang pag-edit sa file ng slice ay nagre-reload ng buong app sa halip na mag-hot-swap ng module, kaya bagong store ang makukuha mo sa halip na dobleng injection. Sulit malaman kung alin sa dalawa ang ginagawa ng stack mo bago magtiwala sa alinman.

May state nang ire-render, kaya nagiging screen na ang pansamantalang tab. Ang mga interesanteng linya ng 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 ?? []);

Nire-render ng mga punong puwesto ang sprite, ang pangalan, at isang kontrol sa pagtanggal na nagdi-dispatch ng remove(member.uid); pinapanatili ng mga bakante ang putol-putol na gilid hanggang anim; binibilang ng header ang {members.length}/{MAX_PARTY}; ang pagpindot sa miyembro ay nagtutulak ng PokemonDetail na may { id }, ang parehong DetailParams ng post 5, walang bagong field. Pansinin na binabasa ng may-ari ang sarili nitong state sa pamamagitan ng mapagparayang hugis. Kahit dito, sulit ang opsyonal: maaaring maunahan ng unang render ang unang action papasok sa store, at ang injection ay nagrerehistro ng reducer pero iniiwang undefined ang state.party hanggang dumating ang susunod na action.

Naglo-load ang mga state module sa boot

Nai-inject ang slice kapag na-import ang module nito. Sa ngayon dalawang bagay ang nag-i-import: ang screen ng party (na nag-i-import din ng remove), at wala nang iba. Ibig sabihin, nandiyan lang ang slice pagkatapos buksan ng user ang Party tab, at malapit nang mag-dispatch ang Pokédex papunta rito nang mas maaga pa. May kailangang mag-load ng state module nang maaga, at iisang kalahok lang ang buhay sa boot sa lahat ng landas: ang shell. apps/host/App.tsx, isang linya sa antas ng module:

// Screens load on demand; state modules load at boot.
import('partyApp/partySlice').catch(err =>
  console.warn('party state module failed to load', err),
);

Dineklara na ng host ang partyApp sa remotes map nito, kaya wala itong idinadagdag na pagkakabit na wala pa. Pinapagana nito ang import at hindi nagtatago ng reference sa resulta: tina-type ng ambient declaration ang module bilang walang laman, dahil ini-import ito ng host para sa side effect at wala itong pakialam sa anumang nasa loob. Nananatiling lazy ang mga screen, dahil walang gastos ang pagpapaliban ng screen na hindi pa binubuksan ng user. Naglo-load ang state sa boot, dahil kailangang nandiyan na ang state bago ang unang dispatch na nakatutok dito.

Walang naghihintay sa import na iyon, kaya hindi kayang harangin ng bumagsak na party server ang boot. Sulit gawin ang pagsubok na iyan sa halip na pagkatiwalaan. Ihinto ang dev server ng party at i-cold-start ang app: iuulat ng federation runtime ang bigong manifest fetch nang malakas sa development ([ Federation Runtime ]: Failed to get manifest. #RUNTIME-003), at tuloy-tuloy ang shell. Nagre-render ang Pokédex, nagbabasa ang bilang ng tapat na 0/6 sa pamamagitan ng mapagparayang hugis, at tumatakbo ang app nang walang slice, na siyang mismong estado na idinisenyo ng PartySliceShape na ilarawan. Isang detalye mula sa naobserbahang takbo: ang import ay nagse-settle nang hindi nagre-reject, kaya hindi kailanman tumatakbo ang .catch sa pagkabigong ito; kinokontrol ito ng runtime at inuulat nang mag-isa. Nananatili ang catch, isang linyang nagbabantay sa landas ng rejection para hindi kailanman mauwi sa unhandled rejection ang bigong pag-load.

Dumarating ang write bilang prop

Ang panig ng Pokédex sa pagtawid ay nagsisimula sa detail view, at ang detail view ay isang naka-install na component library: ang nag-iisang lugar kung saan hindi dapat tumira ang write. Iginuhit ng sanaysay sa pagmamay-ari ang linyang ito: ang shared component ay nagre-render ng ibinibigay dito; ang write na tumatawid ng hangganan ng domain ay ikinakabit ng consumer. Kaya ang @pokedex/detail 3.1.0 ay isang additive minor na may tatlong opsyonal na props:

export interface PokemonDetailViewProps {
  pokemon?: PokemonDetail;
  loading: boolean;
  error: boolean;
  onRetry: () => void;
  onAddToParty?: () => void;
  addDisabled?: boolean;
  addLabel?: string;
}

Nagre-render ang view ng button kapag inabutan ito ng consumer ng onAddToParty at walang nire-render kapag hindi. Wala pa rin itong ini-import na store, contract, o action creator. Tumatawid ang write ng hangganan ng domain, kaya dumarating ito bilang callback at umaalis bilang isang tap.

Ikinakabit ng container ng list app ang tatlo:

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'}
    />
  );
}

Basahin kung ano ang ginagawa ng tap na iyon. Ang screen na pag-aari ng Pokédex team ay nagdi-dispatch ng creator na pag-aari ng contract, at lumalapag ang action sa reducer na pag-aari ng party team. Tatlong panig, walang nag-i-import ng code ng iba. Binabasa ng disabled state ang parehong MAX_PARTY na ginagamit ng reducer bilang guwardiya, kaya hindi maghihiwalay ang button at ang cap. At nakukuha ng header ng Pokédex ang bilang nito sa parehong pagbasa:

const partyCount = useSelector((s: PartySliceShape) => s.party?.members.length ?? 0);
// ...
<Text style={styles.partyCount}>My Party {partyCount}/{MAX_PARTY}</Text>

Walang ine-edit na version pin ang list app para sa lahat ng ito: nasa ^3.0.0 ito sa parehong package, at ang npm update @pokedex/contracts @pokedex/detail ang nagpapasulong ng dalawang caret sa mga bagong minor. Ang nahuling app lang ang gumalaw ng package.json nito. Ito ang caret na gumagana ayon sa disenyo: dumarating ang minor kapag hiningi; nangangailangan ang major ng sinadyang pag-edit.

Ang container ng party ang isa pang consumer ng parehong view, at wala itong ikinakabit sa tatlong props. Ang Pokémon na binuksan mula sa loob ng party ay walang ipinapakitang Add button:

Ang detail screen na binuksan mula sa Party tab: ang parehong shared view na walang Add button, dahil walang ikinakabit na write ang container ng party

Isang view, dalawang consumer, isa sa kanila ang nagkakabit ng write. Ang screenshot na iyan ang component rule ng sanaysay sa pagmamay-ari, tumatakbo.

Kailangan ng party ng sariling data

Ang container sa panig ng party ang pader na ipinangako kanina. Ang pagpindot sa miyembro ng party ay nagtutulak ng PokemonDetail sa loob ng stack ng party, at kailangan ng container sa likod nito ang getPokemonDetail, na nasa list app, na hindi maaaring i-import ng party. Nagdedepende ang mga app sa mga contract, hindi kailanman sa isa’t isa, at ito ang unang pagkakataon na may nakikitang gastos ang patakaran.

Ang nakatuksong labasan ay isang shared data package: @pokedex/data, may-ari ng endpoint na kailangan ng dalawang app. Buburahin nito ang duplikasyon, at para sa dalawang feature ng iisang team, maaaring tama pa nga. Ang presyo ang inilatag ng sanaysay sa pagmamay-ari: bawat consumer ng shared package ay gumagalaw kasabay ng mga release nito, kaya ang mga pagbabago sa endpoint ng Pokédex team ay magha-harang na sa mga petsa ng party team. Hindi binibili ng dalawampung linya ng query definition ang ganoong pagkakabit. Sinusulat ng party ang sarili nitong apps/party/src/detailApi.ts na may parehong pangalan ng endpoint, parehong queryFn, at parehong parsePokemonDetail mula sa contract, sinadyang kinopya:

const detailApi = baseApi.injectEndpoints({
  endpoints: build => ({
    getPokemonDetail: build.query<PokemonDetail, number>({
      // byte for byte, the list app's definition
    }),
  }),
});

export const { useGetPokemonDetailQuery } = detailApi;

Dalawang app na ngayon ang nag-i-inject ng endpoint na nagngangalang getPokemonDetail sa iisang baseApi, at napapansin ito ng RTK. Patakbuhin ang app, buksan ang Party tab, at nagpi-print ang development console:

called `injectEndpoints` to override already-existing endpointName getPokemonDetail without specifying `overrideExisting: true`

Nilalaktawan ang pangalawang injection: maingay sa development, tahimik sa production, kung saan nakokompile palabas ang guwardiyang iyon. Hinahati ng dalawang app ang definition na unang nag-load, kasama ang mga cache entry nito: buksan si Bulbasaur mula sa Pokédex at pagkatapos mula sa party, at ang pangalawang pagbukas ay cache hit, iisang fetch lahat-lahat. Ginagawang hindi mapanganib ng magkaparehong definition ang paglaktaw; dito, halos ito na nga ang punto. Ang bitag ay ang paghihiwalay: kung maghiwalay balang araw ang dalawang kopya, ang unang nag-load ang tahimik na mananalo para sa dalawa, at ang ingay sa console na natutunan mong balewalain ang tanging babala. Ang patakaran, kung gayon: mag-inject ng magkaparehong definition at hayaang mag-deduplicate ang paglaktaw, magpasa ng overrideExisting: true bilang sinadyang hakbang lamang, o bigyan ang endpoint mo ng sariling pangalan.

Ngayon, sirain natin

Ang pahayag na aatakihin: ang boot import ng shell ang nagpapatibay sa slice ng party, hindi lang nagpapabuti ng asal. I-comment ito:

// import('partyApp/partySlice').catch(err =>
//   console.warn('party state module failed to load', err),
// );

Mag-relaunch nang malamig, at huwag buksan ang Party tab. Mahalaga iyan: nag-i-import ang PartyScreen ng remove mula sa file ng slice, kaya ang pagbisita sa tab ay nag-i-inject ng slice bilang side effect at nagtatago ng bug. Ang user na diretso sa Pokédex ang paraan ng pag-reproduce nito.

Pindutin si Bulbasaur. Pindutin ang Add to party. Lumapag ang tap, nagbasa ang bilang ng 0/6, at wala nang ibang nangyari. Walang babala, walang error, walang linya sa console. Ang pagsira sa post 6 ay may isinabit man lang na spinner na matititigan mo. Dito, umabot ang dispatch sa store, walang nakitang rehistradong reducer ang store para sa party/add, at ginawa ng Redux ang ginagawa ng Redux sa action na walang katugma: wala, ayon sa disenyo. Nahulog ang tap ng user sa isang kawalan na dating isang error.

Ngayon, sabihin natin nang malakas ang mas matalim na bahagi. Kahit wala ang boot import, mabubuo rin ang slice sa bandang huli: isang pagbisita sa Party tab ang mag-i-inject nito, at gagana ang bawat add pagkatapos noon. Kaya ang pagkabigo ay may mas makitid at mas pangit na hugis: bawat add na ginawa bago nagkataong buksan ng user ang tab ng may-ari ay tahimik na nawawala, at nagre-reproduce ito para sa ilang user at hindi kailanman para sa iba, depende sa pagkakasunod ng tab. Ang ganitong klase ng bug ang dahilan kung bakit boot import ang pattern at hindi pag-asa: naglo-load ang mga screen kapag hiningi, naglo-load ang mga state module sa boot, at ang pagkakaiba ay kung nakadepende ang hugis ng store sa kasaysayan ng ruta ng user.

Ibalik ang linya, mag-relaunch, at ang parehong tap ay nagtataas ng bilang sa 1/6.

Patakbuhin

Nakalathala at nakainstall na ang mga package, kaya ang takbo ay tatlong dev server at isang build sa simulator:

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

Lumalapag ang cold start sa Pokédex na may bilang nang nagbabasa ng 0/6: ang boot import na gumagana bago pa mag-render ang anuman sa party. Magdagdag mula sa isang detail, panoorin ang bilang na tumaas, at hanapin ang miyembrong nakaupo sa Party tab:

Ang add flow sa iOS: nagbabasa ang header ng Pokédex ng My Party 0/6, bumubukas ang detail na may Add to party button, lumalapag ang tap, tumataas ang bilang sa 1/6, at ipinapakita ng Party tab si Bulbasaur sa unang puwesto

Magpatuloy magdagdag hanggang anim at darating ang cap mula sa dalawang panig nang sabay: ang pang-anim na add ang nagpapalit nang live sa button ng nakabukas na detail sa disabled na estado nito, dahil ang parehong selector na bumibilang para sa header ay bumibilang para sa addDisabled:

Ang detail screen sa cap ng party: ang Add button na naka-disable at may bagong label na Party is full

Magtanggal ng miyembro sa Party tab at babalik sa putol-putol na gilid ang napalayang puwesto, bababa ang bilang sa lahat ng dako nang sabay, at gagana ang susunod na add. Isang slice, isang may-ari, tatlong surface na nagkakasundo dahil lahat sila ay nagbabasa ng parehong state sa parehong hugis.

Ang binuo mo, at ang susunod

May pag-aari na ang app. Nasa isang slice ang party na ang party app lang ang may-ari, na-inject sa boot sa isang store na ikinabit ng host sa paligid ng rootReducer ng contract. Ang nag-iisang interaksiyong tumatawid (ang addToParty, ang cap nito, ang hugis ng pagbasa nito) ay dala ng contract nang may bersiyon, umaabot ang write sa shared detail view bilang prop na ikinakabit ng consumer nito, at kinukuha ng party ang sarili nitong data sa halip na manghiram ng iskedyul ng release ng ibang team. Nakita mo na rin ang pagkabigong iniiwasan ng disenyong ito: isang dispatch papunta sa slice na hindi kailanman nag-load ang may-ari, itinapon nang walang ingay.

May natitirang tapat na limitasyon: i-restart ang app at mawawala ang party. Nakatira ang slice sa memorya, problema ng ibang post ang persistence, at ang pagkukunwaring hindi ay ang klase ng tahimik na paglawak ng saklaw na iniiwasan ng seryeng ito.

Ang mas matalim na tanong ang paulit-ulit na ibinabato ng stack na ito. Ang party ay anim na bagay at isang patakaran, at ang magalang na pagtawid sa seam ay nangailangan ng store, ng contract minor, ng injected na reducer at ng boot import. Ginawang posible ng RTK ang pagtawid; hindi nito ginawang maliit. Sa susunod, muling bubuuin ng serye ang mismong app na ito gamit ang TanStack Query at Zustand, ang stack na unang inaabot ng karamihan sa mga React Native team, at papanoorin kung ano ang gagawin ng federation sa pagpipiliang iyon.

Mga Sanggunian

Warren de Leon
Warren de Leon

Software Engineering Manager. Pinakahuling pinamunuan ang Mobile Platform team sa Hargreaves Lansdown. Sumusulat tungkol sa engineering leadership, React Native, at pagbuo ng magagandang team.

Tingnan ang profile