Dalawang backend na pumupuno sa iisang shared na client cache sa mga federated na module

Dalawang backend, isang client? RTK Query vs Apollo sa React Native

Nagtapos ang post 9 sa isang pangako: mananatili ang stack at mahahati sa dalawa ang backend, REST para sa list, GraphQL para sa mga type badge, isang client at isang cache. Iyon ang itinatayo ng post na ito.

Sinadya ang tandang pananong sa pamagat. Habang nasa transisyon, kapag kadarating pa lang ng GraphQL backend at buhay pa ang REST, panalo ang pagpapatakbo ng iisang client, halos kahit alin pa, at kung RTK Query na ang tinatakbo ng app mo, tapos na ang usapan. Sa dulo ng paglipat, kapag GraphQL na ang lahat, nakasalalay ang sagot sa dalawang bagay na puwede mong i-check laban sa sarili mong app: kung may federation constraint ka, at kung sapat na relational ang schema mo para maging sulit ang normalized na cache. Ang normalized na cache ay nag-iimbak ng bawat entity nang isang beses lang, isang Pokémon sa isang entry, kaya lahat ng view na tumuturo rito ay nagbabasa ng parehong kopya. May federation ang seryeng ito, kaya RTK Query pa rin ang pili dito. Palitan mo ang konteksto at magbabago ang sagot, at sinasabi ng post na ito kung saan.

Ang hugis sa kaliwa ang build: REST at GraphQL papunta sa iisang baseApi cache sa ilalim ng iisang tag graph, at hindi nagagalaw ang Refresh button ng host. Ang hugis sa kanan ay dalawang client na may dalawang cache at walang edge sa pagitan nila. Ang nawawalang edge na iyon ang tunay na paksa ng post na ito.

Dumating ang pangalawang backend

Nagtayo ng GraphQL ang API team. Para sa mga screen na kailangan ang magkakaugnay na data, kaya nitong palitan ng iisa ang ilang round trip, at doon nanggagaling ang bilis, hindi sa mismong protocol, at doon papunta ang backend. Hindi mawawala ang REST ngayong quarter, ni sa susunod. Sa loob ng ilang panahon, kadalasang matagal, buhay ang dalawa, at bawat screen, isa sa dalawa ang nagse-serve nito.

Ang panganib ay ang pagpapatakbo ng dalawang data-fetching client nang sabay, kahit alin pang dalawa ang piliin mo. Dalawang client, dalawang cache, at hindi nag-uusap ang dalawang cache. Ang mutation na lumalabas sa GraphQL ay nag-a-update ng GraphQL cache, at hindi kailanman umaabot ang update sa REST cache na may hawak ng parehong record. Mag-e-edit ang user ng Pokémon sa screen na sineserve ng GraphQL, lilipat sa list na sineserve ng REST, at makikita ang lumang value. Walang exception na lalabas, walang request na papalya, walang babala. Isang screen lang na tahimik na mali hanggang may pumilit ng refetch. Sa kalagitnaan ng paglipat, kapag pinakamagkapatong ang dalawang backend, doon pinakamalaki ang pinsala.

Kaya ang tanong ng transisyon ay kung gaano kakaunti ang cache na kailangan, at ang pinakamurang sagot ay isa.

Isang slice, dalawang protocol

Sa RTK Query, iisang cache ang nagsisilbi sa dalawang protocol, sa slice na tinatakbo ng seryeng ito mula pa noong post 6. Magsimula sa tag ng post 8:

git clone https://github.com/warrendeleon/react-native-module-federation
cd react-native-module-federation
git checkout post-08-client-state

Kinukuha na ng list ang mga row nito sa REST. Ngayon may pangalawa itong pangangailangan sa server state, isang type badge sa bawat row, na sineserve ng GraphQL. Dalawang dependency muna, sa list remote lang. Nananatili ang graphql sa 16 dahil 14 hanggang 16 ang dineklarang peer range ng graphql-request samantalang 17 na ang latest sa npm:

( cd apps/list && npm install graphql@16.14.2 graphql-request@7.4.0 )

Pagkatapos, ang endpoint, sa parehong api slice:

const typesApi = baseApi.injectEndpoints({
  endpoints: build => ({
    getPokemonTypes: build.query<Record<number, string[]>, void>({
      async queryFn() {
        try {
          const raw = await request(GRAPHQL_URL, POKEMON_TYPES);
          return { data: parsePokemonTypes(raw) };
        } catch (err) {
          return {
            error: {
              status: 'CUSTOM_ERROR',
              error: err instanceof Error ? err.message : 'Invalid GraphQL response',
            },
          };
        }
      },
      providesTags: ['PokemonList'],
    }),
  }),
});

export const { useGetPokemonTypesQuery } = typesApi;

Ang isang api slice ay may eksaktong isang baseQuery, at ang sa list ay fetchBaseQuery sa REST base. Ang pangalawang protocol laban sa ibang URL ay galing sa isang queryFn, na inililista mismo ng dokumentasyon ng RTK para sa “one-off queries that use a different base URL”. Ipinapadala ng graphql-request ang query, at dala ng parehong file ang hindi isinama ng sipi: ang nakatakda at nakaayos na GraphQL query, ang Zod schema na nagbabantay sa mga id at pangalan ng type, ang parsePokemonTypes, at ang integration sa screen. Sa halip na ilathala ang lahat, ilapag ang tree mo sa huling estado:

npx degit@3.8.0 --force warrendeleon/react-native-module-federation#post-10-two-backends /tmp/pokedex-ref-10
cp -R /tmp/pokedex-ref-10/. .

Ang response ay vina-validate ng sarili nitong Zod schema sa seam na binabantayan na ng REST list, na pinananatiling pareho para protocol lang ang pagkakaiba ng paghahambing. Nasa list app ang schema at hindi sa contracts package, dahil ang isang data definition ay sumasama sa domain na may-ari nito (ang panuntunan ng post 7).

Nasa graphql.pokeapi.co/v1beta2 ang GraphQL ng PokéAPI. Retirado na ang lumang v1beta schema, kasama ang pokemon_v2_ field prefix na laman ng bawat tutorial bago ang 2025. Ang paghingi ng pokemon_v2_pokemon ngayon ay nagbabalik ng field 'pokemon_v2_pokemon' not found in type: 'query_root'. Ang gumaganang hugis ay ang walang prefix, na sinuri nang live sa mismong endpoint habang sinusulat ang post na ito.

Dalawang bagay ang sadyang wala. Walang bagong bersyon ng contracts: ang endpoint ay nag-i-inject sa baseApi na nandiyan na, at dinedeklara nito ang tag na pag-aari na ng list. At ang graphql-request ay nananatiling labas sa shared map ng Module Federation, katulad na katulad ng Zod. Puro value lang ang ini-export nito, walang instance identity na dapat ibahagi, kaya dala-dala ito bilang sariling dependency ng list remote. Kung aling protocol ang nag-serve ng bawat row, pribadong usapin iyon ng remote, hindi nakikita ng host at ng lahat ng ibang remote.

May limit ang GraphQL ng PokéAPI na 100 call kada oras kada IP, samantalang fair use lang ang hinihingi ng REST. Nasasalo ito ng caching ng RTK Query sa normal na gamit. Kaya pa ring maubos ng sunud-sunod na cold reload, at ang 429 habang nagbi-build ka ay ang limit, hindi bug.

Isang tag para sa buong paglipat

Ang Refresh button ng host ay nagdi-dispatch ng invalidateTags(['PokemonList']) mula pa noong post 6, at wala itong hawak na reference sa alinman sa dalawang endpoint. Parehong nagpo-provide ng 'PokemonList' ang REST list at ang GraphQL types query, kaya isang pindot, refetch ang dalawa. Makikita ito sa Network panel ng React Native DevTools: isang tap, sabay na dumarating ang dalawang request, at sa v1beta2 preview makikita ang type data na dumarating sa GraphQL.

Ang Network panel ng React Native DevTools: walang laman na log, pagkatapos isang tap sa Refresh ang nagpapadala ng dalawang request nang sabay, ang GraphQL call sa v1beta2 at ang REST call sa pokemon list, at makikita sa preview ng GraphQL row ang dumarating na pokemontypes data

Iyan ang buod ng transisyon sa iisang tag: alinman ang piliin mong client, isa lang ang panatilihin. Pero walang tumatawid nang kusa. Nililinis ng isang mutation ang cache ng kabilang protocol kapag pinangalanan nito ang tag na ibinibigay ng dalawa, at doon lang; ang shared graph ang nagpapaposible nito, hindi ito ang gumagawa niyon.

Ang parehong race ay makikita sa device mismo: sa cold start, unang dumarating ang mga REST row, at sumusunod ang mga GraphQL badge makalipas ang isang saglit, habang pinupuno ng bawat query ang shared cache:

Ang Pokédex list sa iOS sa isang cold start: isang loading spinner, pagkatapos ang mga row at pangalan na sineserve ng REST, pagkatapos ang mga type badge na sineserve ng GraphQL na dumarating makalipas ang saglit, at sa dulo ang mga sprite na kumukumpleto sa nakaayos nang list

Tahimik ding sumusuko ang mga badge. Ituro ang GraphQL endpoint sa isang hindi maabot na host at magre-render pa rin ang mga row, wala lang ang mga badge, at walang gumagalaw sa screen.

Nilalagyan din ng mga badge ng numero ang mismong pangako ng GraphQL. Ang types sa REST ay mangangahulugan ng 151 detail call, isa kada row, dahil pangalan at URL lang ang ibinabalik ng list endpoint. Sa GraphQL, isang query lang ito. Tawagin natin ito sa tamang pangalan, client request fan-out at hindi N+1: ang klasikong problema ay tungkol sa backend na inuulit ang isang data access kada row, samantalang dito ay isang client ang gumagawa ng request kada row dahil wala nang ibang ibinibigay ang endpoint. Mas kaunting round trip ang panalo rito, at puntos ito para sa protocol, hindi para sa alinmang client.

Ang ginagawa ng Apollo na hindi kaya nito

Tinatrato ng RTK Query ang GraphQL response bilang data na ika-cache kada endpoint, katulad ng kahit anong REST payload. May ginagawa ang Apollo na hindi man lang tinatangka ng RTK Query: sa default nitong InMemoryCache, nagno-normalize ito. Bawat object na may __typename at id ay may iisang entry sa cache, at bawat query na tumutukoy rito ay nagbabasa sa entry na iyon. Ang mutation na may dalang parehong type at id sa response ay nag-a-update ng entity saanman ito lumitaw, walang refetch.

Totoo iyan, at iyan ang tapat na dahilan para kunin ang Apollo. Pero mas makitid ito kaysa sa pangako. Ang saklaw ng automatic consistency ay ang pagbabago ng entity na nasa cache na. Hindi saklaw ang pagpapalaki ng list. Sa mismong salita ng Apollo, “a newly cached object isn’t automatically added to any list fields that should now include that object”, kaya ang bagong row ay nangangailangan pa rin ng update function o refetch.

Ang natitirang kaso ng Apollo ay tumatayo sa sarili nitong mga merito. Sa fragment colocation, dineklara ng component ang mga field na kailangan nito at pinagsasama-sama paakyat sa simpleng template-literal interpolation, walang build step; opsyonal ang code generation, at sulit kapag gusto mo ng typed na resulta. Kaya ng cache redirects na i-serve ang detail view diretso mula sa data na nakuha na ng list, pero kapag lang nasa cache na ang bawat field na hinihingi ng detail query. Isang dagdag na field at pupunta sa network ang buong query. May suporta ang subscriptions at @defer, at kailangan ng @defer ng incremental-delivery handler at streaming-fetch polyfill sa React Native, kaya “supported, may setup” ito, hindi libre.

At mas mahalaga ang schema kaysa sa lahat ng ito. Ang mismong hugis ng PokéAPI, mga Pokémon na tumutukoy sa types, abilities, species at evolution chains, na ang parehong entities ay ginagamit muli sa maraming query, ang eksaktong relational na hugis kung saan sulit na sulit ang normalization. Sa data na ito, mas bagay ang cache ng Apollo, at ang pagkukunwaring hindi ay ang strawman na iniiwasan ng seryeng ito. (Nasa parehong pamilya ang Relay. Papasok lang doon ang urql kapag nilagyan mo ito ng Graphcache: ang default nitong document cache ay nag-iimbak ng buong response kada query, mas malapit sa modelo ng RTK Query kaysa sa kay Apollo. Ang mga trade-off sa Pagpili ay tungkol sa normalized na pamilya, hindi sa iisang library.)

Ang ginagawa ng federation sa pagpili

Simulan sa parteng nakakagulat. Walang injectEndpoints ang Apollo, at hindi nito kailangan. Ang operation ay isang document na ine-execute laban sa client, hindi artifact na nirerehistro sa store, kaya kahit aling remote ay puwedeng magdala ng sarili nitong mga query laban sa shared client nang walang anumang rehistrasyon. Kaya pa ngang palawakin ng remote ang cache sa pag-mount sa pamamagitan ng cache.policies.addTypePolicies, na dokumentadong public API. Walang opisyal na micro-frontend guide (isang walang sagot na community thread at isang customer story ang buong record), pero nagkataong pinapayagan ito ng cache API. Sa runtime-extensibility axis na mahalaga sa post 9, hindi naiiwan ang Apollo.

Hindi rin nito kasalo ang context problem ng TanStack. Iniimbak ng Apollo ang iisang React context sa mismong React instance, naka-key sa isang kilalang symbol, para kahit magkaroon ng dalawang kopya ng library, hindi ka nito mabibigyan ng magkaibang context. Ang error na “no client in context” ay lumalabas lang kapag ang React mismo ang doble. Ang singleton case ng @apollo/client sa federation ay iisang cache instance at walang version skew sa pagitan ng client at ng mga hook nito, hindi ang hati sa context.

Sa dalawang lugar pa rin humihila ang federation: ang tag graph at ang paglipat. Ang iisang tag na nagre-refetch sa mga remote, at sa dalawang protocol, ay koordinasyong nasa RTK Query na sa mismong pagkakabuo nito at iniaasa ng Apollo sa kumbensiyon. At ang pag-adopt ng Apollo sa kalagitnaan ng paglipat ay nangangahulugan ng pagtatayo ng pangalawang cache katabi ng RTK Query na tinatakbo mo na, ang mismong dalawang-cache na hugis sa simula ng post na ito, habang tumatagal ang migration. Ginagawa ring required peer ng Apollo Client 4 ang rxjs at inililipat ang mga React export nito, kaya nadadagdagan ng bagong dependency ang bundle sa halip na nagpapalit lang.

RTK Query

  • Cache model: kada endpoint, refetch kapag na-invalidate
  • Cross-protocol invalidation: iisang shared tag graph, built in
  • Pagdaragdag ng query mula sa remote: injectEndpoints sa runtime
  • Relational na schema, gumagamit muli ng entities: nire-refetch ang mayroon na

Apollo

  • Cache model: normalized sa __typename + id, patch sa mismong entry
  • Cross-protocol invalidation: kada client; pangalawang cache na kokoordinahin
  • Pagdaragdag ng query mula sa remote: documents laban sa client; walang irerehistro
  • Relational na schema, gumagamit muli ng entities: sulit ang cache

Pagpili

Walang absolute na panalo, kaya hanapin sa ibaba ang case na tugma sa app mo.

Paglipat, buhay ang dalawang backend: isang client lang ang patakbuhin. Kung nasa RTK Query na ang app, at ganoon ang seryeng ito, tapos na ang usapan. Isang cache, isang tag, walang two-cache staleness na babantayan, at ito ang karaniwang case.

Destinasyon, GraphQL na lahat, may federation constraint at shared coordinated state: mananatili ang RTK Query. Ang tapat na presyo ay ang mabuhay nang walang normalization: ang tag invalidation ay nagre-refetch kung saan magpa-patch lang ang Apollo, at sinasaklaw ng updateQueryData ang iilang punto kung saan sulit ang manu-manong patch. Ipinagpapalit mo ang ilang round trip para sa iisang cache at iisang tag graph sa mga remote na hiwalay na naide-deploy.

Destinasyon, GraphQL na lahat, relational at maraming entity ang schema, at walang federation constraint: Apollo, walang gatol. Ang normalized na cache ang mismong punto ng isang GraphQL client, at kung walang federation na humihila pabalik, iyon ang dahilan para i-adopt ito. Walang pero-pero.

Ang mga senyales na dapat bantayan ay dalawang tiyak na araw. Ang araw na mapansin mong karamihan ng invalidation mo ay nagre-refetch ng data na nasa cache na: natipid sana ng normalization ang mga round trip na iyon. At ang araw na mag-live ang pangalawang backend at magkasalungat ang dalawang cache sa screen nang walang error na magpapaliwanag. Ang una ay tulak papunta sa Apollo. Ang pangalawa ang dahilan para siguraduhing isa lang talaga ang client na pinatakbo mo.

Susunod: magiging federated singleton ang design system, isang UI package na shared sa runtime, para ang bawat remote ay magre-render ng parehong mga component nang hindi nagdadala ng sariling kopya.

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