<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Warren de Leon</title>
    <description>The latest articles on DEV Community by Warren de Leon (@warrendeleon).</description>
    <link>https://dev.to/warrendeleon</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3849498%2F78ff4b91-2b34-4678-85ae-19aaf1642e95.png</url>
      <title>DEV Community: Warren de Leon</title>
      <link>https://dev.to/warrendeleon</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/warrendeleon"/>
    <language>en</language>
    <item>
      <title>The design system as a federated singleton in React Native</title>
      <dc:creator>Warren de Leon</dc:creator>
      <pubDate>Tue, 08 Sep 2026 00:04:58 +0000</pubDate>
      <link>https://dev.to/warrendeleon/the-design-system-as-a-federated-singleton-in-react-native-163o</link>
      <guid>https://dev.to/warrendeleon/the-design-system-as-a-federated-singleton-in-react-native-163o</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📚 &lt;strong&gt;React Native Module Federation&lt;/strong&gt; series — &lt;a href="https://warrendeleon.com/blog/series/react-native-module-federation/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-design-system" rel="noopener noreferrer"&gt;read it in full on warrendeleon.com&lt;/a&gt;, where new parts land first.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://warrendeleon.com/blog/rtk-query-vs-apollo-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-design-system" rel="noopener noreferrer"&gt;Post 10&lt;/a&gt; closed on a promise: the design system becomes a federated singleton, one UI package shared at runtime, so every remote renders the same components without shipping its own copy. This post keeps it.&lt;/p&gt;

&lt;p&gt;The debt is real. Since &lt;a href="https://warrendeleon.com/blog/host-shell-federated-tabs-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-design-system" rel="noopener noreferrer"&gt;post 4&lt;/a&gt; every screen has styled itself with &lt;code&gt;StyleSheet.create&lt;/code&gt; and hard-coded hexes, and the duplication now spans three apps and the view package: the same template greys sit in the list, the party and the detail view, and the same blue accent appears in the host's Refresh button and the detail's Add button. Four codebases, four copies of the same design decisions, and no way to change any of them in one place.&lt;/p&gt;

&lt;p&gt;The fix applies two earlier patterns to pixels. &lt;a href="https://warrendeleon.com/blog/shared-singleton-contract-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-design-system" rel="noopener noreferrer"&gt;Post 3&lt;/a&gt; shared libraries as singletons so one copy serves every app. Posts &lt;a href="https://warrendeleon.com/blog/typing-the-seam-between-remotes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-design-system" rel="noopener noreferrer"&gt;5&lt;/a&gt; and &lt;a href="https://warrendeleon.com/blog/one-shared-store-across-remotes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-design-system" rel="noopener noreferrer"&gt;6&lt;/a&gt; put packages at the seam so every side installs one definition.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;@pokedex/ui&lt;/code&gt; does both. It packages gluestack-ui's copy-in components, styles them with NativeWind 4.2.6 and publishes them through the local Verdaccio registry. Module Federation then gives every remote the host's copy at runtime. This build uses &lt;code&gt;@gluestack-ui/core&lt;/code&gt; on &lt;code&gt;^3.0.25&lt;/code&gt;, &lt;code&gt;@gluestack-ui/utils&lt;/code&gt; on &lt;code&gt;^3.0.21&lt;/code&gt; and Tailwind 3.4.19.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📊 &lt;strong&gt;Diagram:&lt;/strong&gt; &lt;a href="https://warrendeleon.com/blog/federated-design-system-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-design-system" rel="noopener noreferrer"&gt;view it on warrendeleon.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The seam now holds three packages, and they cross it differently. The contracts and the design system resolve to one copy at runtime, drawn as the host providing them. The detail package deliberately does not: each consumer keeps its own version pin, which has been its point since post 5. That contrast is the heart of this post.&lt;/p&gt;

&lt;h2&gt;
  
  
  A package for the pixels
&lt;/h2&gt;

&lt;p&gt;Start from post 10's tag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/warrendeleon/react-native-module-federation
&lt;span class="nb"&gt;cd &lt;/span&gt;react-native-module-federation
git checkout post-10-two-backends
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One honest boundary before the anatomy. The design system's source is more than a post can carry: thirteen copied gluestack components, token maps for eighteen types, eleven composed components. So this post's packages are pulled from the finished tag rather than typed in, and that includes the new versions of contracts and detail, because this post grows both (&lt;em&gt;The detail finishes dressing&lt;/em&gt; says how). Fetch the reference copy once (&lt;em&gt;Run it&lt;/em&gt; reuses it for the native chrome) and take all three packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx degit@3.8.0 &lt;span class="nt"&gt;--force&lt;/span&gt; warrendeleon/react-native-module-federation#post-11-design-system /tmp/pokedex-ref
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; packages/ui packages/contracts packages/detail
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; /tmp/pokedex-ref/packages/ui /tmp/pokedex-ref/packages/contracts /tmp/pokedex-ref/packages/detail packages/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;packages/ui&lt;/code&gt; follows the anatomy posts 5 and 6 established: &lt;code&gt;react-native-builder-bob&lt;/code&gt; builds an ESM &lt;code&gt;lib/&lt;/code&gt;, &lt;code&gt;"react-native": "./src/index.ts"&lt;/code&gt; ships raw source so Re.Pack compiles the same files the repo holds, &lt;code&gt;publishConfig&lt;/code&gt; points at the local Verdaccio, and the &lt;code&gt;files&lt;/code&gt; list adds two entries the earlier packages never needed: &lt;code&gt;tailwind.preset.js&lt;/code&gt; and &lt;code&gt;global.css&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The gluestack CLI copies component source into &lt;code&gt;src/components/ui/&lt;/code&gt;. &lt;strong&gt;Copy-in means you own the source&lt;/strong&gt;: your package publishes that code, and upstream updates do not arrive automatically. The companion keeps the thirteen primitives these screens use, from &lt;code&gt;box&lt;/code&gt; to &lt;code&gt;vstack&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One copy has a hidden dependency. &lt;code&gt;heading&lt;/code&gt; imports &lt;code&gt;@expo/html-elements&lt;/code&gt;, which the generated package does not declare. Add it to &lt;code&gt;dependencies&lt;/code&gt;, or consumers fail on a package they did not install.&lt;/p&gt;

&lt;p&gt;Above the primitives sit the tokens and composed components. &lt;code&gt;tailwind.preset.js&lt;/code&gt; carries the brand and neutral colours plus &lt;code&gt;type.&amp;lt;name&amp;gt;&lt;/code&gt; for all eighteen Pokémon types. The same values live in &lt;code&gt;src/tokens/&lt;/code&gt; for runtime code. Eleven components use them: ScreenContainer, PokemonCard, TypeBadge, LoadingState, ErrorState, InfoRow, StatBar, EmptySlot, BackPill, ThemeToggle and Toaster. A &lt;code&gt;primitives.ts&lt;/code&gt; barrel keeps consumer imports on one package surface.&lt;/p&gt;

&lt;p&gt;Two decisions belong with the tokens, and both fail quietly when they are left to the components.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Only literal class strings generate.&lt;/strong&gt; A constructed &lt;code&gt;`bg-type-${t}`&lt;/code&gt; is invisible to Tailwind's scanner, and the colour silently never ships. The token file spells out every class in literal maps (&lt;code&gt;TYPE_BG_CLASS&lt;/code&gt; and friends) and exports lookup helpers, which keeps the whole palette in one auditable place.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The second is text contrast. &lt;strong&gt;Type colours need measured foregrounds, not a visual guess.&lt;/strong&gt; White on the water blue measures 3.48:1 and on the psychic pink 3.41:1, below &lt;a href="https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html" rel="noopener noreferrer"&gt;WCAG's 4.5:1 minimum for small text&lt;/a&gt;. Black clears 6:1 on both. &lt;code&gt;textOnTypeClass()&lt;/code&gt; computes the higher-contrast choice once and every type-coloured surface uses it. A hard-coded black foreground would put the fighting-type hero at 1.7:1.&lt;/p&gt;

&lt;p&gt;One kind of input could never have been typed in anyway: images. The theme toggle's moon and sun and the back pill's chevron ship as PNG files in &lt;code&gt;src/assets&lt;/code&gt;, which came across with the copy, and the &lt;code&gt;files&lt;/code&gt; list bundles them into the published package.&lt;/p&gt;

&lt;p&gt;Publish it the way posts 5 and 6 did, install step and all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;packages/ui &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run build &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm publish &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Version 1.0.2 is the intended 1.0.0 API plus the patches later hardening passes added.&lt;/p&gt;

&lt;h2&gt;
  
  
  The detail finishes dressing
&lt;/h2&gt;

&lt;p&gt;The finished detail needs data the earlier contract did not carry. Contracts 3.2.2 adds &lt;code&gt;heightM&lt;/code&gt;, &lt;code&gt;weightKg&lt;/code&gt;, &lt;code&gt;abilities&lt;/code&gt;, &lt;code&gt;stats&lt;/code&gt; and optional &lt;code&gt;flavourText&lt;/code&gt; to &lt;code&gt;PokemonDetail&lt;/code&gt;. It also adds &lt;code&gt;types&lt;/code&gt; to &lt;code&gt;PartyMember&lt;/code&gt;, so the party can tint cards without another request. &lt;code&gt;parsePokemonDetail&lt;/code&gt; converts PokéAPI's units and normalises the flavour text once at the boundary.&lt;/p&gt;

&lt;p&gt;Both remotes now fetch the species payload beside the Pokémon, and the list passes type data when it adds a member. The app-file sweep in &lt;em&gt;Wiring a styling runtime into a federated build&lt;/em&gt; carries those edits. The new package source already came from the reference copy, so publish contracts and detail before the apps install them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;packages/contracts &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run build &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm publish &lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;packages/detail &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run build &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm publish &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;@pokedex/detail&lt;/code&gt; 4.0.2 rebuilds the view around the design system: a type-coloured collapsing hero, a ringed sprite, flavour text, Info rows, Base Stats and the Add button. &lt;strong&gt;It is a major because it gains peers&lt;/strong&gt;: &lt;code&gt;@pokedex/ui&lt;/code&gt;, NativeWind and Reanimated become requirements on every consumer.&lt;/p&gt;

&lt;p&gt;Its props do not change. The view still accepts &lt;code&gt;{ pokemon, loading, error, onRetry, onAddToParty, addDisabled, addLabel }&lt;/code&gt;, so both containers move to the new view with a version-pin edit. The largest visual change in the series reaches them through &lt;code&gt;npm install&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2tvdhob187h42oycvyvb.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2tvdhob187h42oycvyvb.webp" alt="The finished Pokémon detail screen: a grass-green hero with the sprite in a translucent ring over a ghost #001, Bulbasaur's name and type pills, then a flavour-text quote card, Info rows and Base Stats bars in floating cards" width="600" height="1305"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The collapse is the screen's signature move, and it is worth seeing before the wiring section, because every colour in it is a token class resolved through the seam:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🎞️ &lt;strong&gt;Animated demo:&lt;/strong&gt; &lt;a href="https://warrendeleon.com/blog/federated-design-system-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-design-system#the-detail-finishes-dressing" rel="noopener noreferrer"&gt;watch it on warrendeleon.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Wiring a styling runtime into a federated build
&lt;/h2&gt;

&lt;p&gt;NativeWind's own docs support Metro; Re.Pack appears under community-driven solutions. From the other side, Callstack ships a first-party plugin, &lt;code&gt;@callstack/repack-plugin-nativewind&lt;/code&gt;, version-locked to the installed Re.Pack (5.2.5 here). Reanimated needs the same treatment: Re.Pack detects the package and warns when &lt;code&gt;ReanimatedPlugin&lt;/code&gt; is missing, so &lt;code&gt;@callstack/repack-plugin-reanimated&lt;/code&gt; goes in beside it at the same version. Both halves are true, and this wiring is the plugin's documented path.&lt;/p&gt;

&lt;p&gt;All three apps receive the styling dependencies. The two remotes also install detail 4.0.2 and contracts 3.2.2. Every app declares gluestack core and utils directly because the copied source imports them. Each app also pins &lt;code&gt;react&lt;/code&gt; and &lt;code&gt;react-dom&lt;/code&gt; to its React version, 19.2.3, so peer resolution cannot install a second React.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/host &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm pkg &lt;span class="nb"&gt;set &lt;/span&gt;overrides.react&lt;span class="o"&gt;=&lt;/span&gt;19.2.3 overrides.react-dom&lt;span class="o"&gt;=&lt;/span&gt;19.2.3 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install &lt;/span&gt;nativewind@4.2.6 @gluestack-ui/core@3.0.25 @gluestack-ui/utils@3.0.21 react-native-reanimated@4.6.0 react-native-worklets@0.12.1 @pokedex/ui@1.0.2 @pokedex/contracts@3.2.2 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; tailwindcss@3.4.19 @callstack/repack-plugin-nativewind@5.2.5 @callstack/repack-plugin-reanimated@5.2.5 postcss@8.4.47 postcss-loader@8.1.1 autoprefixer@10.4.20 @module-federation/enhanced@2.9.0 @react-native-community/cli@20.2.0 @react-native-community/cli-platform-android@20.2.0 @react-native-community/cli-platform-ios@20.2.0 &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/list &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm pkg &lt;span class="nb"&gt;set &lt;/span&gt;overrides.react&lt;span class="o"&gt;=&lt;/span&gt;19.2.3 overrides.react-dom&lt;span class="o"&gt;=&lt;/span&gt;19.2.3 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install &lt;/span&gt;nativewind@4.2.6 @gluestack-ui/core@3.0.25 @gluestack-ui/utils@3.0.21 react-native-reanimated@4.6.0 react-native-worklets@0.12.1 @pokedex/ui@1.0.2 @pokedex/contracts@3.2.2 @pokedex/detail@4.0.2 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; tailwindcss@3.4.19 @callstack/repack-plugin-nativewind@5.2.5 @callstack/repack-plugin-reanimated@5.2.5 postcss@8.4.47 postcss-loader@8.1.1 autoprefixer@10.4.20 @module-federation/enhanced@2.9.0 @react-native-community/cli@20.2.0 @react-native-community/cli-platform-android@20.2.0 @react-native-community/cli-platform-ios@20.2.0 &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/party &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm pkg &lt;span class="nb"&gt;set &lt;/span&gt;overrides.react&lt;span class="o"&gt;=&lt;/span&gt;19.2.3 overrides.react-dom&lt;span class="o"&gt;=&lt;/span&gt;19.2.3 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install &lt;/span&gt;nativewind@4.2.6 @gluestack-ui/core@3.0.25 @gluestack-ui/utils@3.0.21 react-native-reanimated@4.6.0 react-native-worklets@0.12.1 @pokedex/ui@1.0.2 @pokedex/contracts@3.2.2 @pokedex/detail@4.0.2 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; tailwindcss@3.4.19 @callstack/repack-plugin-nativewind@5.2.5 @callstack/repack-plugin-reanimated@5.2.5 postcss@8.4.47 postcss-loader@8.1.1 autoprefixer@10.4.20 @module-federation/enhanced@2.9.0 @react-native-community/cli@20.2.0 @react-native-community/cli-platform-android@20.2.0 @react-native-community/cli-platform-ios@20.2.0 &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;The &lt;code&gt;overrides&lt;/code&gt; block is why those installs start with &lt;code&gt;npm pkg set&lt;/code&gt;.&lt;/strong&gt; gluestack pulls &lt;code&gt;react-native-web&lt;/code&gt; and &lt;code&gt;react-dom&lt;/code&gt; in as transitive peers, and their react ranges collide with the app's pinned &lt;code&gt;react@19.2.3&lt;/code&gt;. Pinning both in &lt;code&gt;overrides&lt;/code&gt; settles it. Reaching for &lt;code&gt;--legacy-peer-deps&lt;/code&gt; instead hides the version mismatches a shared singleton needs you to see.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The last four pins update &lt;code&gt;@module-federation/enhanced&lt;/code&gt; and the React Native CLI trio (the core package and both platform packages) to their current releases, which clear this stack's fixable &lt;code&gt;npm audit&lt;/code&gt; advisories; the repo README records what remains and why. &lt;code&gt;react-native-reanimated&lt;/code&gt; and &lt;code&gt;react-native-worklets&lt;/code&gt; carry native code, so the host needs its pods refreshed before the &lt;em&gt;Run it&lt;/em&gt; build: &lt;code&gt;( cd apps/host/ios &amp;amp;&amp;amp; bundle install &amp;amp;&amp;amp; bundle exec pod install )&lt;/code&gt;, the same convention the repo README uses. &lt;em&gt;Who shares what&lt;/em&gt; explains why &lt;code&gt;react-native-reanimated&lt;/code&gt; and &lt;code&gt;react-native-worklets&lt;/code&gt; are here at all. Pin &lt;code&gt;tailwindcss&lt;/code&gt; below 4. NativeWind 4's styling runtime, &lt;code&gt;react-native-css-interop&lt;/code&gt;, declares a Tailwind 3 dependency, and npm's &lt;code&gt;latest&lt;/code&gt; Tailwind is a 4.x that will not work here.&lt;/p&gt;

&lt;p&gt;The per-app configuration, entry files and rebuilt screens come from the reference copy. This sweep makes the reader's tree match the tag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;app &lt;span class="k"&gt;in &lt;/span&gt;host list party&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-ref/apps/&lt;span class="nv"&gt;$app&lt;/span&gt;/babel.config.js &lt;span class="se"&gt;\&lt;/span&gt;
     /tmp/pokedex-ref/apps/&lt;span class="nv"&gt;$app&lt;/span&gt;/global.css &lt;span class="se"&gt;\&lt;/span&gt;
     /tmp/pokedex-ref/apps/&lt;span class="nv"&gt;$app&lt;/span&gt;/index.js &lt;span class="se"&gt;\&lt;/span&gt;
     /tmp/pokedex-ref/apps/&lt;span class="nv"&gt;$app&lt;/span&gt;/jest.config.js &lt;span class="se"&gt;\&lt;/span&gt;
     /tmp/pokedex-ref/apps/&lt;span class="nv"&gt;$app&lt;/span&gt;/rspack.config.mjs &lt;span class="se"&gt;\&lt;/span&gt;
     /tmp/pokedex-ref/apps/&lt;span class="nv"&gt;$app&lt;/span&gt;/tailwind.config.js &lt;span class="se"&gt;\&lt;/span&gt;
     /tmp/pokedex-ref/apps/&lt;span class="nv"&gt;$app&lt;/span&gt;/tsconfig.json apps/&lt;span class="nv"&gt;$app&lt;/span&gt;/
&lt;span class="k"&gt;done
&lt;/span&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-ref/apps/host/App.tsx /tmp/pokedex-ref/apps/host/mf-modules.d.ts apps/host/
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-ref/apps/host/__mocks__/react-native-reanimated.js /tmp/pokedex-ref/apps/host/__mocks__/partyApp-styles.js apps/host/__mocks__/
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-ref/apps/host/__tests__/RemoteBoundary.test.tsx apps/host/__tests__/
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-ref/apps/list/src/ListStack.tsx /tmp/pokedex-ref/apps/list/src/PokedexScreen.tsx /tmp/pokedex-ref/apps/list/src/detailApi.ts /tmp/pokedex-ref/apps/list/src/typesApi.ts apps/list/src/
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; apps/list/__mocks__
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-ref/apps/list/__mocks__/react-native-reanimated.js /tmp/pokedex-ref/apps/list/__mocks__/styleMock.js apps/list/__mocks__/
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-ref/apps/list/__tests__/addGate.test.tsx /tmp/pokedex-ref/apps/list/__tests__/doubleTapAdd.test.tsx /tmp/pokedex-ref/apps/list/__tests__/parsePokemonDetail.test.ts /tmp/pokedex-ref/apps/list/__tests__/parsePokemonTypes.test.ts apps/list/__tests__/
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-ref/apps/party/src/PartyScreen.tsx /tmp/pokedex-ref/apps/party/src/PartyStack.tsx /tmp/pokedex-ref/apps/party/src/detailApi.ts /tmp/pokedex-ref/apps/party/src/styles.ts apps/party/src/
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-ref/apps/party/__tests__/partyStateReady.test.ts /tmp/pokedex-ref/apps/party/__tests__/partySelfRecovery.test.tsx /tmp/pokedex-ref/apps/party/__tests__/removeAnimatesBeforeCommit.test.tsx apps/party/__tests__/
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; apps/party/__mocks__
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-ref/apps/party/__mocks__/react-native-reanimated.js apps/party/__mocks__/
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; /tmp/pokedex-ref/scripts &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-ref/README.md &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Babel gains NativeWind's JSX source, the Rspack config gains the plugin, and a three-line &lt;code&gt;global.css&lt;/code&gt; holds the Tailwind directives. The interesting file is &lt;code&gt;tailwind.config.js&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;presets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nativewind/preset&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@pokedex/ui/tailwind.preset.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
  &lt;span class="na"&gt;darkMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;class&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./App.tsx&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./src/**/*.{js,jsx,ts,tsx}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dirname&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@pokedex/ui/package.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/**/*.{js,jsx,ts,tsx}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dirname&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@pokedex/detail/package.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/**/*.{js,jsx,ts,tsx}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three wiring warnings live in that file and its neighbours, and each one fails silently if ignored.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Scan the packages' source.&lt;/strong&gt; Tailwind generates a class only when it sees the literal string, and the design system's classes live in &lt;code&gt;node_modules&lt;/code&gt;. The &lt;code&gt;content&lt;/code&gt; globs must include the shipped source of &lt;code&gt;@pokedex/ui&lt;/code&gt; and &lt;code&gt;@pokedex/detail&lt;/code&gt;, resolved via &lt;code&gt;require.resolve&lt;/code&gt;. Miss this and the breakage looks intermittent: a class the app also uses still works, and only the classes unique to the library vanish.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Import &lt;code&gt;global.css&lt;/code&gt; in every exposed module, not just the entry.&lt;/strong&gt; Federation loads the exposed module, never the remote's own &lt;code&gt;index.js&lt;/code&gt;, so a &lt;code&gt;global.css&lt;/code&gt; imported only at the entry works standalone and silently no-ops federated. &lt;code&gt;ListStack.tsx&lt;/code&gt; imports it at the top for exactly this reason, and the party reaches it through the module described in &lt;em&gt;Styles are a boot-time registration&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;&lt;code&gt;darkMode: 'class'&lt;/code&gt; is load-bearing.&lt;/strong&gt; NativeWind's &lt;code&gt;setColorScheme&lt;/code&gt; throws without it, and the &lt;em&gt;One toggle, three bundles&lt;/em&gt; flip is built on that call.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Styles are a boot-time registration
&lt;/h3&gt;

&lt;p&gt;The party's copy of that import moved one step earlier, and the reason is worth the detour.&lt;/p&gt;

&lt;p&gt;Registering a stylesheet writes into the styling runtime every mounted component reads. When the party tab's module arrives during a lazy render, that write lands while the Pokédex's screens are on screen, and React says so in development: &lt;em&gt;Can't perform a React state update on a component that hasn't mounted yet&lt;/em&gt;, with a component stack pointing into the list's bundle. Three of them, on the tab's first open. Nothing breaks. The screen paints correctly, the warning is a development-only log, and a reader who never opens the second tab never sees it.&lt;/p&gt;

&lt;p&gt;It is still worth fixing, because the cause is now specific rather than mysterious. Post 8 recorded a warning of the same shape at boot and said plainly that it had not been traced. This one traces: &lt;code&gt;import '../global.css'&lt;/code&gt; at the top of an exposed module runs during the render that loads that module. Bisecting confirms it. Comment the import out and the tab opens silently.&lt;/p&gt;

&lt;p&gt;So the party's styles became a module of their own:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// apps/party/src/styles.ts&lt;/span&gt;
&lt;span class="c1"&gt;// The remote's styles as a module the host can preload, for the same reason partySlice is one:&lt;/span&gt;
&lt;span class="c1"&gt;// registering them is a side effect the styling runtime shares with every app already mounted.&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../global.css&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It joins the exposes map beside the state module, and &lt;code&gt;PartyStack.tsx&lt;/code&gt; imports &lt;code&gt;./styles&lt;/code&gt; rather than the stylesheet directly, so the standalone build still registers them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;exposes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./PartyStack&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./src/PartyStack.tsx&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./partySlice&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./src/partySlice.ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./styles&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./src/styles.ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The host then loads it in the effect that already loads the state module, one line above it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;partyApp/styles&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;party styles failed to load&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That extends post 8's rule by a second half. What a remote must register before it renders (its reducer, its styles) loads at boot; screens still load on demand. The Pokédex tab needs no such move for a reason worth stating: it is the first thing the host renders, so its styles register while there is nothing else mounted to disturb.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who shares what
&lt;/h2&gt;

&lt;p&gt;The shared map decision is the identity test from posts 3 and 6, asked three times with three different answers.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;nativewind&lt;/code&gt; belongs in the shared map because its runtime holds a module-level style registry and colour-scheme state. Two copies let a remote register styles in a registry the host never reads. Re.Pack &lt;a href="https://github.com/callstack/repack/discussions/1032" rel="noopener noreferrer"&gt;discussion #1032&lt;/a&gt; records the same registry failure. Its accepted answer suggests deep-sharing &lt;code&gt;react-native-css-interop/&lt;/code&gt;, later replies still report the problem, and &lt;a href="https://github.com/callstack/repack/pull/1242" rel="noopener noreferrer"&gt;PR #1242&lt;/a&gt; closed without merging its accumulation approach.&lt;/p&gt;

&lt;p&gt;The host provides the one stateful styling runtime. With NativeWind shared as an eager singleton and the Re.Pack plugin wired into each app, both remotes register styles in the registry the host reads. This build verifies that behaviour across both remotes and both colour schemes.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;@pokedex/ui&lt;/code&gt; joins it as an eager host provider: one provider, one theme and one set of component instances. &lt;strong&gt;Build-time tools stay out of the runtime map.&lt;/strong&gt; Tailwind and PostCSS never ship, while Zod carries values rather than process-wide identity. The four host entries are:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@pokedex/ui&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;singleton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;eager&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;requiredVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dependencies&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@pokedex/ui&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="nx"&gt;nativewind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;singleton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;eager&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;requiredVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dependencies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nativewind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-native-reanimated&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;singleton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;eager&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;requiredVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dependencies&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-native-reanimated&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-native-worklets&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;singleton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;eager&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;requiredVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dependencies&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-native-worklets&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reanimated and Worklets register native views and a worklet runtime once per process. When the list bundled another copy for the detail collapse, the app died at load with the same native view name registered twice. The host now provides one copy to every consumer.&lt;/p&gt;

&lt;p&gt;And &lt;code&gt;@pokedex/detail&lt;/code&gt; stays out on purpose. Per-consumer versioning is its feature: the skew ladder from post 5 still works after this post, and a list shipping detail 4.0.2 while the party lags on 3.x is a supported state, not a bug. Same test, opposite answer, both correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  The greys collapse
&lt;/h2&gt;

&lt;p&gt;The screens keep their structure. The list renders PokemonCard in a three-column FlatList, and the party uses the same card beside the design system's EmptySlot. App-level colour literals move behind shared tokens; only token definitions and translucent detail overlays retain raw values:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Before, per app&lt;/th&gt;
&lt;th&gt;After, from the seam&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;#2a75bb&lt;/code&gt; in two apps' buttons&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;bg-pokemonGreen&lt;/code&gt;, one token&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;#6b7280&lt;/code&gt;, &lt;code&gt;#9ca3af&lt;/code&gt;, &lt;code&gt;#f3f4f6&lt;/code&gt; template greys&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;text-darkGrey&lt;/code&gt;, &lt;code&gt;text-midGrey&lt;/code&gt;, &lt;code&gt;bg-offGrey&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;#eef2ff&lt;/code&gt; / &lt;code&gt;#4338ca&lt;/code&gt; badge colours in the list&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;TYPE_BG_CLASS&lt;/code&gt; per real type&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;#fff&lt;/code&gt; screens&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ScreenContainer&lt;/code&gt; variants: &lt;code&gt;bg-offWhite&lt;/code&gt; light, &lt;code&gt;bg-navy&lt;/code&gt; dark&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two smaller changes come with the conversion. The detail now arrives as a push inside each tab's stack, with the design system's BackPill floating over the hero, so the tab bar stays put while a detail is open:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🎞️ &lt;strong&gt;Animated demo:&lt;/strong&gt; &lt;a href="https://warrendeleon.com/blog/federated-design-system-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-design-system#the-greys-collapse" rel="noopener noreferrer"&gt;watch it on warrendeleon.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And post 6's header Refresh button becomes a pull gesture the list owns. The dispatch is unchanged (one invalidated tag, which both of post 10's endpoints provide), so the REST rows and the GraphQL badges refetch together from a gesture instead of a button:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🎞️ &lt;strong&gt;Animated demo:&lt;/strong&gt; &lt;a href="https://warrendeleon.com/blog/federated-design-system-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-design-system#the-greys-collapse" rel="noopener noreferrer"&gt;watch it on warrendeleon.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  One toggle, three bundles
&lt;/h2&gt;

&lt;p&gt;ThemeToggle is a design-system component with no props: a press writes the colour scheme into the shared &lt;code&gt;nativewind&lt;/code&gt; singleton, and each remote mounts it in its own header's right slot. The interesting part is what it does not do: it never talks to a remote. The scheme is module-level state inside the shared styling runtime, every &lt;code&gt;dark:&lt;/code&gt; class in the runtime re-resolves on the write, and three independently shipped bundles repaint together. No doc promises that; it is verified in this build.&lt;/p&gt;

&lt;p&gt;The chrome changes on the same write. Everything derives from the same observable: the host themes its &lt;code&gt;NavigationContainer&lt;/code&gt; from &lt;code&gt;useColorScheme()&lt;/code&gt;, the same subscription the classes use. An earlier cut of this build kept a separate piece of host state beside it, and the native navigation bars flipped almost half a second before the content's styling pass. One source of truth put every surface on the same update wave.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🎞️ &lt;strong&gt;Animated demo:&lt;/strong&gt; &lt;a href="https://warrendeleon.com/blog/federated-design-system-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-design-system#one-toggle-three-bundles" rel="noopener noreferrer"&gt;watch it on warrendeleon.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Feedback rides the same singleton
&lt;/h2&gt;

&lt;p&gt;Party feedback uses the same shared package. &lt;code&gt;toast()&lt;/code&gt; is a plain function in &lt;code&gt;@pokedex/ui&lt;/code&gt;, while the host mounts the runtime's one &lt;code&gt;Toaster&lt;/code&gt; above the tab bar. After dispatching an add, the list reads the store and reports what the owner did. Two fast taps at five members produce one “joined your party” message and one “Your party is full” message because the reducer decides the outcome.&lt;/p&gt;

&lt;p&gt;The list never mounts the component that draws the capsule. It calls the shared presentation API, and the host's Toaster renders the result with the Pokémon's sprite and type colour.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🎞️ &lt;strong&gt;Animated demo:&lt;/strong&gt; &lt;a href="https://warrendeleon.com/blog/federated-design-system-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-design-system#feedback-rides-the-same-singleton" rel="noopener noreferrer"&gt;watch it on warrendeleon.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The party's remove confirms through the same path, which is the ownership rule from post 6 carried into presentation: each write's owner owns its feedback. &lt;code&gt;toast()&lt;/code&gt; also announces the message to the screen reader itself, so a caller cannot show the visual confirmation and skip the accessible one.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🎞️ &lt;strong&gt;Animated demo:&lt;/strong&gt; &lt;a href="https://warrendeleon.com/blog/federated-design-system-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-design-system#feedback-rides-the-same-singleton" rel="noopener noreferrer"&gt;watch it on warrendeleon.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Now break it
&lt;/h2&gt;

&lt;p&gt;The map earned its entries; prove it by removing one. In &lt;code&gt;apps/list/rspack.config.mjs&lt;/code&gt;, delete the &lt;code&gt;nativewind&lt;/code&gt; entry from &lt;code&gt;shared&lt;/code&gt;, restart the list's dev server, and reload.&lt;/p&gt;

&lt;p&gt;Nothing crashes. The app boots, the list renders, the sprites load, and the layout is quietly wrong: the grid's column wrappers lose their widths, so each card sizes to its own content and the three columns stop lining up between rows. The colours hold, and so do the badges and the type, which is what makes this failure easy to miss. The remote now bundles its own styling runtime. Its build compiles the remote's classes into a registry the shared components never read, and the widths are the part that goes missing on screen. The components from &lt;code&gt;@pokedex/ui&lt;/code&gt; are unaffected, because their classes were compiled by the host's scan into the host's runtime.&lt;/p&gt;

&lt;p&gt;Post 6's failure was loud, post 8's was silent, and this one is silent and partial: the screen still works, and its grid stops being the design system's grid. Put the entry back, restart, and the grid snaps into shape. &lt;strong&gt;A styling runtime is shared state, and shared state under federation is either a singleton or a bug.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Run it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Native assets still ship with the host binary.&lt;/strong&gt; Copy the tab icons, app icon, launch ball, Nunito fonts and native configuration from the companion. The &lt;code&gt;ios/Host&lt;/code&gt; copy carries the icon catalogue, launch storyboard and app delegate. Copy &lt;code&gt;project.pbxproj&lt;/code&gt; too, because Xcode renders the loose splash images only when the project lists them as resources. The final command links the typefaces:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; /tmp/pokedex-ref/apps/host/src/assets apps/host/src/
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; /tmp/pokedex-ref/assets &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-ref/apps/host/react-native.config.js apps/host/
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-ref/apps/list/react-native.config.js apps/list/
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-ref/apps/party/react-native.config.js apps/party/
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; /tmp/pokedex-ref/apps/host/ios/Host apps/host/ios/
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-ref/apps/host/ios/Host.xcodeproj/project.pbxproj apps/host/ios/Host.xcodeproj/
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; /tmp/pokedex-ref/apps/host/android/app/src/main/res apps/host/android/app/src/main/
&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/host &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npx react-native-asset@2.3.0 &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verdaccio is already running from the publish steps, and the three packages are published. Start the three dev servers, each from its own terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/list &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run start:remote &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/party &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run start:remote &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/host &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm start &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then build and launch the host app once, so the new app icon and asset catalogue compile in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/host &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run ios &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open the Pokédex tab for the three-column card grid, tap a Pokémon for the finished detail, add it to the party and watch the confirmation rise, then flip the header toggle to watch every surface, both remotes and the chrome included, change scheme on one press. The whole loop, splash to empty party, looks like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🎞️ &lt;strong&gt;Animated demo:&lt;/strong&gt; &lt;a href="https://warrendeleon.com/blog/federated-design-system-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-design-system#run-it" rel="noopener noreferrer"&gt;watch it on warrendeleon.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What you built, and what's next
&lt;/h2&gt;

&lt;p&gt;One design system and one styling runtime now serve every remote. The remotes import &lt;code&gt;@pokedex/ui&lt;/code&gt; as a package, while federation resolves it to the host's copy. A shell release can re-skin them without rebuilding each remote, provided the component API remains compatible.&lt;/p&gt;

&lt;p&gt;The native boundary remains. &lt;code&gt;react-native-svg&lt;/code&gt; ships a native module, and native code cannot arrive through federation. Fonts reach the same boundary for a different reason: the binary must contain and register each typeface at build time.&lt;/p&gt;

&lt;p&gt;The companion makes both cases visible. The tab icons are plain tinted images, the Nunito faces are linked into the host binary, and the launch ball is a storyboard asset.&lt;/p&gt;

&lt;p&gt;Next: accessibility across the seam, a shared testing package that checks touch targets, contrast and focus order in remotes the host only meets at runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://gluestack.io/" rel="noopener noreferrer"&gt;gluestack-ui&lt;/a&gt; — the copy-in component architecture; "you copy, paste, and own your components"&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.nativewind.dev/" rel="noopener noreferrer"&gt;NativeWind&lt;/a&gt; — the className runtime, and the other-bundlers page that lists community solutions&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.npmjs.com/package/@callstack/repack-plugin-nativewind" rel="noopener noreferrer"&gt;@callstack/repack-plugin-nativewind&lt;/a&gt; — the first-party plugin this post wires&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.npmjs.com/package/@callstack/repack-plugin-reanimated" rel="noopener noreferrer"&gt;@callstack/repack-plugin-reanimated&lt;/a&gt; — the Reanimated transform Re.Pack asks for by name&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://v3.tailwindcss.com/docs/content-configuration" rel="noopener noreferrer"&gt;Tailwind v3: content configuration&lt;/a&gt; — scanning your own component library's source&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/callstack/repack/discussions/1032" rel="noopener noreferrer"&gt;callstack/repack discussion #1032&lt;/a&gt; — a remote's styles clobbering the host's registry; still open upstream&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/callstack/repack/pull/1242" rel="noopener noreferrer"&gt;callstack/repack PR #1242&lt;/a&gt; — the closed accumulation attempt; this build's singleton result is its own evidence&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html" rel="noopener noreferrer"&gt;WCAG 2.2, contrast (minimum)&lt;/a&gt; — the 4.5:1 bar the type-colour tokens are computed against&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/warrendeleon/react-native-module-federation" rel="noopener noreferrer"&gt;react-native-module-federation&lt;/a&gt; — the companion repo, the build at the tag &lt;code&gt;post-11-design-system&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>reactnative</category>
      <category>modulefederation</category>
      <category>repack</category>
      <category>rspack</category>
    </item>
    <item>
      <title>Two backends, one client? RTK Query vs Apollo in React Native</title>
      <dc:creator>Warren de Leon</dc:creator>
      <pubDate>Mon, 31 Aug 2026 00:04:31 +0000</pubDate>
      <link>https://dev.to/warrendeleon/two-backends-one-client-rtk-query-vs-apollo-in-react-native-31ln</link>
      <guid>https://dev.to/warrendeleon/two-backends-one-client-rtk-query-vs-apollo-in-react-native-31ln</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📚 &lt;strong&gt;React Native Module Federation&lt;/strong&gt; series — &lt;a href="https://warrendeleon.com/blog/series/react-native-module-federation/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-rtk-query-apollo" rel="noopener noreferrer"&gt;read it in full on warrendeleon.com&lt;/a&gt;, where new parts land first.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://warrendeleon.com/blog/state-stacks-under-federation-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-rtk-query-apollo" rel="noopener noreferrer"&gt;Post 9&lt;/a&gt; closed on a promise: the stack stays the same and the backend splits in two, REST for the list, GraphQL for the type badges, one client and one cache. This post builds it.&lt;/p&gt;

&lt;p&gt;The question mark in the title is deliberate. During the transition, with a GraphQL backend arriving while REST is still live, one client wins almost regardless of which one it is, and if your app already runs RTK Query that settles it. At the destination, with GraphQL everywhere, the answer turns on two things you can check against your own app: whether you have a federation constraint, and whether your schema is relational enough that a normalised cache earns its keep. A normalised cache stores each entity once, one Pokémon in one slot, so every view that references it reads the same copy. This series has federation, so RTK Query stays the pick here. Change the context and the answer changes, and this post says where.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📊 &lt;strong&gt;Diagram:&lt;/strong&gt; &lt;a href="https://warrendeleon.com/blog/rtk-query-vs-apollo-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-rtk-query-apollo" rel="noopener noreferrer"&gt;view it on warrendeleon.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The left shape is the build: REST and GraphQL feeding one &lt;code&gt;baseApi&lt;/code&gt; cache under one tag graph, with the host's Refresh button untouched. The right shape is two clients with two caches and no edge between them. That missing edge is what this post is really about.&lt;/p&gt;

&lt;h2&gt;
  
  
  A second backend arrives
&lt;/h2&gt;

&lt;p&gt;The API team stands up GraphQL. For screens that need related data it can replace several round trips with one, which is where the speed-up comes from rather than from the protocol itself, and it is where the backend is heading. REST is not going away this quarter, or the next. For a while, often a long while, both are live, and every screen is served by one or the other.&lt;/p&gt;

&lt;p&gt;The hazard is running two data-fetching clients at once, whichever two you pick. Two clients keep two caches, and the two do not coordinate on their own. A mutation that goes out over GraphQL updates the GraphQL cache, and the update never reaches the REST cache holding the same record. The user edits a Pokémon on the screen served by GraphQL, switches to the list served by REST, and sees the old value. No exception, no failed request, no warning. Just a screen that is quietly wrong until something forces a refetch. Mid-cutover, when the two backends overlap most, is exactly when it does the most damage.&lt;/p&gt;

&lt;p&gt;So the transition question is how few caches to run, and the cheapest answer is one.&lt;/p&gt;

&lt;h2&gt;
  
  
  One slice, two protocols
&lt;/h2&gt;

&lt;p&gt;RTK Query makes one cache serve both protocols, in the slice this series has run since &lt;a href="https://warrendeleon.com/blog/one-shared-store-across-remotes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-rtk-query-apollo" rel="noopener noreferrer"&gt;post 6&lt;/a&gt;. Start from post 8's tag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/warrendeleon/react-native-module-federation
&lt;span class="nb"&gt;cd &lt;/span&gt;react-native-module-federation
git checkout post-08-client-state
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The list already fetches its rows over REST. It gains a second &lt;a href="https://warrendeleon.com/blog/server-state-and-client-state-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-rtk-query-apollo" rel="noopener noreferrer"&gt;server-state&lt;/a&gt; need, a type badge on each row, served over GraphQL. Two dependencies first, in the list remote only. &lt;code&gt;graphql&lt;/code&gt; stays on 16 because &lt;code&gt;graphql-request&lt;/code&gt; declares a peer range of 14 to 16 while npm's latest is 17:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/list &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install &lt;/span&gt;graphql@16.14.2 graphql-request@7.4.0 &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the endpoint, in the same api slice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;typesApi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;baseApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;injectEndpoints&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;endpoints&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;build&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;getPokemonTypes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;build&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;queryFn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;GRAPHQL_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;POKEMON_TYPES&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;parsePokemonTypes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CUSTOM_ERROR&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invalid GraphQL response&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;providesTags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PokemonList&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useGetPokemonTypesQuery&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;typesApi&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An api slice has exactly one &lt;code&gt;baseQuery&lt;/code&gt;, and the list's is &lt;code&gt;fetchBaseQuery&lt;/code&gt; on the REST base. A second protocol against a different URL comes from a &lt;code&gt;queryFn&lt;/code&gt;, which RTK's own docs list for "one-off queries that use a different base URL". &lt;code&gt;graphql-request&lt;/code&gt; posts the query, and the same file carries what the excerpt leaves out: the bounded, ordered GraphQL query itself, the Zod schema that polices ids and type names, &lt;code&gt;parsePokemonTypes&lt;/code&gt;, and the screen integration. Rather than print all of it, land your tree on the finished state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx degit@3.8.0 &lt;span class="nt"&gt;--force&lt;/span&gt; warrendeleon/react-native-module-federation#post-10-two-backends /tmp/pokedex-ref-10
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; /tmp/pokedex-ref-10/. &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response is validated with its own &lt;a href="https://zod.dev/" rel="noopener noreferrer"&gt;Zod&lt;/a&gt; schema at the seam the REST list already guards, held constant so the comparison swaps only the protocol. The schema lives in the list app rather than the contract package, because a data definition travels with the domain that owns it (&lt;a href="https://warrendeleon.com/blog/who-owns-what-federated-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-rtk-query-apollo" rel="noopener noreferrer"&gt;post 7's rule&lt;/a&gt;).&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;PokéAPI's GraphQL lives at &lt;code&gt;graphql.pokeapi.co/v1beta2&lt;/code&gt;. The older &lt;code&gt;v1beta&lt;/code&gt; schema is retired, and with it the &lt;code&gt;pokemon_v2_&lt;/code&gt; field prefix that fills every pre-2025 tutorial. Asking for &lt;code&gt;pokemon_v2_pokemon&lt;/code&gt; today returns &lt;code&gt;field 'pokemon_v2_pokemon' not found in type: 'query_root'&lt;/code&gt;. The working shape is the unprefixed one, checked live against the endpoint while writing this.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Two things stay deliberately absent. There is no contracts bump: the endpoint injects into the existing &lt;code&gt;baseApi&lt;/code&gt;, and it declares the tag the list already owns. And &lt;code&gt;graphql-request&lt;/code&gt; stays out of the Module Federation shared map, exactly as Zod does. It is a value-only library with no instance identity to share, so it rides as the list remote's own dependency. Which protocol served a row stays the remote's private business, invisible to the host and to every other remote.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;PokéAPI's GraphQL is rate-limited to 100 calls an hour per IP, where REST is fair-use only. RTK Query's caching absorbs it in normal use. A run of cold reloads can still hit the cap, and a 429 mid-build is the limit, not a bug.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  One tag across the cutover
&lt;/h2&gt;

&lt;p&gt;The host's Refresh button has dispatched &lt;code&gt;invalidateTags(['PokemonList'])&lt;/code&gt; since post 6, and it holds no reference to either endpoint. Both the REST list and the GraphQL types query provide &lt;code&gt;'PokemonList'&lt;/code&gt;, so one press refetches both. React Native DevTools' Network panel shows it happening: one tap lands both requests together, and the &lt;code&gt;v1beta2&lt;/code&gt; preview shows the type data arriving over GraphQL.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fya0simec9ptxvbkragmk.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fya0simec9ptxvbkragmk.webp" alt="React Native DevTools' Network panel: an empty log, then one Refresh tap lands two requests together, the GraphQL v1beta2 call and the REST pokemon list call, and the GraphQL row's preview shows the pokemontypes data arriving" width="800" height="551"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That is the transition thesis in one tag: &lt;strong&gt;whichever client you keep, keep one&lt;/strong&gt;. Nothing crosses automatically, though. A mutation clears the other protocol's cache when it names the tag they both provide, and only then; the shared graph is what makes that possible, not what performs it.&lt;/p&gt;

&lt;p&gt;The same race is visible on the device: on a cold start the REST rows land first, and the GraphQL badges arrive a moment later as each query fills the shared cache:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🎞️ &lt;strong&gt;Animated demo:&lt;/strong&gt; &lt;a href="https://warrendeleon.com/blog/rtk-query-vs-apollo-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-rtk-query-apollo#one-tag-across-the-cutover" rel="noopener noreferrer"&gt;watch it on warrendeleon.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The badges also degrade quietly. Point the GraphQL endpoint at an unreachable host and the rows still render, the badges are simply absent, and nothing moves on the screen.&lt;/p&gt;

&lt;p&gt;The badges make GraphQL's own pitch concrete too. Types over REST would mean 151 detail calls, one per row, because the list endpoint returns only names and URLs. Over GraphQL it is a single query. Call that what it is, client request fan-out rather than N+1: the classic problem describes a backend repeating a data access per row, while this is one client making a request per row because the endpoint gives it nothing else. Fewer round trips is the win, and it is a point for the protocol rather than for either client.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Apollo does that this can't
&lt;/h2&gt;

&lt;p&gt;RTK Query treats a GraphQL response as data to cache by endpoint, the same as any REST payload. Apollo does something RTK Query does not attempt: with its default &lt;code&gt;InMemoryCache&lt;/code&gt; it normalises. Each object with a &lt;code&gt;__typename&lt;/code&gt; and an id gets one slot in the cache, and every query that references it reads that slot. A mutation whose response carries the same type and id updates the entity everywhere it appears, with no refetch.&lt;/p&gt;

&lt;p&gt;That is real, and it is the honest reason to reach for Apollo. It is also narrower than the pitch. Auto-consistency covers modifying an entity already in the cache. It does not cover growing a list. In Apollo's own words, "a newly cached object isn't automatically added to any &lt;em&gt;list fields&lt;/em&gt; that should now include that object", so a new row still needs an update function or a refetch.&lt;/p&gt;

&lt;p&gt;The rest of Apollo's case holds up on its own terms. Fragment colocation lets a component declare the fields it needs and compose them upward by plain template-literal interpolation, with no build step; code generation is optional, and earns its place when you want typed results. Cache redirects can serve a detail view straight from data a list already fetched, but only when every field the detail query asks for is already in the cache. One extra field and the whole query hits the network. Subscriptions and &lt;code&gt;@defer&lt;/code&gt; are supported, and &lt;code&gt;@defer&lt;/code&gt; needs an incremental-delivery handler plus a streaming-fetch polyfill on React Native, so it is "supported, with setup" rather than free.&lt;/p&gt;

&lt;p&gt;And the schema matters more than any of this. PokéAPI's own shape, Pokémon referencing types, abilities, species and evolution chains, with the same entities reused across many queries, is exactly the relational shape where normalisation pays for itself. On this data, Apollo's cache is the better fit, and pretending otherwise would be the strawman this series avoids. (Relay sits in the same family. urql belongs there only once you add Graphcache: its default document cache stores whole responses by query, closer to RTK Query's model than to Apollo's. The trade-offs in &lt;em&gt;Choosing&lt;/em&gt; are about the normalised family, not any one library.)&lt;/p&gt;

&lt;h2&gt;
  
  
  What federation does to the choice
&lt;/h2&gt;

&lt;p&gt;Start with the part that surprises people. Apollo has no &lt;code&gt;injectEndpoints&lt;/code&gt;, and it needs none. An operation is a document executed against a client, not an artifact registered in a store, so any remote can ship its own queries against a shared client with zero registration machinery. A remote can even extend the cache at mount through &lt;code&gt;cache.policies.addTypePolicies&lt;/code&gt;, which is documented public API. There is no official micro-frontend guide (an unanswered community thread and one customer story are the whole record), but the cache API happens to allow it. On the runtime-extensibility axis &lt;a href="https://warrendeleon.com/blog/state-stacks-under-federation-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-rtk-query-apollo" rel="noopener noreferrer"&gt;post 9&lt;/a&gt; cared about, Apollo is not behind.&lt;/p&gt;

&lt;p&gt;It also does not share TanStack's context problem. Apollo caches a single React context on the React instance itself, keyed by a well-known symbol, precisely so two copies of the library cannot hand you divergent contexts. The "no client in context" error fires only when React itself is duplicated. The singleton case for &lt;code&gt;@apollo/client&lt;/code&gt; under federation is one cache instance and no version skew between the client and its hooks, not the context split.&lt;/p&gt;

&lt;p&gt;Federation still tips the choice in two places: the tag graph and the transition. The one tag that refetches across remotes, and across two protocols, is coordination RTK Query has &lt;a href="https://warrendeleon.com/blog/rtk-query-tags-vs-tanstack-query-keys/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-rtk-query-apollo" rel="noopener noreferrer"&gt;by construction&lt;/a&gt; and Apollo leaves to convention. And adopting Apollo mid-cutover means standing up a second cache next to the RTK Query one you already run, the exact two-cache shape from the start of this post, for the length of the migration. Apollo Client 4 also makes rxjs a required peer and moves its React exports, so the bundle gains a new dependency rather than swapping one.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;p&amp;gt;RTK Query&amp;lt;/p&amp;gt;
&amp;lt;ul&amp;gt;
  &amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;Cache model:&amp;lt;/strong&amp;gt; by endpoint, refetch on invalidate&amp;lt;/li&amp;gt;
  &amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;Cross-protocol invalidation:&amp;lt;/strong&amp;gt; one shared tag graph, built in&amp;lt;/li&amp;gt;
  &amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;Adding queries from a remote:&amp;lt;/strong&amp;gt; &amp;lt;code&amp;gt;injectEndpoints&amp;lt;/code&amp;gt; at runtime&amp;lt;/li&amp;gt;
  &amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;Relational, entity-reuse schema:&amp;lt;/strong&amp;gt; refetches what it already had&amp;lt;/li&amp;gt;
&amp;lt;/ul&amp;gt;


&amp;lt;p&amp;gt;Apollo&amp;lt;/p&amp;gt;
&amp;lt;ul&amp;gt;
  &amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;Cache model:&amp;lt;/strong&amp;gt; normalised by &amp;lt;code&amp;gt;__typename&amp;lt;/code&amp;gt; + id, patch in place&amp;lt;/li&amp;gt;
  &amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;Cross-protocol invalidation:&amp;lt;/strong&amp;gt; per-client; a second cache to coordinate&amp;lt;/li&amp;gt;
  &amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;Adding queries from a remote:&amp;lt;/strong&amp;gt; documents against the client; nothing to register&amp;lt;/li&amp;gt;
  &amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;Relational, entity-reuse schema:&amp;lt;/strong&amp;gt; the cache pays for itself&amp;lt;/li&amp;gt;
&amp;lt;/ul&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  Choosing
&lt;/h2&gt;

&lt;p&gt;No absolute winner, so read down to the case that fits your app.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Transition, both backends live:&lt;/strong&gt; run one client. If the app is already on RTK Query, and this series is, that decides it. One cache, one tag, no two-cache staleness to police, and it is the common case.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Destination, GraphQL everywhere, with a federation constraint and shared coordinated state:&lt;/strong&gt; RTK Query stays. The honest price is living without normalisation: tag invalidation refetches where Apollo would patch in place, and &lt;code&gt;updateQueryData&lt;/code&gt; covers the few spots where a hand-patched update is worth it. You trade some round trips for one cache and one tag graph across independently shipped remotes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Destination, GraphQL everywhere, a relational entity-heavy schema, and no federation constraint:&lt;/strong&gt; Apollo, plainly. The normalised cache is the point of a GraphQL client, and without federation pulling the other way, that is the reason to adopt it. No hedge.&lt;/p&gt;

&lt;p&gt;The signals to watch for are two specific days. The day you notice most of your invalidations refetch data the cache already held: normalisation would have saved those round trips. And the day the second backend goes live and two caches disagree on screen with no error to explain it. The first is a nudge toward Apollo. The second is the reason to run one client, and only one, all along.&lt;/p&gt;

&lt;p&gt;Next: the design system becomes a federated singleton, one UI package shared at runtime, so every remote renders the same components without shipping its own copy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://redux-toolkit.js.org/rtk-query/usage/customizing-queries" rel="noopener noreferrer"&gt;RTK Query: customising queries&lt;/a&gt; — &lt;code&gt;queryFn&lt;/code&gt; as an inline baseQuery, and the GraphQL baseQuery example&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://redux-toolkit.js.org/rtk-query/usage/automated-refetching" rel="noopener noreferrer"&gt;RTK Query: automated re-fetching&lt;/a&gt; — providing and invalidating by tag&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.apollographql.com/docs/react/caching/overview" rel="noopener noreferrer"&gt;Apollo: caching overview&lt;/a&gt; — normalisation by &lt;code&gt;__typename&lt;/code&gt; and id&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.apollographql.com/docs/react/caching/advanced-topics" rel="noopener noreferrer"&gt;Apollo: cache redirects&lt;/a&gt; — serving a query from the cache, and the all-fields-present rule&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.apollographql.com/docs/react/data/defer" rel="noopener noreferrer"&gt;Apollo: &lt;code&gt;@defer&lt;/code&gt;&lt;/a&gt; — incremental delivery and its setup&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://pokeapi.co/docs/graphql" rel="noopener noreferrer"&gt;PokéAPI GraphQL&lt;/a&gt; — the v1beta2 endpoint and the 100-calls-per-hour limit&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.npmjs.com/package/graphql-request" rel="noopener noreferrer"&gt;graphql-request&lt;/a&gt; — the fetch-based client the queryFn uses&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/warrendeleon/react-native-module-federation" rel="noopener noreferrer"&gt;react-native-module-federation&lt;/a&gt; — the companion repo, the build at the tag &lt;code&gt;post-10-two-backends&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>reactnative</category>
      <category>modulefederation</category>
      <category>repack</category>
      <category>rspack</category>
    </item>
    <item>
      <title>State stacks under federation: the same React Native app built twice</title>
      <dc:creator>Warren de Leon</dc:creator>
      <pubDate>Mon, 24 Aug 2026 00:09:59 +0000</pubDate>
      <link>https://dev.to/warrendeleon/state-stacks-under-federation-the-same-react-native-app-built-twice-3d30</link>
      <guid>https://dev.to/warrendeleon/state-stacks-under-federation-the-same-react-native-app-built-twice-3d30</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📚 &lt;strong&gt;React Native Module Federation&lt;/strong&gt; series — &lt;a href="https://warrendeleon.com/blog/series/react-native-module-federation/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-state-stacks" rel="noopener noreferrer"&gt;read it in full on warrendeleon.com&lt;/a&gt;, where new parts land first.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://warrendeleon.com/blog/client-state-across-federated-remotes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-state-stacks" rel="noopener noreferrer"&gt;Post 8&lt;/a&gt; ended by promising this one: the same app rebuilt on TanStack Query and Zustand, the stack most React Native teams reach for first. That build now exists, on a branch cut from post 8's tag.&lt;/p&gt;

&lt;p&gt;The rules I set myself: same three apps, same installed detail package, same feature set (the Pokédex list, the detail with its capped Add button, the party grid with remove, the live "My Party 3/6" counter), and no change the state stack did not force. The finished twin looks the same on the simulator, screen for screen, which is the control this comparison needs.&lt;/p&gt;

&lt;p&gt;The verdict, before the proof. Both stacks build this app well. The TanStack and Zustand version has fewer moving parts, and for adding server state from a remote it is the more federation-friendly of the two: nothing to register, nothing to inject. Redux Toolkit buys two narrow things: a store that grows at runtime, and a closed set of writes. They matter only when separately deployed teams share live client state with rules that have to hold. If your app never reaches that point, the lighter stack wins and most of this argument will not apply.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📊 &lt;strong&gt;Diagram:&lt;/strong&gt; &lt;a href="https://warrendeleon.com/blog/state-stacks-under-federation-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-state-stacks" rel="noopener noreferrer"&gt;view it on warrendeleon.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The arrows run into the contract on the Redux side and out of it on the twin: Redux lets the remotes add to the seam at runtime, and this branch has them consume what the seam already ships. &lt;code&gt;@pokedex/detail&lt;/code&gt; sits in both halves at the same version, because it is the same package, unchanged.&lt;/p&gt;

&lt;h2&gt;
  
  
  Server state: nothing to inject
&lt;/h2&gt;

&lt;p&gt;TanStack Query's model is decentralised, and that is its best argument here. There is no central API object, no registration step, no build-time list of what exists. Any component in any bundle calls &lt;code&gt;useQuery&lt;/code&gt; with any key against the shared client. A remote shipped a year after the shell can add server state to the running app by calling a hook.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;A claim you will meet says TanStack needs every query key registered at the shell's build time.&lt;/strong&gt; It is false. A key is a plain array named at the call site, so a remote shipped long after the shell registers its own keys at runtime, against the shared client, with nothing to inject. If anything, that is the more decentralised model.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Post 8 needed &lt;code&gt;baseApi.injectEndpoints&lt;/code&gt;, one shared instance, and the middleware that runs its cache. The list app's data access on this branch is a fetch and a hook:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// apps/list/src/listApi.ts&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fetchPokemonList&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;PokemonSummary&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://pokeapi.co/api/v2/pokemon?limit=151&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`PokéAPI responded &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;parsePokemonList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;usePokemonList&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;useQuery&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;queryKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pokemonKeys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="na"&gt;queryFn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fetchPokemonList&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;parsePokemonList&lt;/code&gt; is imported from the contract and is byte-identical to post 8's. The &lt;a href="https://warrendeleon.com/blog/runtime-api-validation-zod-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-state-stacks" rel="noopener noreferrer"&gt;Zod&lt;/a&gt; parsing, the model, the shapes: all of it survives the swap, because the data model belongs to the domain rather than to whichever library is holding the cache.&lt;/p&gt;

&lt;p&gt;Sharing one client across separately built bundles is supported, with a caveat. The v5 migration guide's answer for micro-frontends is to pass a &lt;code&gt;queryClient&lt;/code&gt; to a hook or a provider, replacing v4's custom context. That is a paragraph in a migration guide rather than a chapter on federation. The failure modes in React Native are the singleton ones this series has met before: two copies of &lt;code&gt;@tanstack/react-query&lt;/code&gt; give you "No QueryClient set", two copies of React give you null hooks.&lt;/p&gt;

&lt;p&gt;The cost lands somewhere else. RTK's injected endpoints extend one typed surface, so a team adding an endpoint sees the whole API and the tag graph it shares. TanStack's coordination is a string array. My answer is the contract package again: the key factory ships beside the client, so a rename is a version bump every consumer sees.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// packages/contracts/src/query.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;queryClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;QueryClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;defaultOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;queries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;staleTime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pokemonKeys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;all&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pokemon&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;list&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;pokemonKeys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;all&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;list&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;pokemonKeys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;all&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;detail&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a recommendation, not documented practice. Key factories are established community practice for key sprawl at scale rather than an answer the official docs prescribe, and nothing in either addresses two teams owning keys under one prefix.&lt;/p&gt;

&lt;p&gt;Post 8's endpoint-name collision has a quieter mirror here. On RTK, two apps injecting an endpoint called &lt;code&gt;getPokemonDetail&lt;/code&gt; had to declare the collision with &lt;code&gt;overrideExisting&lt;/code&gt;, and load order picked the winner. On TanStack, two apps calling &lt;code&gt;useQuery&lt;/code&gt; with &lt;code&gt;pokemonKeys.detail(1)&lt;/code&gt; share one cache entry, because keys compare by value. Nothing is registered, so nothing collides. Open Bulbasaur in the Pokédex and open it again from the Party tab, and the second screen renders from cache.&lt;/p&gt;

&lt;p&gt;Whether a request follows is a default worth knowing: left on its defaults, TanStack marks cached data stale immediately (&lt;code&gt;staleTime: 0&lt;/code&gt;), so a second observer mounting on the same key renders the cache &lt;em&gt;and&lt;/em&gt; fires a background refetch. The &lt;code&gt;staleTime: 60_000&lt;/code&gt; on the client above (a minute, matching how long the Redux build keeps an unsubscribed cache entry) is what makes the remount cost no second request, so the two builds answer it the same way. The bad version of the shared entry is two teams whose &lt;code&gt;queryFn&lt;/code&gt;s have drifted, where the data depends on which screen the user opened first.&lt;/p&gt;

&lt;p&gt;One small thing the shell gave up. Post 8's &lt;code&gt;baseApi&lt;/code&gt; carried the base URL for every consumer. Here each &lt;code&gt;queryFn&lt;/code&gt; fetches whatever it likes, and the host has no say in where a remote's data comes from.&lt;/p&gt;

&lt;h2&gt;
  
  
  Invalidation: a prefix is a promise
&lt;/h2&gt;

&lt;p&gt;The host's Refresh button is host chrome that reaches into a remote's data without importing any of it. Post 8 dispatched &lt;code&gt;invalidateTags(['PokemonList'])&lt;/code&gt;. The twin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Pressable&lt;/span&gt;
  &lt;span class="na"&gt;onPress&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;queryClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invalidateQueries&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;queryKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pokemonKeys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;all&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;hitSlop&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;refresh&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;accessibilityRole&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt;
  &lt;span class="na"&gt;accessibilityLabel&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Refresh Pokédex"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;refreshText&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Refresh&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Pressable&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Query filters match by prefix, and only an &lt;code&gt;exact&lt;/code&gt; filter narrows to one key, so everything the factory builds under &lt;code&gt;['pokemon']&lt;/code&gt; goes stale at once and any mounted query refetches. The prefix is the whole agreement.&lt;/p&gt;

&lt;p&gt;I compared these two models in &lt;a href="https://warrendeleon.com/blog/rtk-query-tags-vs-tanstack-query-keys/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-state-stacks" rel="noopener noreferrer"&gt;tags vs query keys&lt;/a&gt;, and that argument holds here. Federation adds one thing: the two sides are separately deployed bundles that never sat in the same build. A tag is a value declared in a shared list; a key prefix is string equality across code that ships on different days. Putting the factory in the versioned contract turns the convention back into something a compiler and a release process can see.&lt;/p&gt;

&lt;h2&gt;
  
  
  Client state: the store moves back to the shell
&lt;/h2&gt;

&lt;p&gt;This is where the two designs differ most.&lt;/p&gt;

&lt;p&gt;In post 8 the party app created the party's state, owned it, and injected it into a store the host had already built. &lt;code&gt;combineSlices&lt;/code&gt; exists for exactly that, and the slice arrived at runtime from a bundle the shell knew nothing about.&lt;/p&gt;

&lt;p&gt;Zustand has no equivalent to that injection step. It creates stores dynamically without any trouble, so a remote can bring a store to a running app; what it has no built-in answer for is folding one into an existing store the way &lt;code&gt;inject&lt;/code&gt; does. I do not read that as an oversight: Zustand's natural federated answer is several independent stores, one per remote, with no machinery at all. That answer works until a piece of state has to cross a module boundary, which is the case this whole series is about.&lt;/p&gt;

&lt;p&gt;So the store ships in the contract package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// packages/contracts/src/partyStore.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;partyStore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;createStore&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;PartyState&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="kd"&gt;set&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;members&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
  &lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;member&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;members&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;MAX_PARTY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// the cap lives with the owner's action&lt;/span&gt;
    &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;members&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;members&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;member&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;members&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;members&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uid&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})),&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;createStore&lt;/code&gt; from &lt;code&gt;zustand/vanilla&lt;/code&gt;, because the contract holds no React. Consumers subscribe with &lt;code&gt;useStore(partyStore, selector)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The ownership inversion is the honest cost. The party app no longer writes the party's rules; it reads state that arrives in a package it installs. Its &lt;code&gt;partySlice.ts&lt;/code&gt; is deleted, and so is the &lt;code&gt;./partySlice&lt;/code&gt; entry in its Module Federation config, so it exposes one module again instead of two. On the host side, &lt;code&gt;src/store.ts&lt;/code&gt; is gone, the boot import that loaded the party's state module is gone, and the ambient declaration that typed it went with them.&lt;/p&gt;

&lt;p&gt;Those deletions are the trade. Post 8 spent a section on a dispatch that vanishes because the slice's owner had not loaded yet, and it took a boot import, a readiness marker and a gated button to close that window. Here the failure cannot occur: the state exists as soon as anything imports the contract. The price was paid earlier, as a release. Every new piece of shared state is a new version of &lt;code&gt;@pokedex/contracts&lt;/code&gt; that every app installs: a deploy on the web, an App Store cycle on mobile. Neither stack removes the coordination; they move it to a different day.&lt;/p&gt;

&lt;p&gt;The component package is the control group. &lt;code&gt;@pokedex/detail&lt;/code&gt; 3.1.0 is untouched: same version, same file, installed by both apps, wired to a Zustand action on one branch and a Redux action creator on the other. Worth being exact about what Redux matches on, because the comparison rests on it: a reducer recognises a closed set of action-type strings, so the two sides agree on &lt;code&gt;party/add&lt;/code&gt;, not on a privileged creator object. A view that renders what it is handed does not care which library computed it. That is the payoff of the &lt;a href="https://warrendeleon.com/blog/one-shared-store-across-remotes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-state-stacks" rel="noopener noreferrer"&gt;library rule from post 6&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ownership: setState from anywhere
&lt;/h2&gt;

&lt;p&gt;The cap is a rule the party owns, and on this branch it lives inside &lt;code&gt;add&lt;/code&gt;. Every write that goes through &lt;code&gt;add&lt;/code&gt; is capped. &lt;code&gt;setState&lt;/code&gt; is public on every Zustand store, and it does not go through &lt;code&gt;add&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The whole demonstration sits in the list app's detail container, next to the legitimate wiring:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// apps/list/src/ListStack.tsx — a module with no business writing this state&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bypassTheCap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
  &lt;span class="nx"&gt;partyStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;members&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;members&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`bypass-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;members&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;spriteUri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;spriteUri&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No type error. No warning. Nothing the owning team can review. The party fills to six, the Add button disables itself, and a long-press on the same screen puts a seventh Pokémon in a six-slot party:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🎞️ &lt;strong&gt;Animated demo:&lt;/strong&gt; &lt;a href="https://warrendeleon.com/blog/state-stacks-under-federation-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-state-stacks#ownership-setstate-from-anywhere" rel="noopener noreferrer"&gt;watch it on warrendeleon.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The long-press is only there so the bypass can be filmed. The call is the point.&lt;/p&gt;

&lt;p&gt;Watch the header. The party's grid renders six slots, so the seventh member has nowhere to appear, and the counter reading &lt;strong&gt;7/6&lt;/strong&gt; is the only surface that shows it. The owning team's own UI cannot display the state a foreign module just wrote.&lt;/p&gt;

&lt;p&gt;Redux permits foreign writes too: any module can dispatch anything. What differs is that a slice declares every write the owner will honour, so an action the owner never declared changes nothing, and the legitimate mutations are readable in one file. A Zustand store exposes the state itself, and a rule inside a function binds only the callers who use that function.&lt;/p&gt;

&lt;p&gt;Inside one team this is a non-issue. Everyone knows &lt;code&gt;setState&lt;/code&gt; is there, code review catches it, and the convention holds because the people holding it sit together. The moment the writer and the owner are different teams on different release schedules, a convention is what you have in place of a boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing
&lt;/h2&gt;

&lt;p&gt;There is no absolute winner. The deciding factor is who is allowed to write what.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One team owns all the state.&lt;/strong&gt; TanStack Query and Zustand. Fewer concepts, less ceremony, nothing to inject, and &lt;code&gt;setState&lt;/code&gt; is a convenience rather than a hole.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Independent teams, shared server cache, no shared client state.&lt;/strong&gt; Still TanStack. Share the client as a singleton, put the key factory in a versioned contract, and you have the coordination you need.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Independent teams writing each other's client state, with rules that must hold.&lt;/strong&gt; This is the case Redux Toolkit's weight is for: a store that grows at runtime and a closed set of writes per slice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mobile, with a slow release cycle.&lt;/strong&gt; Weigh the shape of the change. On the Zustand branch every new piece of shared state is a contract release, and on mobile a release waits on App Store review.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The signal to watch for is the day a second team needs to write state the first team owns. After that day, the question is whether you would rather coordinate through a type or through a convention.&lt;/p&gt;

&lt;p&gt;The twin is on its own branch, cut from post 8's tag, at &lt;code&gt;post-09-tanstack-zustand&lt;/code&gt;. It is a fork, not a step forward: the series carries on from post 8 on &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Next, the other half of the comparison. The stack stays the same and the backend splits in two: REST for the list, GraphQL for the type badges, one client, one cache, two teams.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://tanstack.com/query/latest/docs/framework/react/guides/migrating-to-v5" rel="noopener noreferrer"&gt;TanStack Query: migrating to v5&lt;/a&gt; — the micro-frontend answer, passing a &lt;code&gt;queryClient&lt;/code&gt; in place of a custom context&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://tanstack.com/query/latest/docs/framework/react/guides/filters" rel="noopener noreferrer"&gt;TanStack Query: query filters&lt;/a&gt; — prefix matching, and &lt;code&gt;exact&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://tanstack.com/query/latest/docs/framework/react/guides/query-keys" rel="noopener noreferrer"&gt;TanStack Query: query keys&lt;/a&gt; and &lt;a href="https://tkdodo.eu/blog/effective-react-query-keys" rel="noopener noreferrer"&gt;effective query keys&lt;/a&gt; — key factories at scale&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://zustand.docs.pmnd.rs/reference/apis/create-store" rel="noopener noreferrer"&gt;Zustand: &lt;code&gt;createStore&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://zustand.docs.pmnd.rs/reference/hooks/use-store" rel="noopener noreferrer"&gt;&lt;code&gt;useStore&lt;/code&gt;&lt;/a&gt; — the vanilla store and its React subscription&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://redux-toolkit.js.org/api/combineSlices" rel="noopener noreferrer"&gt;Redux Toolkit: &lt;code&gt;combineSlices&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://redux-toolkit.js.org/rtk-query/usage/code-splitting" rel="noopener noreferrer"&gt;RTK Query code splitting&lt;/a&gt; — what the other branch relies on&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/warrendeleon/react-native-module-federation" rel="noopener noreferrer"&gt;react-native-module-federation&lt;/a&gt; — the companion repo, at the tag &lt;code&gt;post-09-tanstack-zustand&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>reactnative</category>
      <category>modulefederation</category>
      <category>repack</category>
      <category>rspack</category>
    </item>
    <item>
      <title>Client state across the seam: remotes that bring their own slices in React Native</title>
      <dc:creator>Warren de Leon</dc:creator>
      <pubDate>Mon, 17 Aug 2026 00:09:56 +0000</pubDate>
      <link>https://dev.to/warrendeleon/client-state-across-the-seam-remotes-that-bring-their-own-slices-in-react-native-l7b</link>
      <guid>https://dev.to/warrendeleon/client-state-across-the-seam-remotes-that-bring-their-own-slices-in-react-native-l7b</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📚 &lt;strong&gt;React Native Module Federation&lt;/strong&gt; series — &lt;a href="https://warrendeleon.com/blog/series/react-native-module-federation/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-client-state" rel="noopener noreferrer"&gt;read it in full on warrendeleon.com&lt;/a&gt;, where new parts land first.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://warrendeleon.com/blog/one-shared-store-across-remotes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-client-state" rel="noopener noreferrer"&gt;Post 6&lt;/a&gt; ended on an honest limitation: close the app and reopen it, and nothing you did survives. No party, no favourites, no memory. Everything crossing the seam so far has been server state: data PokéAPI owns, held in a cache every module shares. The app itself owns nothing yet.&lt;/p&gt;

&lt;p&gt;This post gives it something to own: the party of six. &lt;a href="https://warrendeleon.com/blog/server-state-and-client-state-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-client-state" rel="noopener noreferrer"&gt;Client state&lt;/a&gt; has no server behind it and no cache to refetch. It lives in a Redux slice, and under federation a slice raises the question this whole series circles: who owns it, and how do other features touch it without touching its owner? The &lt;a href="https://warrendeleon.com/blog/who-owns-what-federated-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-client-state" rel="noopener noreferrer"&gt;ownership essay&lt;/a&gt; answered in principle. This post answers in code.&lt;/p&gt;

&lt;p&gt;The shape we're building:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📊 &lt;strong&gt;Diagram:&lt;/strong&gt; &lt;a href="https://warrendeleon.com/blog/client-state-across-federated-remotes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-client-state" rel="noopener noreferrer"&gt;view it on warrendeleon.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Hold one idea in your head: everyone shares the cache, but a client-state slice has exactly one owner. The party app owns the party. Everything another module needs (one action, one read shape, one cap) crosses through the contract, and near the end we dispatch into the slice before its owner has loaded, on purpose, to watch what Redux does with an action nobody is listening for.&lt;/p&gt;

&lt;p&gt;Carry on from your own post 6 code if you built along. Otherwise, start from its finished state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/warrendeleon/react-native-module-federation
&lt;span class="nb"&gt;cd &lt;/span&gt;react-native-module-federation
git checkout post-06-shared-store
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Who owns client state?
&lt;/h2&gt;

&lt;p&gt;Two kinds of state now live in this app, each with a different owner, and between them sits the interaction that crosses.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;server cache&lt;/strong&gt; is everyone's. Post 6 built it: one &lt;code&gt;baseApi&lt;/code&gt; in the contract, one store in the host, endpoints injected by whoever owns the data. No app owns a cache entry. The data belongs to PokéAPI and the cache is only holding a copy, which is why any module may read it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Private state&lt;/strong&gt; has one owner and stays inside it. Which members the party holds, how removal works, what the cap means: all of that is the party app's business, in a slice nothing else imports.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;crossing interaction&lt;/strong&gt; is the narrow strip between them. The Pokédex needs to add a member to a party it does not own, and its header wants to show "My Party 3/6" for state it cannot see. Those crossings get typed, named, versioned and put in the contract, because the rule from the ownership essay holds here too: apps never depend on each other. Apps depend on contracts.&lt;/p&gt;

&lt;p&gt;That taxonomy decides everything else in this post. What follows is just each row of it, built.&lt;/p&gt;

&lt;h2&gt;
  
  
  The wall, and the reducer moves
&lt;/h2&gt;

&lt;p&gt;The party's slice has to join the running store, and Redux Toolkit (RTK) has an API for exactly this: &lt;code&gt;combineSlices&lt;/code&gt; builds a reducer with an &lt;code&gt;inject&lt;/code&gt; method, and a slice injected at runtime starts reducing from that moment on. So the party app needs to call &lt;code&gt;inject&lt;/code&gt; on the reducer object the host's store actually wired in.&lt;/p&gt;

&lt;p&gt;Post 6's host built that reducer inline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// apps/host/src/store.ts, in post 6&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;configureStore&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;reducer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;combineSlices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;baseApi&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;getDefaultMiddleware&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;getDefaultMiddleware&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;baseApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is nothing to reach. The reducer is an expression inside the host's source, never exported, and even an export would not help: apps don't import apps. A copy is worse than useless: &lt;code&gt;combineSlices(baseApi)&lt;/code&gt; in the party app builds a second reducer that no store runs, and injecting into it changes nothing on screen.&lt;/p&gt;

&lt;p&gt;This is the identity argument from post 6, third time around. &lt;code&gt;baseApi&lt;/code&gt; moved to the contract because a consumer can only inject endpoints into the &lt;em&gt;same&lt;/em&gt; instance the store wired. The root reducer moves for the same reason, one file over. &lt;code&gt;packages/contracts/src/store.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;combineSlices&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@reduxjs/toolkit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;baseApi&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rootReducer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;combineSlices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;baseApi&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the host's store slims to an import:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// apps/host/src/store.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;configureStore&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@reduxjs/toolkit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;baseApi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rootReducer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@pokedex/contracts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;configureStore&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;reducer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rootReducer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;getDefaultMiddleware&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;getDefaultMiddleware&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;baseApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The host still owns the store: the middleware, the &lt;code&gt;Provider&lt;/code&gt;, the wiring. The contract package now owns the reducer the same way it owns the api instance. Because contracts is a federation singleton, &lt;code&gt;rootReducer&lt;/code&gt; is one object in the whole runtime, and a slice injected by an app built months after the shell lands in the reducer the shell is already running.&lt;/p&gt;

&lt;h2&gt;
  
  
  The contract carries the crossing
&lt;/h2&gt;

&lt;p&gt;Now the taxonomy's middle row. The interactions that cross an app boundary go in a new &lt;code&gt;packages/contracts/src/party.ts&lt;/code&gt;, and it is short on purpose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createAction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;nanoid&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@reduxjs/toolkit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MAX_PARTY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;PartyMember&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;spriteUri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;addToParty&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;party/add&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;member&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Omit&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;PartyMember&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;uid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;member&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;nanoid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;partyStateReady&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;party/stateReady&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;PartySliceShape&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;party&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;members&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PartyMember&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;addToParty&lt;/code&gt; is the one action anything outside the party dispatches. The contract owns the action's shape; the party's reducer owns what it means. The &lt;code&gt;prepare&lt;/code&gt; callback stamps each member with a &lt;code&gt;nanoid()&lt;/code&gt; (shipped inside RTK, no new dependency), so the reducer stays pure and two copies of the same Pokémon stay distinguishable. Duplicates are allowed by design: a party of six Magikarp is a valid life choice, and the uid is what tells them apart when one gets removed.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;MAX_PARTY&lt;/code&gt; sits at the seam because every surface that reflects the rule has to read the same number: a disabled button in one app, a counter in another, the guard in the owner's reducer.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;partyStateReady&lt;/code&gt; is the odd one out: no reducer case handles it, and nothing about the party changes when it is dispatched. It exists for the boot sequence, and the section on loading state modules shows the one useful thing it does.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;PartySliceShape&lt;/code&gt; is the read side, and the optional marker is the design, not defensiveness. The slice is injected at runtime by a module the reader does not control, so at the moment a foreign module reads, &lt;code&gt;state.party&lt;/code&gt; may not exist yet. Readers write &lt;code&gt;s.party?.members ?? []&lt;/code&gt; and render something honest either way. Typed alternatives exist (RTK's &lt;code&gt;withLazyLoadedSlices&lt;/code&gt; can thread the possibly-absent slice through declaration merging), but the tolerant shape teaches the situation rather than hiding it, and the &lt;em&gt;Now break it&lt;/em&gt; sabotage depends on you understanding it.&lt;/p&gt;

&lt;p&gt;Notice what is absent. The party will also have a &lt;code&gt;remove&lt;/code&gt; action, and it appears in no contract, because nobody else dispatches it. The contract carries what crosses, nothing more; an entry nobody consumes is a liability with a version number.&lt;/p&gt;

&lt;p&gt;Both new files are additive, so the version is a minor; the tag carries &lt;code&gt;3.1.3&lt;/code&gt;, the minor plus the same hardening patches the 3.0.x line took. Publish:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;packages/contracts
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm publish
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;+ @pokedex/contracts@3.1.3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The host and the list both sit on &lt;code&gt;^3.0.0&lt;/code&gt;, and here the lockfile matters more than the caret. A plain &lt;code&gt;npm install&lt;/code&gt; changes nothing: the lockfile pins &lt;code&gt;3.0.3&lt;/code&gt;, and install honours the lockfile even though the caret would accept &lt;code&gt;3.1.3&lt;/code&gt;. Walking a caret forward is its own command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/host &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm update @pokedex/contracts &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;package.json&lt;/code&gt; edit, one lockfile line moved. The pin only gets touched when a consumer crosses a major. One consumer is about to.&lt;/p&gt;

&lt;h2&gt;
  
  
  The owner
&lt;/h2&gt;

&lt;p&gt;The party app has been the series' control group: contract on &lt;code&gt;^1.0.0&lt;/code&gt;, detail on &lt;code&gt;^1.0.0&lt;/code&gt;, no store access, a static grid of six empty slots. Growing a slice ends that on every front, and the adoption is a deliberate edit because two majors is a real distance. &lt;code&gt;apps/party/package.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"@pokedex/contracts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^3.1.0"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"@pokedex/detail"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^3.1.0"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"@reduxjs/toolkit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^2.12.0"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"react-redux"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^9.3.0"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;npm install&lt;/code&gt;, and the build breaks immediately, in the compiler rather than at runtime. Party's stack still mounts the detail screen the 1.0.0 way: &lt;code&gt;import PokemonDetailScreen from '@pokedex/detail'&lt;/code&gt;, straight into &lt;code&gt;component=&lt;/code&gt;. The 3.x package has no default export any more, so that import now resolves to the package's namespace object, and the mount stops type-checking:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="nx"&gt;TS2322&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Type&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;typeof import(".../@pokedex/detail/dist/index")&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nx"&gt;not&lt;/span&gt;
&lt;span class="nx"&gt;assignable&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ScreenComponentType&amp;lt;PartyParamList, "PokemonDetail"&amp;gt; | undefined&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The detail package has crossed two majors around this app while it wasn't looking, 1.x to 3.x. What 3.x exports is the named &lt;code&gt;PokemonDetailView&lt;/code&gt;, a view that demands data as props, so the compile error forces party to write the same four-line container the list app wrote in post 6. A container needs data, and that becomes a wall of its own in &lt;em&gt;The party needs data of its own&lt;/em&gt;. Chains like this are what version lag actually costs: not a crash in production, a stack of walls on the day you finally adopt.&lt;/p&gt;

&lt;p&gt;The slice itself is the whole point of the post, and it fits on a screen. &lt;code&gt;apps/party/src/partySlice.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createSlice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;PayloadAction&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@reduxjs/toolkit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;addToParty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;MAX_PARTY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rootReducer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;PartyMember&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@pokedex/contracts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;partySlice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createSlice&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;party&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;initialState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;members&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;PartyMember&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;reducers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Private: nobody else dispatches remove, so it ships in no contract.&lt;/span&gt;
    &lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PayloadAction&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;members&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;members&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uid&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;extraReducers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;builder&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addCase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;addToParty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;members&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;MAX_PARTY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// the cap lives with the owner&lt;/span&gt;
      &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;members&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;remove&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;partySlice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Importing this module is what adds the reducer to the shared store.&lt;/span&gt;
&lt;span class="nx"&gt;rootReducer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;partySlice&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things carry the design. The crossing action binds through &lt;code&gt;extraReducers&lt;/code&gt;: the party matches the &lt;em&gt;contract's&lt;/em&gt; &lt;code&gt;addToParty&lt;/code&gt;. Be precise about why that works across separately built apps, because it is easy to credit the wrong thing. &lt;code&gt;builder.addCase&lt;/code&gt; reads &lt;code&gt;actionCreator.type&lt;/code&gt; and keys its reducer by that string, so what has to agree is &lt;code&gt;party/add&lt;/code&gt;, not the identity of the creator object. Two copies of the contract would still match. Defining the string, the cap and the read shape once instead of retyping them per side is the &lt;em&gt;versioned package's&lt;/em&gt; work: publishing the contract is what prevents that drift, singleton or not. Where runtime singleton identity earns its place is the objects, &lt;code&gt;rootReducer&lt;/code&gt; and &lt;code&gt;baseApi&lt;/code&gt;: an injection only lands in the store the host wired if both sides hold the same instance.&lt;/p&gt;

&lt;p&gt;And the cap guard lives here, in the owner. The contract publishes the number; only the owner enforces it. A dispatcher that forgets to disable its button still cannot push a seventh member.&lt;/p&gt;

&lt;p&gt;The last line is the mechanism. &lt;code&gt;inject&lt;/code&gt; takes the slice itself (&lt;code&gt;createSlice&lt;/code&gt; defaults its &lt;code&gt;reducerPath&lt;/code&gt; to &lt;code&gt;name&lt;/code&gt;, so the state mounts at &lt;code&gt;state.party&lt;/code&gt;, matching &lt;code&gt;PartySliceShape&lt;/code&gt;), and importing the module is what performs the injection. That makes the slice a &lt;em&gt;state module&lt;/em&gt;: a federated module whose value is its side effect. The party's bundler config exposes it alongside the stack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;exposes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./PartyStack&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./src/PartyStack.tsx&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./partySlice&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./src/partySlice.ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The party also joins the state trio in its &lt;code&gt;shared&lt;/code&gt; map: &lt;code&gt;@reduxjs/toolkit&lt;/code&gt; and &lt;code&gt;react-redux&lt;/code&gt; with the hand-stated &lt;code&gt;version&lt;/code&gt; every exports-map package needs in this config, &lt;code&gt;@pokedex/contracts&lt;/code&gt; as a singleton, none of them eager, exactly as the list app declares them. Same rule as post 6: the host provides the copies, remotes consume them.&lt;/p&gt;

&lt;p&gt;One dev-loop observation, since RTK guards against a hazard here: on paper, re-running this module hot-swaps a new function identity into an existing &lt;code&gt;reducerPath&lt;/code&gt;, and RTK's &lt;code&gt;inject&lt;/code&gt; refuses to replace it (a console error in development; the original reducer stays live). In this Re.Pack setup the hazard stays theoretical: an edit to the slice file reloads the app rather than hot-swapping the module, so you get a fresh store instead of a double inject. Worth knowing which of the two your stack does before you trust either.&lt;/p&gt;

&lt;p&gt;With state to render, the placeholder tab becomes a screen. The interesting lines of &lt;code&gt;PartyScreen.tsx&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useDispatch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useSelector&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-redux&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;MAX_PARTY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;PartySliceShape&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@pokedex/contracts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;remove&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./partySlice&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;members&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSelector&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PartySliceShape&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;party&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;members&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Filled slots render the sprite, the name, and a remove control dispatching &lt;code&gt;remove(member.uid)&lt;/code&gt;; empty slots keep the dashed placeholder up to six; the header counts &lt;code&gt;{members.length}/{MAX_PARTY}&lt;/code&gt;; tapping a member pushes &lt;code&gt;PokemonDetail&lt;/code&gt; with &lt;code&gt;{ id }&lt;/code&gt;, the same &lt;code&gt;DetailParams&lt;/code&gt; from post 5, no new fields. Note the owner reading its &lt;em&gt;own&lt;/em&gt; state through the tolerant shape. Even here the optional earns its keep: the first render can beat the first action into the store, and injection registers the reducer but leaves &lt;code&gt;state.party&lt;/code&gt; undefined until an action next reaches it.&lt;/p&gt;

&lt;h2&gt;
  
  
  State modules load at boot
&lt;/h2&gt;

&lt;p&gt;The slice injects when its module is imported. So far only one thing imports it: the party screen, which also imports &lt;code&gt;remove&lt;/code&gt;. That means the slice exists only after the user opens the Party tab, and the Pokédex is about to dispatch into it long before that. Someone has to load the state module early, and only one participant is alive at boot on every path: the shell. &lt;code&gt;apps/host/App.tsx&lt;/code&gt;, in an effect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Screens load on demand; state modules load at boot.&lt;/span&gt;
&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;partyApp/partySlice&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;partyStateReady&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;party state module failed to load&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The host already declares &lt;code&gt;partyApp&lt;/code&gt; in its remotes map, so this adds no coupling it didn't have. It fires the import and holds no reference to the module's contents: the ambient declaration types the module as empty, because the host imports it for the side effect and has no business naming anything inside. Screens stay lazy, because a screen the user has not opened costs nothing to defer.&lt;/p&gt;

&lt;p&gt;Loading at boot is a head start, not a guarantee. The chunk crosses the network, and nothing stops a fast finger reaching an Add button before it lands. A &lt;code&gt;party/add&lt;/code&gt; dispatched into a store with no reducer for it vanishes without a trace: no warning, no error, Redux ignoring an unmatched action by design. So the resolve is made visible, and this is where &lt;code&gt;partyStateReady&lt;/code&gt; earns its keep. &lt;code&gt;inject()&lt;/code&gt; swaps an entry in a reducer map and rebuilds the combined reducer, but it never dispatches, so &lt;code&gt;state.party&lt;/code&gt; stays undefined until the &lt;em&gt;next&lt;/em&gt; action runs through the new reducer. The marker is that action: the &lt;code&gt;.then&lt;/code&gt; dispatches it the moment the module resolves, &lt;code&gt;state.party&lt;/code&gt; appears, and every subscriber re-renders with the slice in place.&lt;/p&gt;

&lt;p&gt;The write side closes the loop by gating on exactly that: the container in &lt;em&gt;The write arrives as a prop&lt;/em&gt; disables Add while &lt;code&gt;s.party&lt;/code&gt; is undefined. A tap that cannot happen is a dispatch that cannot be lost.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;The effect placement is an observation, not a diagnosis.&lt;/strong&gt; At module scope this import produced &lt;em&gt;Can't perform a React state update on a component that hasn't mounted yet&lt;/em&gt; on some cold starts and not others, and moving it into an effect removed the warning across repeated cold starts. What produces the update is not something this post has pinned down: &lt;code&gt;rootReducer.inject()&lt;/code&gt; is not the culprit, because it swaps an entry in a reducer map and rebuilds the combined reducer without ever dispatching or calling &lt;code&gt;replaceReducer&lt;/code&gt;, so it notifies nobody by itself. Until the update is traced to its source, treat the placement as a local observation: the effect is the arrangement that made the warning stop, and an effect is where React expects a side effect to live anyway.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Nothing awaits that import, so a party server that is down cannot block boot. That run is worth doing rather than trusting. Stop the party dev server and cold-start the app: the federation runtime reports the failed manifest fetch loudly in development (&lt;code&gt;[ Federation Runtime ]: Failed to get manifest. #RUNTIME-003&lt;/code&gt;), and the shell carries on. The Pokédex renders, the counter reads an honest 0/6 through the tolerant shape, the Add button stays disabled because &lt;code&gt;partyStateReady&lt;/code&gt; never fired, and the app runs without the slice, which is precisely the state &lt;code&gt;PartySliceShape&lt;/code&gt; was designed to describe.&lt;/p&gt;

&lt;p&gt;One detail from the observed run: the import settles without rejecting, so the &lt;code&gt;.catch&lt;/code&gt; never actually fires for this failure; the runtime contains it and reports it on its own. The catch stays, one line guarding the rejection path so a failed load can never surface as an unhandled rejection.&lt;/p&gt;

&lt;h2&gt;
  
  
  The write arrives as a prop
&lt;/h2&gt;

&lt;p&gt;The Pokédex side of the crossing starts in the detail view, and the detail view is an installed component library: the one place the write must &lt;em&gt;not&lt;/em&gt; live. The ownership essay drew this line: a shared component renders what it is given; a write that crosses a domain boundary is wired by the consumer. So &lt;code&gt;@pokedex/detail&lt;/code&gt; 3.1.0 is an additive minor with three optional props:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;PokemonDetailViewProps&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;pokemon&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;PokemonDetail&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;onRetry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;onAddToParty&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;addDisabled&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;addLabel&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The view renders a button when a consumer hands it &lt;code&gt;onAddToParty&lt;/code&gt; and renders nothing when it does not. It still imports no store, no contract, no action creator. The write crosses a domain boundary, so it arrives as a callback and leaves as a tap.&lt;/p&gt;

&lt;p&gt;The list app's container wires all three:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;PokemonDetailRoute&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;route&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;route&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DetailParams&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;refetch&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useGetPokemonDetailQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dispatch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useDispatch&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;members&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSelector&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PartySliceShape&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;party&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;members&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;partyReady&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;members&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;members&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;full&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;MAX_PARTY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PokemonDetailView&lt;/span&gt;
      &lt;span class="na"&gt;pokemon&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isLoading&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isError&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;onRetry&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;refetch&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;onAddToParty&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;addToParty&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;spriteUri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;spriteUri&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;
      &lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;addDisabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;full&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;partyReady&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;addLabel&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;full&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Party is full&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Add to party&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read what that tap does. A screen owned by the Pokédex team dispatches an action the contract owns, and that action lands in a reducer owned by the party team. Three parties, none importing another's code. The disabled state reads the same &lt;code&gt;MAX_PARTY&lt;/code&gt; the reducer guards with, so the button and the cap can't drift. It also holds the button down until &lt;code&gt;state.party&lt;/code&gt; exists, so the one dispatch that could be lost is the one that can never be made. And the Pokédex header gets its counter through the identical read:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;partyCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSelector&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PartySliceShape&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;party&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;members&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;partyCount&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;My Party &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;partyCount&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;/&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;MAX_PARTY&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The list app edits no version pin for any of this: it was on &lt;code&gt;^3.0.0&lt;/code&gt; for both packages, and &lt;code&gt;npm update @pokedex/contracts @pokedex/detail&lt;/code&gt; walks both carets to the new minors. Only the lagging app touched its &lt;code&gt;package.json&lt;/code&gt;. That is the caret working as intended: a minor arrives on request; a major takes a deliberate edit.&lt;/p&gt;

&lt;p&gt;The party's container is the other consumer of the same view, and it wires none of the three props. A Pokémon opened from inside the party shows no Add button at all:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm88fve6ant0m56nwc3kq.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm88fve6ant0m56nwc3kq.webp" alt="The detail screen opened from the Party tab: the same shared view with no Add button, because the party's container wires no write"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One view, two consumers, one of them wiring a write. That single screenshot is the ownership essay's component rule, running.&lt;/p&gt;

&lt;h2&gt;
  
  
  The party needs data of its own
&lt;/h2&gt;

&lt;p&gt;That party-side container is the wall promised earlier. Tapping a party member pushes &lt;code&gt;PokemonDetail&lt;/code&gt; inside the party's stack, and the container behind it needs &lt;code&gt;getPokemonDetail&lt;/code&gt;, which lives in the list app, which the party cannot import. Apps depend on contracts, never on each other, and this is the first time the rule costs something visible.&lt;/p&gt;

&lt;p&gt;The tempting exit is a shared data package: &lt;code&gt;@pokedex/data&lt;/code&gt;, owning the endpoint both apps need. It would delete the duplication, and for two features owned by one team it might even be right. The price is the one the &lt;a href="https://warrendeleon.com/blog/who-owns-what-federated-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-client-state" rel="noopener noreferrer"&gt;ownership essay&lt;/a&gt; priced: a shared package puts the party's data access on another team's release train. Compatible releases don't force anyone to move, but the day the party needs a fix in that package, or a breaking version lands, the party's ship date waits on another team's review and release. Twenty lines of query definition do not buy that coupling. The party writes its own &lt;code&gt;apps/party/src/detailApi.ts&lt;/code&gt; with the same endpoint name, the same &lt;code&gt;queryFn&lt;/code&gt; and the same &lt;code&gt;parsePokemonDetail&lt;/code&gt; from the contract, copied deliberately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;detailApi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;baseApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;injectEndpoints&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;endpoints&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;build&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;getPokemonDetail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;build&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;PokemonDetail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="c1"&gt;// byte for byte, the list app's definition&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useGetPokemonDetailQuery&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;detailApi&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two apps now inject an endpoint named &lt;code&gt;getPokemonDetail&lt;/code&gt; into one &lt;code&gt;baseApi&lt;/code&gt;, and RTK notices. Run the app, open the Party tab, and the development console prints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;called `injectEndpoints` to override already-existing endpointName getPokemonDetail without specifying `overrideExisting: true`
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Left alone, RTK skips the second injection and keeps the first: loudly in development, silently in production, where that guard compiles out. This build declares the duplicate instead: both apps pass &lt;code&gt;overrideExisting: true&lt;/code&gt;, the console error goes away, and the &lt;em&gt;last&lt;/em&gt; injection wins. Either way load order decides, and neither app controls load order. That is the honest reason the two definitions have to stay identical rather than merely similar.&lt;/p&gt;

&lt;p&gt;The cache does not change: one endpoint name means one set of entries, so opening Bulbasaur from the Pokédex and then from the party costs one fetch total. The trap is drift: declared or not, whichever copy load order favours silently wins for both, and once &lt;code&gt;overrideExisting&lt;/code&gt; has silenced the console there is no warning left to ignore. The rule, then: keep the copies byte for byte identical, declare the collision so it is visibly deliberate, or name your endpoint something of your own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Now break it
&lt;/h2&gt;

&lt;p&gt;The claim to attack: the gate on &lt;code&gt;state.party&lt;/code&gt; is what stands between a slow chunk and a lost tap. First remove the gate (change the container's &lt;code&gt;addDisabled&lt;/code&gt; back to plain &lt;code&gt;full&lt;/code&gt;), then comment out the boot import:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// useEffect(() =&amp;gt; {&lt;/span&gt;
&lt;span class="c1"&gt;//   import('partyApp/partySlice')&lt;/span&gt;
&lt;span class="c1"&gt;//     .then(() =&amp;gt; store.dispatch(partyStateReady()))&lt;/span&gt;
&lt;span class="c1"&gt;//     .catch(err =&amp;gt; console.warn('party state module failed to load', err));&lt;/span&gt;
&lt;span class="c1"&gt;// }, []);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Relaunch fresh, and do not open the Party tab. That matters: &lt;code&gt;PartyScreen&lt;/code&gt; imports &lt;code&gt;remove&lt;/code&gt; from the slice file, so visiting the tab injects the slice as a side effect and hides the bug. A user who goes straight to the Pokédex is how you reproduce it.&lt;/p&gt;

&lt;p&gt;Tap Bulbasaur. Tap &lt;strong&gt;Add to party&lt;/strong&gt;. The tap lands, the counter reads 0/6, and nothing else happens. No warning, no error, no console line. Post 6's break-it at least hung a spinner you could stare at. Here the dispatch reached the store, the store found no reducer registered for &lt;code&gt;party/add&lt;/code&gt;, and Redux did what Redux does with an unmatched action: nothing, by design. The user's tap reached a store with nothing registered to answer it, and Redux is behaving exactly as documented.&lt;/p&gt;

&lt;p&gt;Now say the sharper thing out loud. With the boot import gone, the slice still exists &lt;em&gt;eventually&lt;/em&gt;: one visit to the Party tab injects it, and every add after that works. So the failure has a narrower and nastier shape: every add made before the user happens to open the owner's tab is lost in silence, which reproduces for some users and never for others, depending on tab order. And the boot import alone only shrinks the window, from "until the user opens the Party tab" down to "until the chunk lands". It cannot close it, because the network owes you nothing.&lt;/p&gt;

&lt;p&gt;Closing it is the gate's job: put the gate back, leave the import commented out, and the same cold start shows an Add button that is simply disabled. Ugly, visible, and honest: a tap that cannot happen instead of a tap that lies.&lt;/p&gt;

&lt;p&gt;Restore the import, relaunch, and the button wakes the moment &lt;code&gt;partyStateReady&lt;/code&gt; fires; the same tap ticks the counter to 1/6. The pattern is all three pieces: the boot import for the head start, the marker to surface the slice, and the gate for the window neither can close.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Land exactly on this post's finished state.&lt;/strong&gt; The walkthrough prints the load-bearing files; manifests, configs, tests and the smaller edits live in the tag. To finish with a tree byte-equal to the tag, sweep the reference copy over yours. A file you typed correctly is overwritten with itself, and the sweep fills in what the prose did not print:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx degit@3.8.0 &lt;span class="nt"&gt;--force&lt;/span&gt; warrendeleon/react-native-module-federation#post-08-client-state /tmp/pokedex-ref-08
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; /tmp/pokedex-ref-08/. &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The sweep is also how the behaviour gets its proof: it carries the two regression tests this post's design earned (the party store walked through the vanish-inject-marker-add window, and the list container's Add gate held down until readiness), the mocks and Jest wiring they run on, and the ambient federation declarations the walkthrough summarised rather than printed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run it
&lt;/h2&gt;

&lt;p&gt;Packages published and installed, so the run is three dev servers and a simulator build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;apps/list &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run start:remote      &lt;span class="c"&gt;# :8082&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;apps/party &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run start:remote     &lt;span class="c"&gt;# :8083&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;apps/host &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm start                 &lt;span class="c"&gt;# :8081&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;apps/host &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run ios
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cold start lands on the Pokédex with the counter reading 0/6. Do not read that as proof the boot import worked: the tolerant selector returns 0/6 whether the slice is registered or still missing, which is the whole point of it. The proof is the Add button being enabled at all, since it only wakes once &lt;code&gt;partyStateReady&lt;/code&gt; has surfaced the slice. Add from a detail, watch the counter tick, and find the member sitting in the Party tab:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🎞️ &lt;strong&gt;Animated demo:&lt;/strong&gt; &lt;a href="https://warrendeleon.com/blog/client-state-across-federated-remotes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-client-state#run-it" rel="noopener noreferrer"&gt;watch it on warrendeleon.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Keep adding to six and the cap arrives from both sides at once: the sixth add flips the open detail's button to a disabled state live, because the same selector that counts for the header counts for &lt;code&gt;addDisabled&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi8pwg1s3939f7h1wt22f.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi8pwg1s3939f7h1wt22f.webp" alt="The detail screen at the party cap: the Add button disabled and relabelled Party is full"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Remove a member in the Party tab and the freed slot goes dashed again, the counter drops everywhere at once, and the next add works. One slice, one owner, three surfaces agreeing because they all read the same state through the same shape.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you built, and what's next
&lt;/h2&gt;

&lt;p&gt;The app now owns something. The party lives in a slice the party app alone owns, registered at boot into a store the host wired around the contract's &lt;code&gt;rootReducer&lt;/code&gt;. Registered, not populated: &lt;code&gt;state.party&lt;/code&gt; stays absent until the next action reaches the combined reducer. The &lt;code&gt;partyStateReady&lt;/code&gt; marker is that action, and the tolerant read shape covers every moment before it fires. The one interaction that crosses (&lt;code&gt;addToParty&lt;/code&gt;, its cap, its read shape) ships versioned in the contract, the write reaches the shared detail view as a prop its consumer wires, and the party fetches its own data rather than borrow another team's release schedule. You have also watched the failure this design exists to prevent: a dispatch into a slice whose owner never loaded, dropped without a sound.&lt;/p&gt;

&lt;p&gt;One honest limitation stands: restart the app and the party is gone. The slice lives in memory, persistence is a different post's problem, and pretending otherwise would be the kind of quiet scope creep this series tries to avoid.&lt;/p&gt;

&lt;p&gt;The sharper question is the one this stack keeps raising. The party is six items and one rule, and it took a store, a contract minor, an injected reducer and a boot import to cross the seam politely. RTK made that crossing possible; it did not make it small. &lt;a href="https://warrendeleon.com/blog/state-stacks-under-federation-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-client-state" rel="noopener noreferrer"&gt;Next, the series rebuilds this exact app on TanStack Query and Zustand&lt;/a&gt;, the stack most React Native teams reach for first, and watches what federation does to that choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://redux-toolkit.js.org/api/combineSlices" rel="noopener noreferrer"&gt;Redux Toolkit: &lt;code&gt;combineSlices&lt;/code&gt;&lt;/a&gt; — the injectable root reducer and &lt;code&gt;inject&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://redux-toolkit.js.org/api/createAction" rel="noopener noreferrer"&gt;Redux Toolkit: &lt;code&gt;createAction&lt;/code&gt;&lt;/a&gt; — prepare callbacks, and &lt;code&gt;nanoid&lt;/code&gt; shipping with RTK&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://redux-toolkit.js.org/rtk-query/usage/code-splitting" rel="noopener noreferrer"&gt;RTK Query: code splitting&lt;/a&gt; — &lt;code&gt;injectEndpoints&lt;/code&gt; and the &lt;code&gt;overrideExisting&lt;/code&gt; guard&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://pokeapi.co/" rel="noopener noreferrer"&gt;PokéAPI&lt;/a&gt; — the free REST API the app fetches from&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/warrendeleon/react-native-module-federation" rel="noopener noreferrer"&gt;react-native-module-federation&lt;/a&gt; — the companion repo, at the tag &lt;code&gt;post-08-client-state&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>reactnative</category>
      <category>modulefederation</category>
      <category>repack</category>
      <category>rspack</category>
    </item>
    <item>
      <title>Who owns what: boundaries, teams and coupling in a federated React Native app</title>
      <dc:creator>Warren de Leon</dc:creator>
      <pubDate>Mon, 10 Aug 2026 00:09:24 +0000</pubDate>
      <link>https://dev.to/warrendeleon/who-owns-what-boundaries-teams-and-coupling-in-a-federated-react-native-app-5f30</link>
      <guid>https://dev.to/warrendeleon/who-owns-what-boundaries-teams-and-coupling-in-a-federated-react-native-app-5f30</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📚 &lt;strong&gt;React Native Module Federation&lt;/strong&gt; series — &lt;a href="https://warrendeleon.com/blog/series/react-native-module-federation/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-ownership" rel="noopener noreferrer"&gt;read it in full on warrendeleon.com&lt;/a&gt;, where new parts land first.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A plain React Native app usually avoids this argument, because the common shape is one team, one repo, one store. Nothing about React Native requires that; a monorepo with several teams and workspace packages has the same ownership questions without any federation involved. A screen is a folder, a shared component is an import, and the question of who owns a data definition has an answer too obvious to say out loud: the team does, because there is only one.&lt;/p&gt;

&lt;p&gt;Posts 4 to 6 of this series took that arrangement apart on purpose: &lt;a href="https://warrendeleon.com/blog/host-shell-federated-tabs-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-ownership" rel="noopener noreferrer"&gt;two tab features that build and ship on their own&lt;/a&gt;, a screen that travels between them as a versioned package, one store filled at runtime by code the shell has never seen. Every step forced an ownership call: where the shared screen lives, who may touch the store, where a data definition sits. Each call got a paragraph at the moment the tutorial made it. This post gathers those calls and argues them once, as a whole.&lt;/p&gt;

&lt;p&gt;It is written as a set of tests, not a set of verdicts. Each section states the reasoning and the conditions under which it holds; change the conditions (team count, trust between teams, release cadence, the size of the platform team) and some of the answers flip honestly. Take the tests away, hold them against your own org chart, and disagree with this series precisely.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first test: do you need any of this?
&lt;/h2&gt;

&lt;p&gt;Every problem in this essay is bought, not inherited. A single-team app organised &lt;a href="https://warrendeleon.com/blog/feature-first-project-structure-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-ownership" rel="noopener noreferrer"&gt;feature-first&lt;/a&gt; already draws the boundaries that matter: a folder per domain, with the domain's screens, hooks and API calls inside it, and every crossing visible in review as an import. That model costs nothing to run: no registry, no version numbers, no second dev server. A boundary can be redrawn in an afternoon with a file move, and an emergency can cross one with an import and a TODO.&lt;/p&gt;

&lt;p&gt;Federation is what happens when the folders become teams. The folder model's weakness is the release train, not the boundaries: every folder still ships in one binary, on one date, gated by the slowest feature on it. When separate teams need separate release dates, the folder boundary has to become a deployment boundary, and every question in this essay follows from that promotion.&lt;/p&gt;

&lt;p&gt;So the first test is the cheapest one, and it gates the rest.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;The gate.&lt;/strong&gt; Does more than one team ship this app? If not, stop at folders. Every test in this essay assumes the answer is yes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The &lt;a href="https://warrendeleon.com/blog/why-module-federation-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-ownership" rel="noopener noreferrer"&gt;first post of this series&lt;/a&gt; weighs the trade in full and lands where this paragraph does: federation converts organisational friction into technical machinery, and an app without the friction pays for machinery it never uses.&lt;/p&gt;

&lt;h2&gt;
  
  
  A boundary is a team, not a screen
&lt;/h2&gt;

&lt;p&gt;When the Pokémon detail screen needed a home both tabs could reach, the federated answer suggested itself: a third remote, on its own port, declared in both consumers' maps. &lt;a href="https://warrendeleon.com/blog/typing-the-seam-between-remotes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-ownership" rel="noopener noreferrer"&gt;Post 5&lt;/a&gt; gave that option a fair hearing: it works mechanically, Module Federation resolves nested remotes without complaint, and the screen stays updatable without touching either consumer. The answer was still no, in the sentence this whole essay grows from: a federation boundary is a deployable, and teams cut deployables along domains.&lt;/p&gt;

&lt;p&gt;The refusal is about running costs. A remote carries costs a folder never does: a dev server to run, a pipeline to keep green, a version to publish, and a failure mode of its own when its bundle fails to arrive over the network. Those costs are fixed per boundary, and a team absorbs them as overhead on a domain it was going to own anyway. A screen has no team behind it. Cut a screen loose as its own deployable and the overhead turns up while the owner never does; grant that status once and there is no principled place to refuse the next screen. The end of that road is &lt;strong&gt;an app per screen&lt;/strong&gt;, and a release process that is all coordination and no shipping.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📊 &lt;strong&gt;Diagram:&lt;/strong&gt; &lt;a href="https://warrendeleon.com/blog/who-owns-what-federated-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-ownership#a-boundary-is-a-team-not-a-screen" rel="noopener noreferrer"&gt;view it on warrendeleon.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So the screen shipped as &lt;code&gt;@pokedex/detail&lt;/code&gt; instead: a package on a registry, installed by both tabs, mounted by each inside its own stack. The tabs stay the deployables; the screen is a dependency. This is &lt;a href="https://www.melconway.com/Home/Committees_Paper.html" rel="noopener noreferrer"&gt;Conway's law&lt;/a&gt; used deliberately: if the system is going to mirror the communication structure anyway, draw the module lines where the team lines already are.&lt;/p&gt;

&lt;p&gt;&lt;span&gt;Test 1&lt;/span&gt;&lt;span&gt;Does this boundary have a team behind it?&lt;/span&gt;&lt;br&gt;
  &lt;/p&gt;
&lt;p&gt;The test cuts in both directions: it refuses remotes to screens, and it grants them to domains that have outgrown their folder.&lt;/p&gt;

&lt;h2&gt;
  
  
  A shared component renders what it is given
&lt;/h2&gt;

&lt;p&gt;Web micro-frontends have &lt;a href="https://martinfowler.com/articles/micro-frontends.html" rel="noopener noreferrer"&gt;a respectable pattern&lt;/a&gt; in which a shared fragment arrives smart: it fetches its own data, owns its own loading states, and drops into any host as a finished feature. The appeal is real. One team keeps pixels and data behind one seam, consumers write a single line, and a data fix rolls out with no consumer changing code. Plenty of web micro-frontend setups run this way coherently, at web deployment cadence.&lt;/p&gt;

&lt;p&gt;The price is in what the fragment has to carry. A component that fetches carries an opinion about every layer beneath it: an HTTP client, a cache, a retry policy, an auth story. Every consumer inherits those opinions without seeing them. Two such fragments on one screen can disagree about all of it. On mobile the inheritance weighs more, because whatever the fragment carries is duplicated into a bundle a phone has to download.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://warrendeleon.com/blog/one-shared-store-across-remotes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-ownership" rel="noopener noreferrer"&gt;Post 6&lt;/a&gt; drew the line the other way. &lt;code&gt;PokemonDetailView&lt;/code&gt; takes a Pokémon, a loading flag, an error flag and a retry callback, and renders them. That is the whole surface, and the whole arrangement fits in the consumer's container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;refetch&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useGetPokemonDetailQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PokemonDetailView&lt;/span&gt; &lt;span class="na"&gt;pokemon&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isLoading&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isError&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onRetry&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;refetch&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Going live added no dependencies to the package at all: the same four peers as the static version (the contract, React, React Native, the safe-area context) and Redux nowhere among them, because where the data comes from is the app's business and what it looks like is the library's.&lt;/p&gt;

&lt;p&gt;&lt;span&gt;Test 2&lt;/span&gt;&lt;span&gt;Does this component know where its data comes from?&lt;/span&gt;&lt;br&gt;
  &lt;/p&gt;
&lt;p&gt;A yes is worth stopping on. It can be the right call, for a widget a platform team owns end to end and every consumer wants identical. It is misdrawn when the fetching is incidental to what the component is for, because then what is being shared is a piece of one team's app wearing a component's name.&lt;/p&gt;

&lt;p&gt;The practical check takes a minute: render it in a bare app with hardcoded props. Every provider, client or store it demands before it renders is a piece of one app's runtime the package now asks every consumer to rebuild. A theme context is a reasonable ask; a query client wired to one team's endpoints is an app wearing a component's name.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data definitions live with their domain
&lt;/h2&gt;

&lt;p&gt;That last release split the screen in two, the view in a package and the fetch in an app, and the split had to put the endpoint definition somewhere. The somewhere is the rule. &lt;code&gt;getPokemonDetail&lt;/code&gt; landed in the list app, next to &lt;code&gt;getPokemonList&lt;/code&gt;, because Pokémon data belongs to the Pokédex domain and the list app is where that domain lives. The view is fed the result as props and never learns the endpoint exists.&lt;/p&gt;

&lt;p&gt;The counter-instinct says data code is infrastructure and belongs with the rest of the infrastructure, down in the shared layer. The series splits that claim in two. &lt;code&gt;baseApi&lt;/code&gt;, the HTTP layer with its cache and tag machinery, genuinely is infrastructure: it carries no &lt;strong&gt;domain knowledge&lt;/strong&gt;, and every team already depends on it the way they depend on React, so it lives in the contract package and everyone couples to it. A feature endpoint is different. It encodes what one domain fetches, when, and what it exposes, and whoever owns that definition ends up owning the questions, the bugs and the migrations that come with it. Move a domain's endpoints into a shared layer and the layer's owners slowly become the owners of every domain's data plumbing, without the domain knowledge that would make the job possible.&lt;/p&gt;

&lt;p&gt;&lt;span&gt;Test 3&lt;/span&gt;&lt;span&gt;Does this definition live with its domain?&lt;/span&gt;&lt;br&gt;
  &lt;/p&gt;
&lt;p&gt;An endpoint (what a domain fetches and when) travels with the domain. Machinery with no domain knowledge is shared. Models and parsers sit in between, and the split follows the backend: the schema for a wire format every consumer already depends on lives in the contract package (Test 4's logic), while a schema only one domain reads stays with that domain, which is why this series parses PokéAPI's REST payloads in the contract and post 10 keeps its GraphQL parse inside the list app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Duplication is cheaper than a dependency between teams
&lt;/h2&gt;

&lt;p&gt;In the next post the party remote starts holding state of its own, and its first need is Pokémon data for a tapped slot: data the Pokédex domain already knows how to fetch. The tidy instinct proposes a shared data package: one definition, two consumers, no drift. The series does the untidy thing instead and has the party team write its own endpoint file. It comes to about twenty lines and looks like a failure of housekeeping. The file it duplicates, the Pokédex's own, trimmed to its shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;detailApi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;baseApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;injectEndpoints&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;endpoints&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;build&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;getPokemonDetail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;build&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;PokemonDetail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;queryFn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;_api&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;_extra&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;baseQuery&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;baseQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`pokemon/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;parsePokemonDetail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Count what the tidy version costs. A data package shared between two feature teams is &lt;strong&gt;a dependency between peers&lt;/strong&gt;. A compatible release lands quietly, because consumers sit on a caret and adopt when they next install. But the changes worth making to a shared definition are not all compatible, and the day one lands breaking, every consumer has to be moved before the old version can go. Releases chain: bump the package, wait for the other team to take the bump, then ship the thing you actually wanted to ship. And the package itself needs an owner: one of the two teams answers for its bugs, or nobody does. A shared definition between peers does not remove the coordination; it converts drift you can see into scheduling you have to do.&lt;/p&gt;

&lt;p&gt;Post 5's loose peer range let one team's app pair an old contract with a new screen, every compiler stayed green, and a user got a sentence with a hole in it:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8up7q9gcd4ooiaqmsje1.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8up7q9gcd4ooiaqmsje1.webp" alt="The Pokémon detail screen reading 'Opened from the' with nothing after it, because the list app holds an older contract version and never passed the field the newer screen renders" width="600" height="1305"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Twenty duplicated lines cost twenty lines. Each copy follows its own team's needs and ships on its own team's dates, and the drift between them stays visible, because both parse the same wire format from the same backend: the copies can diverge in what they keep and how they shape it, but a divergence shows up against the shared payload rather than hiding behind two different ones. When the duplication grows past a file (five endpoints, ten), the answer changes, and &lt;em&gt;The coupling test&lt;/em&gt; says how to tell.&lt;/p&gt;

&lt;h2&gt;
  
  
  The coupling test
&lt;/h2&gt;

&lt;p&gt;The duplication rule needs a limit, because teams couple to shared code all the time and are right to: React, the navigation runtime, &lt;code&gt;baseApi&lt;/code&gt;, the contract types. The line between those and the peer data package is the most reusable test in this essay.&lt;/p&gt;

&lt;p&gt;&lt;span&gt;Test 4&lt;/span&gt;&lt;span&gt;Does this dependency already exist, or does sharing invent it?&lt;/span&gt;&lt;br&gt;
  &lt;/p&gt;
&lt;p&gt;Couple to what you already depend on; refuse the dependency that sharing itself creates.&lt;/p&gt;

&lt;p&gt;A client generated from the backend's API specification is fine to share, because it couples its consumers to the backend, a dependency every one of them &lt;em&gt;already&lt;/em&gt; has. When the specification changes incompatibly, every consumer was affected the moment it changed, and the generated client surfaces the fact at build time instead of in production; an additive change waits politely for each consumer to care. The contract package passes the same test: params and module shapes are agreements the apps were already bound by implicitly, written down where every compiler can see them. The peer data package fails it. Nothing about the party domain depended on the Pokédex team's release schedule before the shared package existed; the package is what would create that dependency.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📊 &lt;strong&gt;Diagram:&lt;/strong&gt; &lt;a href="https://warrendeleon.com/blog/who-owns-what-federated-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-ownership#the-coupling-test" rel="noopener noreferrer"&gt;view it on warrendeleon.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The pull of the shell
&lt;/h2&gt;

&lt;p&gt;Every rule so far pushes code outward, into domains and packages. One argument pushes the other way, and it deserves the longest hearing in the essay, because it is usually made by the most careful engineer in the room.&lt;/p&gt;

&lt;p&gt;Endpoints multiply. Several teams fetch overlapping data. Someone proposes the obvious consolidation: move the shared capability up into the shell, where the platform team can hold one definition of everything. One HTTP layer, one auth story, one set of data definitions, consistent behaviour by construction, drift impossible. Nobody proposes this cynically. On pure technical grounds it delivers exactly what it promises, and one definition really is easier to reason about than four copies.&lt;/p&gt;

&lt;p&gt;The costs arrive from two directions. The first is organisational, and it lands before any code is written: escalating a capability to the shell makes the platform team &lt;strong&gt;a precondition&lt;/strong&gt;. A feature team that needs a shell-level change cannot start until another team schedules the work, builds it and ships it. Its start date now sits inside someone else's sprint. Multiply that by every team with a request, and the platform team becomes a queue in front of the whole programme, while inheriting, request by request, domain knowledge it never owned and cannot keep current. The word to sit with is &lt;em&gt;start&lt;/em&gt;: the feature team is stopped, and it stays stopped until a team with different priorities ships.&lt;/p&gt;

&lt;p&gt;&lt;span&gt;Test 5&lt;/span&gt;&lt;span&gt;Can the feature team start without another team shipping first?&lt;/span&gt;&lt;br&gt;
  &lt;/p&gt;
&lt;p&gt;Escalating a capability to the shell fails this one by design.&lt;/p&gt;

&lt;p&gt;The second cost is specific to mobile: &lt;strong&gt;the shell is the binary&lt;/strong&gt;, so shell code ships at App Store cadence. Build, submit, review, staged rollout, then a long tail of users who never update at all.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;A capability moved into the shell moves onto the App Store train, and a schema tweak that rides a store review is the exact cost this series adopted federation to escape. Escalation reintroduces it quietly, one capability at a time.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The platform layer genuinely earns one kind of code, whether it rides in the shell binary or in a platform-owned package like the contract: the slow-moving kind. The HTTP client, auth, &lt;code&gt;baseApi&lt;/code&gt;'s machinery, observability, the runtimes every remote resolves against. The pattern across that list: capability that changes slowly, carries no domain knowledge, and was already depended on by everyone. The list grows rarely, and each addition needs an argument, because everything on it inherits the binary's cadence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inner source: the workable middle
&lt;/h2&gt;

&lt;p&gt;Between "every team waits on the platform team" and "no shared capability at all" there is a middle that large orgs have a name for: &lt;a href="https://innersourcecommons.org/" rel="noopener noreferrer"&gt;inner source&lt;/a&gt;. Run the shared code like an open-source project that happens to be &lt;em&gt;internal&lt;/em&gt;: the shell, the contract package, a component library, a shared data layer if your org decides it wants one. The platform team are the maintainers and code owners. Product teams send pull requests.&lt;/p&gt;

&lt;p&gt;The blocking mechanism changes shape rather than disappearing. A feature team that needs a shell-level change writes the change itself, against the shared repo's contribution rules, and waits for a review instead of a roadmap slot. The platform team's job shifts from build-everything to review-and-steward: it holds the bar on quality and coherence and stops being the queue. The Pokédex team adding a component to the shared library becomes a pull request, not a ticket in another team's backlog. So does the party team's schema addition, if a shared data layer is the choice your org made after all.&lt;/p&gt;

&lt;p&gt;The costs are real. Review latency does not vanish; a contested pull request can wait as long as a roadmap slot ever did. The platform team ends up maintaining code it did not write, a genuine burden the day the contributing team moves on. And the model only works on real contribution infrastructure:&lt;/p&gt;

&lt;p&gt;&lt;span&gt;Before the first PR&lt;/span&gt;&lt;span&gt;What inner source runs on&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;span&gt;Contribution conventions a stranger can follow, written down&lt;/span&gt;&lt;br&gt;
  &lt;span&gt;CI a contributing team can run without asking anyone&lt;/span&gt;&lt;br&gt;
  &lt;span&gt;Ownership files that route each review to the right people&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;Inner source pays for itself when contributions come often enough to justify that overhead.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“Just open a PR” against an undocumented repo is a politeness, not a process. Without the contribution infrastructure, inner source is the queue with a friendlier name.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The tests, and what moves the answers
&lt;/h2&gt;

&lt;p&gt;The whole argument fits on one map, each box holding what it owns and the cadence it ships at:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📊 &lt;strong&gt;Diagram:&lt;/strong&gt; &lt;a href="https://warrendeleon.com/blog/who-owns-what-federated-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-ownership#the-tests-and-what-moves-the-answers" rel="noopener noreferrer"&gt;view it on warrendeleon.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Five tests, in the order the series met them:&lt;/p&gt;

&lt;p&gt;&lt;span&gt;The five tests&lt;/span&gt;&lt;span&gt;Who owns what&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;&lt;strong&gt;Does this boundary have a team behind it?&lt;/strong&gt; Domains get remotes; screens get packages.&lt;/span&gt;&lt;br&gt;
  &lt;span&gt;2&lt;/span&gt;&lt;span&gt;&lt;strong&gt;Does this component know where its data comes from?&lt;/strong&gt; A shared component renders what it is given; data arrives as props.&lt;/span&gt;&lt;br&gt;
  &lt;span&gt;3&lt;/span&gt;&lt;span&gt;&lt;strong&gt;Does this definition live with its domain?&lt;/strong&gt; Machinery is shared; endpoints travel with the domain that owns the data. Parsers and schemas are the judgement call, and this build splits them by who reads them: the REST schemas every app depends on sit in the contract, while the GraphQL schema only the list reads stays in the list app.&lt;/span&gt;&lt;br&gt;
  &lt;span&gt;4&lt;/span&gt;&lt;span&gt;&lt;strong&gt;Does this dependency already exist, or does sharing invent it?&lt;/strong&gt; Couple to the backend you already depend on; refuse the peer package that manufactures a schedule dependency between teams.&lt;/span&gt;&lt;br&gt;
  &lt;span&gt;5&lt;/span&gt;&lt;span&gt;&lt;strong&gt;Can the feature team start without another team shipping first?&lt;/strong&gt; If not, the capability sits at the wrong level, or the process around it does.&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;All five sit behind the gate: more than one team ships this app. If not, folders answer every one of them for free.&lt;/p&gt;

&lt;p&gt;None of these produce one answer for every org; they tell you what each option costs in yours. Two teams that trust each other and release together can share a data package and barely feel the coupling. An app with three teams and a quarterly cadence might keep everything in the shell and never notice the price. If your org cut these lines differently and the seams hold, that is a different answer to the same tests, and worth defending on its own terms.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://warrendeleon.com/blog/client-state-across-federated-remotes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-ownership" rel="noopener noreferrer"&gt;The next post&lt;/a&gt; puts the tests straight to work. The party grid has been empty since post 4, because nothing in the app can reach it. The party team is about to fill it with &lt;a href="https://warrendeleon.com/blog/server-state-and-client-state-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-ownership" rel="noopener noreferrer"&gt;state it owns&lt;/a&gt;, and the first decisions on the way are exactly the ones this essay just argued: who owns the slice, where the endpoint file goes, what the contract carries. The rules stop being prose and start being files.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://martinfowler.com/articles/micro-frontends.html" rel="noopener noreferrer"&gt;Micro Frontends&lt;/a&gt; — Cam Jackson's survey, including the self-contained fragment pattern this essay argues against for mobile&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.melconway.com/Home/Committees_Paper.html" rel="noopener noreferrer"&gt;Conway's law&lt;/a&gt; — the 1968 paper: systems mirror the communication structures of the organisations that build them&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://innersourcecommons.org/" rel="noopener noreferrer"&gt;InnerSource Commons&lt;/a&gt; — the practice, its patterns, and the contribution infrastructure it depends on&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://teamtopologies.com/" rel="noopener noreferrer"&gt;Team Topologies&lt;/a&gt; — Skelton and Pais on platform teams and stream-aligned teams, the vocabulary behind the shell debate&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/warrendeleon/react-native-module-federation" rel="noopener noreferrer"&gt;react-native-module-federation&lt;/a&gt; — the companion repo whose decisions this essay defends&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>reactnative</category>
      <category>modulefederation</category>
      <category>architecture</category>
      <category>engineeringmanagement</category>
    </item>
    <item>
      <title>One shared store: server state across federated remotes in React Native</title>
      <dc:creator>Warren de Leon</dc:creator>
      <pubDate>Mon, 03 Aug 2026 00:07:47 +0000</pubDate>
      <link>https://dev.to/warrendeleon/one-shared-store-server-state-across-federated-remotes-in-react-native-4b5o</link>
      <guid>https://dev.to/warrendeleon/one-shared-store-server-state-across-federated-remotes-in-react-native-4b5o</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📚 &lt;strong&gt;React Native Module Federation&lt;/strong&gt; series — &lt;a href="https://warrendeleon.com/blog/series/react-native-module-federation/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-shared-store" rel="noopener noreferrer"&gt;read it in full on warrendeleon.com&lt;/a&gt;, where new parts land first.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The &lt;a href="https://warrendeleon.com/blog/typing-the-seam-between-remotes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-shared-store" rel="noopener noreferrer"&gt;last post&lt;/a&gt; ended with a sentence with a hole in it: a screen rendering "Opened from the " and then nothing, because a type checked at build time is no help against a value that only shows up at runtime. It closed on a promise, too: real data, one store, shared across the remotes. This post keeps the promise, and the hole gets its proper answer along the way.&lt;/p&gt;

&lt;p&gt;It leans on &lt;a href="https://warrendeleon.com/blog/server-state-and-client-state-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-shared-store" rel="noopener noreferrer"&gt;server state and client state&lt;/a&gt; from the short break in the series. The split between data a server owns and data the app owns is assumed here, not re-explained. This post is about one half of it, server state, under federation: one Redux Toolkit (RTK) store in the host, one RTK Query cache, and the Pokédex domain filling it with both its endpoints, while the installed &lt;code&gt;@pokedex/detail&lt;/code&gt; view renders what it is fed. Live PokéAPI data replaces every hardcoded Pokémon in the app, both drifted copies included.&lt;/p&gt;

&lt;p&gt;The shape we're building, before any code:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📊 &lt;strong&gt;Diagram:&lt;/strong&gt; &lt;a href="https://warrendeleon.com/blog/one-shared-store-across-remotes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-shared-store" rel="noopener noreferrer"&gt;view it on warrendeleon.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The one thing to hold in your head: &lt;code&gt;baseApi&lt;/code&gt; is a single object, and every side imports the exact same one. That is what makes the cache shared. Break that, and the app breaks in a way worth seeing, so we will break it on purpose near the end.&lt;/p&gt;

&lt;p&gt;Carry on from your own post 5 code if you built along. Otherwise, start from its finished state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/warrendeleon/react-native-module-federation
&lt;span class="nb"&gt;cd &lt;/span&gt;react-native-module-federation
git checkout post-05-contracts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The contract package grows a runtime seam
&lt;/h2&gt;

&lt;p&gt;So far &lt;code&gt;@pokedex/contracts&lt;/code&gt; has held only types. Every export was erased at build, so nothing from it reached a bundle. Now it gains its first runtime export: the RTK Query API object the whole app fetches through.&lt;/p&gt;

&lt;p&gt;The host looks like the natural home for it. The host owns the store, the store owns the cache, and an API object next to the store it feeds is where a single app would put it. In a single app that instinct is right. Under federation it fails on identity: a consumer can only add endpoints to the &lt;em&gt;same&lt;/em&gt; &lt;code&gt;baseApi&lt;/code&gt; instance the host store wired in, and an instance that lives inside the host's source is one nothing else can import. The contract package is the one module every side already resolves to a single copy of, because it is a Module Federation singleton. Put the instance there and a consumer's &lt;code&gt;baseApi.injectEndpoints({...})&lt;/code&gt; registers against the one cache and middleware the store already runs. One instance means one HTTP cache, one deduplication pipeline, one tag graph across every feature, including features shipped long after the shell. &lt;code&gt;packages/contracts/src/api.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createApi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fetchBaseQuery&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@reduxjs/toolkit/query/react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zod&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;baseApi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createApi&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;reducerPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;baseQuery&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;fetchBaseQuery&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://pokeapi.co/api/v2/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;tagTypes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PokemonList&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;endpoints&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;PokemonSummary&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;spriteUri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;PokemonDetail&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;spriteUri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;types&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PokemonListResponseSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="c1"&gt;// The url is not just any string: the row's id is derived from it, so the schema&lt;/span&gt;
  &lt;span class="c1"&gt;// polices the trailing numeric id a PokéAPI resource url carries.&lt;/span&gt;
  &lt;span class="na"&gt;results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\/\d&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;\/?&lt;/span&gt;&lt;span class="sr"&gt;$/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PokemonDetailResponseSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;})),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;artworkUri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.png`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;idFromResourceUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\/(\d&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;)\/?&lt;/span&gt;&lt;span class="sr"&gt;$/&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`PokéAPI resource URL has no trailing id: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isSafeInteger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`PokéAPI resource URL id is out of range: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;formatName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;word&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;word&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;word&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;parsePokemonList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;PokemonSummary&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;PokemonListResponseSchema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;idFromResourceUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;formatName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="na"&gt;spriteUri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;artworkUri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;parsePokemonDetail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;PokemonDetail&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;PokemonDetailResponseSchema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;formatName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;spriteUri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;artworkUri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;formatName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;createApi&lt;/code&gt; with no endpoints builds an empty shell: a reducer, some middleware, and an &lt;code&gt;injectEndpoints&lt;/code&gt; method the consumers will call. &lt;code&gt;fetchBaseQuery&lt;/code&gt; is a small wrapper over &lt;code&gt;fetch&lt;/code&gt; that prepends the base URL and parses JSON. &lt;code&gt;tagTypes&lt;/code&gt; names the one label this app invalidates by; it does nothing yet, and does real work in the last section.&lt;/p&gt;

&lt;p&gt;The two parse functions are the part post 5's broken sentence pointed at. A type is a compile-time promise, and it is gone by the time a response actually lands. A renamed field or a &lt;code&gt;null&lt;/code&gt; where a string used to be slips straight through a hand-written cast and crashes three screens later. So each raw response is validated with a &lt;a href="https://zod.dev/" rel="noopener noreferrer"&gt;Zod&lt;/a&gt; schema right at the seam, and a bad shape becomes a value the screen can handle instead of a crash.&lt;/p&gt;

&lt;p&gt;The seam has to police everything it depends on, which is why the list schema checks the url's trailing id rather than just &lt;code&gt;z.string()&lt;/code&gt;: the id is derived from that url, and a lenient check here would let a malformed payload through as a phantom Pokémon #0. &lt;code&gt;idFromResourceUrl&lt;/code&gt; throws for the same reason: a helper that quietly invents an id on bad input is a parser that passes malformed data instead of failing it. The range check earns its two lines too, since a trailing digit run is not yet an id. &lt;code&gt;/pokemon/0/&lt;/code&gt; names nothing, and a digit string long enough to leave the safe-integer range has stopped being the number printed in the url.&lt;/p&gt;

&lt;p&gt;The detail schema keeps only what the detail screen renders; the full PokéAPI payload for one Pokémon is close to 300 KB of JSON, and parsing fields nothing displays would just be a bigger surface to break on. Runtime validation gets a post of its own later in the series.&lt;/p&gt;

&lt;p&gt;A runtime export and two new peer dependencies are a breaking change, so the version takes a major bump. The number itself needs a glance at your own registry first: post 5's ladder already published &lt;code&gt;1.1.0&lt;/code&gt; and &lt;code&gt;2.0.0&lt;/code&gt;, and a published number is spent for good, because the registry refuses to reuse it and every install relies on that refusal. So the runtime seam ships as &lt;code&gt;3.0.3&lt;/code&gt;. The major is the story; the trailing patch is parser hardening that landed after first publication. &lt;code&gt;packages/contracts/package.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@pokedex/contracts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"3.0.3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"zod"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^3.25.76"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"peerDependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@reduxjs/toolkit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;gt;=2.10.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"react"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"react-redux"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;gt;=9"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;zod&lt;/code&gt; is a real runtime dependency, so it ships inside the package. &lt;code&gt;@reduxjs/toolkit&lt;/code&gt; and &lt;code&gt;react-redux&lt;/code&gt; are peers: the apps install them, and the contract uses their copies rather than bundling its own. Install the package's dev dependencies and publish:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;packages/contracts
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm publish
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;+ @pokedex/contracts@3.0.3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The caret does here what post 5 showed it doing. Every consumer sits on &lt;code&gt;^1.0.0&lt;/code&gt;, so &lt;code&gt;npm install&lt;/code&gt; leaves all three apps exactly where they are even with &lt;code&gt;3.0.3&lt;/code&gt; on the registry. Picking up the new contract is a deliberate move, one app at a time, and one of the three stays put on purpose.&lt;/p&gt;

&lt;h2&gt;
  
  
  One store in the host
&lt;/h2&gt;

&lt;p&gt;The host gains a store. &lt;code&gt;apps/host/src/store.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;combineSlices&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;configureStore&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@reduxjs/toolkit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;baseApi&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@pokedex/contracts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;configureStore&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;reducer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;combineSlices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;baseApi&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;getDefaultMiddleware&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;getDefaultMiddleware&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;baseApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;RootState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;ReturnType&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getState&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;AppDispatch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The split of ownership is the point of the file.&lt;/strong&gt; The store lives in the host: one store per app is the shell's job, like the tab bar. The API instance the store is built around lives in the contract package, for the identity reason above. The host owns the wiring; the seam owns the instance.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;baseApi.middleware&lt;/code&gt; is load-bearing. It runs the cache lifecycle: fetching, deduplication, tag invalidation, cache eviction. Leave it off the store and the first screen to run a query hook throws a full-screen red box in development, with RTK naming the mistake outright:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Warning: Middleware for RTK-Query API at reducerPath "api" has not been added to the store.
You must add the middleware for RTK-Query to function correctly!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpccfmct7awyqtkxr6xbr.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpccfmct7awyqtkxr6xbr.webp" alt="A full-screen React Native error box: middleware for RTK-Query API at reducerPath 'api' has not been added to the store, with the component stack pointing into the list remote's federated chunk"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Worth pausing on the component stack in that capture: the crash site is &lt;code&gt;PokedexScreen&lt;/code&gt; inside &lt;code&gt;__federation_expose_ListStack.chunk.bundle&lt;/code&gt;. A remote's screen, in a chunk the host downloaded at runtime, hits an error caused by one missing line in the host's own store file. The failure crosses the module boundary in the opposite direction to everything else in this series. A loud failure, at least. Keep it in mind, because the &lt;em&gt;Now break it&lt;/em&gt; sabotage gets no error at all.&lt;/p&gt;

&lt;p&gt;The store goes in over the whole tree, so everything federated into it can read the cache. &lt;code&gt;apps/host/App.tsx&lt;/code&gt;, the wrapper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Provider&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-redux&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./src/store&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;App&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Provider&lt;/span&gt; &lt;span class="na"&gt;store&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;SafeAreaProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;NavigationContainer&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* the tab navigator from post 4 */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;NavigationContainer&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;SafeAreaProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Provider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remotes never create or import a store. They render inside this tree and reach the store through the shared &lt;code&gt;react-redux&lt;/code&gt; singleton, the same way they reached the shared safe-area context back in post 3.&lt;/p&gt;

&lt;h2&gt;
  
  
  Share the state trio
&lt;/h2&gt;

&lt;p&gt;For anything outside the host to inject into the shared &lt;code&gt;baseApi&lt;/code&gt;, three packages have to resolve to one copy at runtime: &lt;code&gt;@reduxjs/toolkit&lt;/code&gt;, &lt;code&gt;react-redux&lt;/code&gt;, and &lt;code&gt;@pokedex/contracts&lt;/code&gt; itself. They join the host's &lt;code&gt;shared&lt;/code&gt; map as eager singletons, and post 5's hard-won lesson applies to two of them straight away. &lt;code&gt;apps/host/rspack.config.mjs&lt;/code&gt;, the additions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;rtkPkg&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@reduxjs/toolkit/package.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="kd"&gt;with&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;reactReduxPkg&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-redux/package.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="kd"&gt;with&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// ...in the shared map:&lt;/span&gt;
&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@reduxjs/toolkit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;singleton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;eager&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rtkPkg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;requiredVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dependencies&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@reduxjs/toolkit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-redux&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;singleton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;eager&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;reactReduxPkg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;requiredVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dependencies&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-redux&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@pokedex/contracts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;singleton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;eager&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;requiredVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dependencies&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@pokedex/contracts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;A package resolved through an &lt;code&gt;exports&lt;/code&gt; map needs its &lt;code&gt;version&lt;/code&gt; stated by hand.&lt;/strong&gt; Both Redux packages do, and &lt;code&gt;@pokedex/contracts&lt;/code&gt; does not. Post 5 found this in the same setup: rspack reads the version out of the package it is sharing, and skips the provide for a package resolved through an &lt;code&gt;exports&lt;/code&gt; map. It held on these versions, in this configuration, which is the claim being made. &lt;code&gt;@reduxjs/toolkit&lt;/code&gt; and &lt;code&gt;react-redux&lt;/code&gt; both carry an &lt;code&gt;exports&lt;/code&gt; map, so without the explicit &lt;code&gt;version&lt;/code&gt; neither would land in the share scope. The contract package has no &lt;code&gt;exports&lt;/code&gt; map, so it needs nothing. The bundle-grep from post 5 confirms all three provides register, contract included:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="s2"&gt;"@pokedex/contracts"&lt;/span&gt;, version: &lt;span class="s2"&gt;"3.0.3"&lt;/span&gt;
&lt;span class="s2"&gt;"@reduxjs/toolkit"&lt;/span&gt;, version: &lt;span class="s2"&gt;"2.12.0"&lt;/span&gt;
&lt;span class="s2"&gt;"react-redux"&lt;/span&gt;, version: &lt;span class="s2"&gt;"9.3.0"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The list remote mirrors the same three entries without &lt;code&gt;eager&lt;/code&gt;: the host provides the copies, the remote consumes them. This is the first time &lt;code&gt;@pokedex/contracts&lt;/code&gt; appears in any &lt;code&gt;shared&lt;/code&gt; map. Until now it was types-only, erased at build, so there was nothing to share. Now it carries &lt;code&gt;baseApi&lt;/code&gt;, and one instance is the whole point.&lt;/p&gt;

&lt;p&gt;Two of the apps add the packages and bump the contract:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/host &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install&lt;/span&gt; @reduxjs/toolkit@2.12.0 react-redux@9.3.0 @pokedex/contracts@3.0.3 &lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/list &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install&lt;/span&gt; @reduxjs/toolkit@2.12.0 react-redux@9.3.0 @pokedex/contracts@3.0.3 &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The party app is the third, and it gets nothing. No Redux packages, no new shared entries, and both of its installed packages stay where they are: the contract on &lt;code&gt;^1.0.0&lt;/code&gt; (two majors behind the seam it types against) and the detail screen on &lt;code&gt;^1.0.0&lt;/code&gt;, the static version. That is safe for a precise reason: the party app consumes only types from the contract, and types are erased at build. A runtime major changes what ships in the package's JavaScript; a consumer that never imports any of that JavaScript has nothing to break. Remotes opt into shared state; the shell forces nothing on them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The list remote: real data
&lt;/h2&gt;

&lt;p&gt;Now the first injection. &lt;code&gt;apps/list/src/listApi.ts&lt;/code&gt;, a new file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;baseApi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;parsePokemonList&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;PokemonSummary&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@pokedex/contracts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;listApi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;baseApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;injectEndpoints&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;endpoints&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;build&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;getPokemonList&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;build&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;PokemonSummary&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;queryFn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_arg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;_api&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;_extra&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;baseQuery&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;baseQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pokemon?limit=151&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;parsePokemonList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CUSTOM_ERROR&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invalid PokéAPI response&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;providesTags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PokemonList&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useGetPokemonListQuery&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;listApi&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;injectEndpoints&lt;/code&gt; adds &lt;code&gt;getPokemonList&lt;/code&gt; to the shared &lt;code&gt;baseApi&lt;/code&gt; and hands back a typed hook. The endpoint fetches the first 151 Pokémon in one request, hands the raw body to &lt;code&gt;parsePokemonList&lt;/code&gt;, and returns either the shaped rows or a caught error. &lt;code&gt;providesTags: ['PokemonList']&lt;/code&gt; stamps the result with the label the host will invalidate by. The shell knew nothing about this endpoint when it was built; the remote adds it to the running store the first time its code loads.&lt;/p&gt;

&lt;p&gt;The screen drops its hardcoded five-row array and reads the hook. &lt;code&gt;apps/list/src/PokedexScreen.tsx&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;PokedexScreen&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;insets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSafeAreaInsets&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;navigation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useNavigation&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;NativeStackNavigationProp&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ListParamList&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;refetch&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useGetPokemonListQuery&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;centre&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ActivityIndicator&lt;/span&gt; &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"large"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isError&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;centre&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Couldn't reach PokéAPI.&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Pressable&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;retry&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onPress&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;refetch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;retryText&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Try again&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Pressable&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FlatList&lt;/span&gt;
      &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;keyExtractor&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;contentContainerStyle&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;paddingBottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;insets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bottom&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;renderItem&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Pressable&lt;/span&gt;
          &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="na"&gt;onPress&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;navigation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;navigate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PokemonDetail&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Image&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;spriteUri&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sprite&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;#&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;padStart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Pressable&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three states instead of one: a spinner while the request is in flight, an error screen with a retry when PokéAPI is unreachable, and the list. The navigation from post 5 is untouched: tapping a row still pushes &lt;code&gt;PokemonDetail&lt;/code&gt; with &lt;code&gt;{ id }&lt;/code&gt; inside this remote's own stack. What changed is where the rows come from: a cache in the host's store, filled by an endpoint this remote injected, read through a hook that did not exist when the host shipped.&lt;/p&gt;

&lt;h2&gt;
  
  
  The detail goes live; the library stays a view
&lt;/h2&gt;

&lt;p&gt;Post 5 left a deliberate smell in the detail package: its own private copy of the Pokémon data, drifted from the list's copy, with a comment promising that live data would delete it. This is that moment, and it takes two releases that draw one line.&lt;/p&gt;

&lt;p&gt;The line: a component library ships pixels, and data definitions belong to the domain that owns the data. &lt;a href="https://warrendeleon.com/blog/who-owns-what-federated-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-shared-store" rel="noopener noreferrer"&gt;The next post argues that line in full&lt;/a&gt;; this one applies it. Pokémon data is the Pokédex domain's, so the second endpoint lands next to the first, in the list app. &lt;code&gt;apps/list/src/detailApi.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;baseApi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;parsePokemonDetail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;PokemonDetail&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@pokedex/contracts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;detailApi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;baseApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;injectEndpoints&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;endpoints&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;build&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;getPokemonDetail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;build&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;PokemonDetail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;queryFn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;_api&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;_extra&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;baseQuery&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;baseQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`pokemon/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;parsePokemonDetail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CUSTOM_ERROR&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invalid PokéAPI response&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useGetPokemonDetailQuery&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;detailApi&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A query that takes the id as its argument, fetches one Pokémon, parses it at the seam. No tags: nothing invalidates a single Pokémon yet.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;@pokedex/detail&lt;/code&gt; goes to &lt;code&gt;3.0.0&lt;/code&gt;: a major, because the props change shape entirely. The static copy and its lookup are gone; what remains is a view: &lt;code&gt;PokemonDetailView&lt;/code&gt; takes the Pokémon and the three states as props, renders them, and does nothing else. The list app composes hook and view in a small container on its detail route:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PokemonDetailView&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@pokedex/detail&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useGetPokemonDetailQuery&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./detailApi&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;PokemonDetailRoute&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;route&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;route&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DetailParams&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;refetch&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useGetPokemonDetailQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PokemonDetailView&lt;/span&gt; &lt;span class="na"&gt;pokemon&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isLoading&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isError&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onRetry&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;refetch&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Where the data comes from is the app's business; what it looks like is the library's. Nothing in the package touches the store, and going live added no dependencies to it at all: the same four peers the static version had, Redux nowhere among them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Count the cost of going live:&lt;/strong&gt; one endpoint file in the domain that owns the data, one honest major on the view, and a container of four lines. And the quieter payoff: the list's copy of the data and the package's copy have both gone, so the drift between them has gone too. Both screens now render whatever PokéAPI says, through one cache, and disagreeing with each other is no longer something they know how to do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Now break it
&lt;/h2&gt;

&lt;p&gt;The claim is that one shared instance holds it all together. The fastest way to trust that is to remove it and watch.&lt;/p&gt;

&lt;p&gt;Delete the &lt;code&gt;@pokedex/contracts&lt;/code&gt; entry from the &lt;code&gt;shared&lt;/code&gt; map in both configs that carry it (host and list), leaving &lt;code&gt;@reduxjs/toolkit&lt;/code&gt; and &lt;code&gt;react-redux&lt;/code&gt; in place. Restart the dev servers and relaunch.&lt;/p&gt;

&lt;p&gt;It spins. Forever. No crash, no red box, and this time nothing in the console either. Twenty seconds in, the only line the dev server has logged is the app starting up. Watch it for as long as you like: nothing else arrives.&lt;/p&gt;

&lt;p&gt;The middleware warning from the store section would seem the natural failure here, and the reason it never fires is the whole lesson. With the contract no longer shared, the host bundles its own copy of &lt;code&gt;@pokedex/contracts&lt;/code&gt; and the list remote bundles a separate one. Two copies means two &lt;code&gt;baseApi&lt;/code&gt; objects. The host store wired the reducer and middleware of &lt;em&gt;its&lt;/em&gt; copy, so from where RTK stands the setup is complete and healthy: nothing missing, nothing to warn about. Both of the domain's endpoints registered in the list's copy, one no store ever wired, so the endpoints exist, the hooks run, and the fetches they should trigger go nowhere. Each copy is internally consistent. The mistake sits between them, and nothing at runtime owns "between".&lt;/p&gt;

&lt;p&gt;This is the quiet failure the shared-singleton posts keep returning to, and it now has a full ladder. Two Reacts crash on launch. A missing middleware throws a red box that names the file to fix. Two &lt;code&gt;baseApi&lt;/code&gt;s give you a spinner over a cache that never fills, and the only diagnostic is the absence of everything else. A loud failure is cheap; the silent one costs the afternoon. Put the shared entries back, restart, and the list fills again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Invalidate across the seam
&lt;/h2&gt;

&lt;p&gt;The tag graph has been sitting unused since the contract declared it. Time to pull it.&lt;/p&gt;

&lt;p&gt;Post 4 hid every header with &lt;code&gt;headerShown: false&lt;/code&gt; on the tab navigator, and post 5 gave each remote's stack its own headers inside the tab. The host now turns the Pokédex tab's header back on, because it is about to put something host-owned in it. &lt;code&gt;apps/host/App.tsx&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useDispatch&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-redux&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;baseApi&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@pokedex/contracts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;RefreshButton&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dispatch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useDispatch&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Pressable&lt;/span&gt;
      &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;refresh&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;onPress&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;baseApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;util&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invalidateTags&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PokemonList&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;hitSlop&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;accessibilityRole&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt;
      &lt;span class="na"&gt;accessibilityLabel&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Refresh Pokédex"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;refreshText&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Refresh&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Pressable&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getFocusedRouteNameFromRoute&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@react-navigation/native&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Tab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Screen&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Pokédex"&lt;/span&gt;
  &lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;PokedexTab&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;route&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;headerShown&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;getFocusedRouteNameFromRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PokemonDetail&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headerRight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;RefreshButton&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One detail in there deserves a closer look. The detail route brings its own stack header with a back button, and a tab header stacked on top of it would put two bars on screen. So the host hides its bar while the stack sits on the detail, and shows it the moment the stack pops back.&lt;/p&gt;

&lt;p&gt;Checking against &lt;code&gt;'PokemonDetail'&lt;/code&gt; looks like the host reaching into the remote's internal route names, and that is exactly what it would be if the name lived in the remote. But it doesn't. It is the route name from &lt;code&gt;DetailParamList&lt;/code&gt; in &lt;code&gt;@pokedex/contracts&lt;/code&gt;, the same agreement the params come from. &lt;strong&gt;Be honest about how much that buys:&lt;/strong&gt; the host writes the string &lt;code&gt;'PokemonDetail'&lt;/code&gt; by hand, and TypeScript does not connect a bare string to the contract's key. Rename the route in the contract and this comparison keeps compiling while it quietly stops matching. The agreement is real; the compiler is not enforcing this end of it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;RefreshButton&lt;/code&gt; needs &lt;code&gt;Pressable&lt;/code&gt; and &lt;code&gt;Text&lt;/code&gt; added to the &lt;code&gt;react-native&lt;/code&gt; import, plus two small style entries; the complete file is in the companion tag.&lt;/p&gt;

&lt;p&gt;The host never defined &lt;code&gt;getPokemonList&lt;/code&gt;. It holds no reference to the list remote's endpoint, its hook, or its query. All it dispatches is a tag. &lt;code&gt;invalidateTags(['PokemonList'])&lt;/code&gt; walks the shared cache, finds every query that provided that tag, and refetches the ones with a subscriber on screen. The list refetches; the detail endpoint, which provides no tags, is untouched. One label, declared in the contract, provided by one feature, dispatched by the host, and the refetch lands in code the dispatcher has never seen.&lt;/p&gt;

&lt;p&gt;In a real app the invalidation hangs off a mutation's &lt;code&gt;invalidatesTags&lt;/code&gt; rather than a button, but the reach across the module boundary is the same. Teams agree on tag names in the contract the way they agree on types, and that agreement is the entire refresh protocol between them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Land exactly on this post's finished state.&lt;/strong&gt; The walkthrough prints the load-bearing files; manifests, configs, tests and the smaller edits live in the tag. To finish with a tree byte-equal to the tag, sweep the reference copy over yours. A file you typed correctly is overwritten with itself, and the sweep fills in what the prose did not print:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx degit@3.8.0 &lt;span class="nt"&gt;--force&lt;/span&gt; warrendeleon/react-native-module-federation#post-06-shared-store /tmp/pokedex-ref-06
&lt;span class="nb"&gt;rm &lt;/span&gt;packages/detail/src/PokemonDetailScreen.tsx
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; /tmp/pokedex-ref-06/. &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;rm&lt;/code&gt; matters: this post deletes that screen, and a copy over the tree adds and overwrites but never removes.&lt;/p&gt;

&lt;p&gt;The sweep also brings the Jest transform configuration the store stack needs, the parser regression tests, the per-tab stack changes and the detail refactor this walkthrough described in prose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run it
&lt;/h2&gt;

&lt;p&gt;The packages are published and installed, so Verdaccio isn't needed for the run. Three dev servers, one per app, then the simulator build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;apps/list &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run start:remote      &lt;span class="c"&gt;# :8082&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;apps/party &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run start:remote     &lt;span class="c"&gt;# :8083&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;apps/host &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm start                 &lt;span class="c"&gt;# :8081&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;apps/host &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run ios
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Pokédex tab shows a spinner for a moment, then fills with the first 151 Pokémon, official artwork included, straight from PokéAPI. Tap a row and the detail route fetches its Pokémon through the same cache. Tap Refresh in the header and the list reloads across the seam:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🎞️ &lt;strong&gt;Animated demo:&lt;/strong&gt; &lt;a href="https://warrendeleon.com/blog/one-shared-store-across-remotes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-shared-store#run-it" rel="noopener noreferrer"&gt;watch it on warrendeleon.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The party tab still renders its empty grid, contract two majors behind, detail screen still the static 1.0.0, store nowhere in sight, working exactly as it did before this post. Independence includes the freedom to not participate.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you built, and what's next
&lt;/h2&gt;

&lt;p&gt;The contract package owns the one &lt;code&gt;baseApi&lt;/code&gt; every side injects into, so the cache, the deduplication, and the tag graph are shared across features that were built and shipped on their own. The host owns the store built around it. The Pokédex domain fills the cache with both its endpoints, the installed view renders what the containers feed it, the boundary is guarded with a schema, and the host refreshes data it cannot name by dispatching a tag. When the sharing is removed, the failure is a silent spinner; you have seen it once here, so you will know it when it is expensive.&lt;/p&gt;

&lt;p&gt;Everything crossing the seam is still server state, though: data a server owns and the cache holds a copy of. Nothing the app itself owns has crossed a module boundary yet, and the app is starting to accumulate things it owns. The party grid is still empty because nothing you do in the Pokédex can reach it. Close the app and reopen it, and nothing you did survives: no party, no favourites, no memory.&lt;/p&gt;

&lt;p&gt;Client state is the other half of the split this post started from, and the build-along &lt;a href="https://warrendeleon.com/blog/client-state-across-federated-remotes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-shared-store" rel="noopener noreferrer"&gt;returns for it in two posts' time&lt;/a&gt;. First, the series &lt;a href="https://warrendeleon.com/blog/who-owns-what-federated-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-shared-store" rel="noopener noreferrer"&gt;steps back and argues the ownership calls&lt;/a&gt; the last three posts kept making one at a time: where a boundary goes, what a shared component may know, where a data definition lives.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://redux-toolkit.js.org/rtk-query/usage/code-splitting" rel="noopener noreferrer"&gt;Redux Toolkit: code splitting&lt;/a&gt; — &lt;code&gt;injectEndpoints&lt;/code&gt; and adding endpoints to an existing API at runtime&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://redux-toolkit.js.org/rtk-query/overview" rel="noopener noreferrer"&gt;RTK Query&lt;/a&gt; — the cache, the tags, and the generated hooks&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://zod.dev/" rel="noopener noreferrer"&gt;Zod&lt;/a&gt; — the schema library guarding the runtime boundary&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://pokeapi.co/" rel="noopener noreferrer"&gt;PokéAPI&lt;/a&gt; — the free REST API the app fetches from&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/warrendeleon/react-native-module-federation" rel="noopener noreferrer"&gt;react-native-module-federation&lt;/a&gt; — the companion repo, at the tag &lt;code&gt;post-06-shared-store&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>reactnative</category>
      <category>modulefederation</category>
      <category>repack</category>
      <category>rspack</category>
    </item>
    <item>
      <title>Tags vs query keys: a small API difference that changes how teams coordinate</title>
      <dc:creator>Warren de Leon</dc:creator>
      <pubDate>Mon, 27 Jul 2026 00:12:20 +0000</pubDate>
      <link>https://dev.to/warrendeleon/tags-vs-query-keys-a-small-api-difference-that-changes-how-teams-coordinate-222e</link>
      <guid>https://dev.to/warrendeleon/tags-vs-query-keys-a-small-api-difference-that-changes-how-teams-coordinate-222e</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📚 &lt;strong&gt;State Management&lt;/strong&gt; series — &lt;a href="https://warrendeleon.com/blog/series/state-management/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=tags-vs-query-keys" rel="noopener noreferrer"&gt;read it in full on warrendeleon.com&lt;/a&gt;, where new parts land first.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Tags survive a rename, keys don't
&lt;/h2&gt;

&lt;p&gt;Declarative tags scale to teams. String-based keys don't.&lt;/p&gt;

&lt;p&gt;TanStack Query's keys are shorter, lighter, and pleasant to read inside a single codebase. For a feature team that owns every query, the key-based API is the obvious choice. RTK Query's tag system adds ceremony you don't need at that size.&lt;/p&gt;

&lt;p&gt;The shape that works for one team becomes a liability across team boundaries. The failure mode shifts from "fix it before merge" to "find it from a support ticket."&lt;/p&gt;

&lt;h2&gt;
  
  
  One order, two teams
&lt;/h2&gt;

&lt;p&gt;The orders team ships a &lt;code&gt;placeOrder&lt;/code&gt; mutation. The catalogue team has a &lt;code&gt;getProduct&lt;/code&gt; query that returns an item's details and stock level. When the order succeeds, the cached product is stale (its stock count just changed), and something has to mark it for refetch.&lt;/p&gt;

&lt;h2&gt;
  
  
  With RTK Query tags
&lt;/h2&gt;

&lt;p&gt;The catalogue team declares what their query holds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;getProduct&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`/products/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;providesTags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Product&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;}),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The orders team declares what their mutation affects:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;placeOrder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mutation&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/orders&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;invalidatesTags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Product&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;}),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the mutation succeeds, the library walks its cache, finds queries that provide the &lt;code&gt;Product&lt;/code&gt; tag, and refetches the ones currently mounted. The orders team never reads the catalogue's code. They don't need to know what query key the catalogue uses or what shape the cache entry takes. They declare intent at a categorical level and the library wires it.&lt;/p&gt;

&lt;p&gt;Both teams are coupled to the string &lt;code&gt;'Product'&lt;/code&gt;. That coupling lives in a shared &lt;code&gt;tagTypes&lt;/code&gt; list declared in the API module, so it has a single source of truth.&lt;/p&gt;

&lt;h2&gt;
  
  
  With TanStack Query keys
&lt;/h2&gt;

&lt;p&gt;TanStack invalidates by key, not by category. The orders team writes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;queryClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invalidateQueries&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;queryKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;product&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For that line to be correct, the orders team has to know the catalogue team's exact query key shape. They go look at the catalogue's code, find the &lt;code&gt;useQuery&lt;/code&gt; call, read the key, and copy it into their own invalidation.&lt;/p&gt;

&lt;p&gt;The teams are coupled to the same string &lt;code&gt;'product'&lt;/code&gt;, but the coupling lives in a different place. It's a magic string in the orders team's mutation handler, with no contract pointing back to the catalogue's query.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens when teams drift
&lt;/h2&gt;

&lt;p&gt;Both approaches produce the same outcome when teams stay coordinated. The cached product refetches, the UI updates, the user sees the new stock level. The approaches diverge when coordination slips.&lt;/p&gt;

&lt;p&gt;Take a rename. The catalogue team is refactoring. The domain term has shifted. What they used to call "product" is now called "listing" across the API, the docs, and the team's day-to-day language. They rename the query key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// before&lt;/span&gt;
&lt;span class="nf"&gt;useQuery&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;queryKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;product&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;queryFn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fetchProduct&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// after&lt;/span&gt;
&lt;span class="nf"&gt;useQuery&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;queryKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;listing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;listingId&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;queryFn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fetchListing&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They ship.&lt;/p&gt;

&lt;p&gt;In an RTK Query world with tags, the rename has no effect on the orders team's invalidation. Tags are independent of query keys. The catalogue team's query still provides &lt;code&gt;'Product'&lt;/code&gt;. The orders team's mutation still invalidates &lt;code&gt;'Product'&lt;/code&gt;. The wiring holds.&lt;/p&gt;

&lt;p&gt;In a TanStack Query world with keys, the rename breaks the orders team silently. &lt;code&gt;invalidateQueries({ queryKey: ['product'] })&lt;/code&gt; now matches zero cached queries. The mutation succeeds. The library doesn't surface an error because finding zero matches isn't one. The user's product page stays stale until they navigate away and back, or until a support ticket surfaces the bug.&lt;/p&gt;

&lt;p&gt;No type error. No compile failure. No runtime exception. Stale UI in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the coupling lives
&lt;/h2&gt;

&lt;p&gt;Both approaches encode the same coupling between the two teams. The difference is where the coupling lives, and what surfaces when it breaks.&lt;/p&gt;

&lt;p&gt;Tags couple the teams through a &lt;strong&gt;category name&lt;/strong&gt;: an abstract noun describing a kind of data. It lives in a shared enum. Renames need coordination through that enum, and the rename surfaces at compile time: inside one &lt;code&gt;createApi&lt;/code&gt; module, immediately; across independently shipped apps, at each app's next build against the updated shared package.&lt;/p&gt;

&lt;p&gt;Keys couple the teams through a &lt;strong&gt;string identifier&lt;/strong&gt;: the literal shape that identifies a cached entry. It lives wherever someone types it. Renames don't need coordination because nothing forces it. They surface when the bug hits production.&lt;/p&gt;

&lt;p&gt;Neither behaviour is baked into the words "tag" and "key". A team can hard-code a tag string outside the enum, and a TanStack team can build shared, typed key factories that surface renames just as loudly. The enforcement comes from the shared contract, and the difference is which way each API leans: RTK Query asks for tags from a declared &lt;code&gt;tagTypes&lt;/code&gt; list, so the contract is the path of least resistance; TanStack accepts any array, so the contract is something you have to build and police yourself.&lt;/p&gt;

&lt;p&gt;That gap in surfacing isn't a flaw in TanStack Query's design. The key-based API is shorter, lighter, and well-suited to a single team that holds every query in its head. The shape stops fitting when the people writing the invalidation and the people writing the query stop being the same people.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which one fits your team
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;One team, one codebase.&lt;/strong&gt; If that's your app, the difference is mostly aesthetic. TanStack's key-based API is shorter and adds no ceremony. The risk of silent invalidation drift is small because you have full visibility into every query key.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multiple teams shipping independently.&lt;/strong&gt; Here the failure mode of key-based invalidation starts to compound. Each team's refactor is a possible silent break for any other team that referenced their keys. You can build conventions to soften that (query key factories, shared key constants, code review checklists, integration tests across features), but those are conventions, not enforcement: you're rebuilding what the tag system gives you for free.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A library choice still open, with federation or multi-team work ahead.&lt;/strong&gt; In that case the tag system is worth understanding before the API shape feels arbitrary. Picking between the two libraries means picking which failure modes you're willing to live with.&lt;/p&gt;

&lt;p&gt;The Module Federation series takes up the broader question of state management across independently shipped remotes, in &lt;a href="https://warrendeleon.com/blog/one-shared-store-across-remotes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=tags-vs-query-keys" rel="noopener noreferrer"&gt;One shared store&lt;/a&gt; for the server half and &lt;a href="https://warrendeleon.com/blog/client-state-across-federated-remotes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=tags-vs-query-keys" rel="noopener noreferrer"&gt;Client state across the seam&lt;/a&gt; for the client half. If you're new to the server-vs-client-state split this one assumes, &lt;a href="https://warrendeleon.com/blog/server-state-and-client-state-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=tags-vs-query-keys" rel="noopener noreferrer"&gt;start here&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>statemanagement</category>
      <category>rtkquery</category>
      <category>tanstackquery</category>
    </item>
    <item>
      <title>Server state and client state: why React apps need two libraries</title>
      <dc:creator>Warren de Leon</dc:creator>
      <pubDate>Mon, 20 Jul 2026 10:32:17 +0000</pubDate>
      <link>https://dev.to/warrendeleon/server-state-and-client-state-why-react-apps-need-two-libraries-144h</link>
      <guid>https://dev.to/warrendeleon/server-state-and-client-state-why-react-apps-need-two-libraries-144h</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📚 &lt;strong&gt;State Management&lt;/strong&gt; series — &lt;a href="https://warrendeleon.com/blog/series/state-management/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=server-vs-client-state" rel="noopener noreferrer"&gt;read it in full on warrendeleon.com&lt;/a&gt;, where new parts land first.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Almost every React Native app holds two kinds of state that behave nothing alike: server state and client state. Tell them apart and the choice of library mostly answers itself; blur them and you end up building by hand what a library would give you for free.&lt;/p&gt;

&lt;p&gt;This post and its follow-up on cache invalidation are a short break from the &lt;a href="https://warrendeleon.com/blog/why-module-federation-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=server-vs-client-state" rel="noopener noreferrer"&gt;Module Federation series&lt;/a&gt;, because the federated work ahead leans on exactly this distinction. It is easier to get straight in one plain app first.&lt;/p&gt;

&lt;p&gt;Two pairings hold up well as an app grows. With Redux Toolkit and RTK Query you keep one store, but server and client state are handled by two different tools inside it: RTK Query is a purpose-built server-state layer, caching machinery and all, and client state stays in plain &lt;code&gt;createSlice&lt;/code&gt; slices. With TanStack Query and Zustand you split across two libraries instead, the server cache living in TanStack's own client, outside any store, and client state in a small Zustand store. Either way the two kinds get different handling and are never forced through one. The shortcut that undoes that is one general-purpose store, a thunk per request, and cache logic grown by hand. It is fine on a small app and turns into a second job as it grows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two kinds of state
&lt;/h2&gt;

&lt;p&gt;Server state is a local copy of something a server owns: a profile from &lt;code&gt;/users/me&lt;/code&gt;, a product list, an order status. The copy is only ever a snapshot, and the original can change the instant after you read it. That one fact is where all the work comes from. A copy of remote data needs caching, so you are not refetching on every mount; deduplication, so two components asking for the same resource fire one request; background refetching, so the screen freshens quietly; retry, for the network that drops; invalidation, so a write can mark related data stale; and garbage collection, to drop what nothing is watching. None of that is peculiar to React. It is what any client owes data it does not own.&lt;/p&gt;

&lt;p&gt;Client state is the other half: data the app owns outright. The selected account, a sort order, a form draft, whether a modal is open, the auth tokens you hold after login (where you keep them safely is &lt;a href="https://warrendeleon.com/blog/tiered-secure-storage-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=server-vs-client-state" rel="noopener noreferrer"&gt;its own topic&lt;/a&gt;), the user's theme and language. It has to be held, it has to be reactive, and once in a while it has to survive a restart. That is nearly the whole list. It cannot go stale, so it needs no caching; there is nothing to refetch, so it needs no background sync. The two are not degrees of the same thing. &lt;strong&gt;They have different jobs, and a library built for one is the wrong tool for the other.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What each half asks for
&lt;/h2&gt;

&lt;p&gt;Server state needs a library that already has the machinery. Build it by hand and every feature grows the same scaffolding: a request, a loading flag, an error field, and a branch for each of the three states a fetch can be in. A server-state library hands you all of it behind a hook. On Redux, that library is RTK Query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createApi&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;reducerPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;baseQuery&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;fetchBaseQuery&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;endpoints&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;builder&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;getProfile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Profile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/users/me&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useGetProfileQuery&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The component reads one line, and caching, deduplication and the whole loading-and-error lifecycle come with it (retry and refetch-on-focus are opt-ins: the &lt;code&gt;retry&lt;/code&gt; wrapper around your base query, and &lt;code&gt;refetchOnFocus&lt;/code&gt; plus a &lt;code&gt;setupListeners&lt;/code&gt; call):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useGetProfileQuery&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Off Redux, TanStack Query does the same job in the same shape: declare the query, let the library run the cache and the lifecycle.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useProfile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nf"&gt;useQuery&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;queryKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;profile&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;queryFn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;getProfile&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Client state needs almost nothing by comparison. A preference has no server to compare against, so whatever the app holds is correct by definition, and caching or refetching would be solving a problem it does not have. A small store is enough. With Zustand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useSettings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Settings&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()(&lt;/span&gt;&lt;span class="kd"&gt;set&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;system&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;setTheme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;theme&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;theme&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No reducer, no dispatch, no lifecycle. On Redux the same thing is a plain &lt;code&gt;createSlice&lt;/code&gt; with a couple of setters. Either way it holds a value and lets components react to it. The mistake in both directions is the same: put client state in a server-state tool and you carry machinery it never uses; make one hand-rolled store do both and you rebuild the library's job by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which split, and when
&lt;/h2&gt;

&lt;p&gt;There is no universal winner between the two pairings, and I would distrust any post that handed you one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Already on Redux.&lt;/strong&gt; RTK Query is the shorter road, because it drops the server-state machinery into the store you already run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A fresh app with no Redux commitment.&lt;/strong&gt; TanStack Query and Zustand are two focused tools that compose without treading on each other.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A small app.&lt;/strong&gt; One store with a thunk per request is a perfectly reasonable place to start. Watch for the day you find yourself writing cache logic by hand: that is the signal to reach for a tool that already ships it.&lt;/p&gt;

&lt;p&gt;So before you next reach for a slice to hold an API response, stop on the smaller question first: do you need server-state machinery, or just somewhere to keep a value a few components can watch?&lt;/p&gt;

&lt;p&gt;Once features stop sharing a codebase the question gets sharper still. When each ships as its own app and loads at runtime, whose cache is authoritative stops being a convenience and becomes something teams have to coordinate. That is where the Module Federation series picks back up. Before then, the next post stays on the server half: how RTK Query's cache tags and TanStack's query keys handle the same invalidation, and why that small API difference decides how well each holds up once more than one team is committing to it.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>statemanagement</category>
      <category>redux</category>
      <category>reduxtoolkit</category>
    </item>
    <item>
      <title>The contract package: a versioned seam between federated remotes in React Native</title>
      <dc:creator>Warren de Leon</dc:creator>
      <pubDate>Mon, 13 Jul 2026 00:05:19 +0000</pubDate>
      <link>https://dev.to/warrendeleon/the-contract-package-a-versioned-seam-between-federated-remotes-in-react-native-1ega</link>
      <guid>https://dev.to/warrendeleon/the-contract-package-a-versioned-seam-between-federated-remotes-in-react-native-1ega</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📚 &lt;strong&gt;React Native Module Federation&lt;/strong&gt; series — &lt;a href="https://warrendeleon.com/blog/series/react-native-module-federation/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-contracts" rel="noopener noreferrer"&gt;read it in full on warrendeleon.com&lt;/a&gt;, where new parts land first.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Post 4 ended on a promise: both tabs would want to open the same Pokémon detail screen, and separately built apps would have to agree on what to pass it.&lt;/p&gt;

&lt;p&gt;This post builds that, and it takes three changes that need each other. Each remote stops exposing a bare screen and exposes a whole navigation stack, so pushing a detail belongs to the tab that pushed it. The detail screen itself ships as a versioned package both tabs install, not as another deployable, for a reason worth spelling out. And the agreement about what crosses into that screen becomes a package too: a contract with a version number, which is where the real lesson of this post lives. At the close we break the agreement on purpose, three rungs up a version ladder, and end at a hole no compiler in the codebase can see.&lt;/p&gt;

&lt;p&gt;Carry on from your own post 4 code if you built along. Otherwise:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/warrendeleon/react-native-module-federation
&lt;span class="nb"&gt;cd &lt;/span&gt;react-native-module-federation
git checkout post-04-host-shell
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Each tab grows a stack
&lt;/h2&gt;

&lt;p&gt;In &lt;a href="https://warrendeleon.com/blog/host-shell-federated-tabs-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-contracts" rel="noopener noreferrer"&gt;post 4&lt;/a&gt; each remote handed the host a bare screen and the host arranged them into tabs. A detail screen changes what "inside a tab" means: tapping a row should push a new screen while the tab bar stays put, and whoever owns that push owns navigation inside the tab. That is the remote's business, not the shell's. So each remote now exposes a stack.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;apps/list/src/ListStack.tsx&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createNativeStackNavigator&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@react-navigation/native-stack&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;PokemonDetailScreen&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@pokedex/detail&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ListParamList&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./routes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;PokedexScreen&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./PokedexScreen&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Stack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;createNativeStackNavigator&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ListParamList&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ListStack&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Navigator&lt;/span&gt; &lt;span class="na"&gt;screenOptions&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;headerShown&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Screen&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"PokedexList"&lt;/span&gt;
        &lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;PokedexScreen&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Pokédex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Screen&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"PokemonDetail"&lt;/span&gt;
        &lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;PokemonDetailScreen&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;headerShown&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Navigator&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;import PokemonDetailScreen from '@pokedex/detail'&lt;/code&gt; is the second change of the post, and we will come back to it. First, the wiring around it. The remote's public surface changed: it exposes &lt;code&gt;./ListStack&lt;/code&gt; where it used to expose &lt;code&gt;./PokedexScreen&lt;/code&gt;, so its rspack config renames the entry in &lt;code&gt;exposes&lt;/code&gt;, and the host's two lazy imports become &lt;code&gt;listApp/ListStack&lt;/code&gt; and &lt;code&gt;partyApp/PartyStack&lt;/code&gt;. The party app mirrors all of it with its own &lt;code&gt;PartyStack&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The share map evolves too, in both directions of post 4's rule. The remotes now import &lt;code&gt;@react-navigation/native&lt;/code&gt;, &lt;code&gt;@react-navigation/native-stack&lt;/code&gt; and &lt;code&gt;react-native-screens&lt;/code&gt;, and all three carry identity: navigation context, module-level registries, a native view registered once per process. So all three join the shared singleton maps on every side. Being imported by more than one party is what put them on the table; carrying identity is what decides the entry. Two of the new entries state &lt;code&gt;version&lt;/code&gt; by hand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@react-navigation/native&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;singleton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;navPkg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;requiredVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dependencies&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@react-navigation/native&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;A package resolved through an &lt;code&gt;exports&lt;/code&gt; map gets no provide without a stated &lt;code&gt;version&lt;/code&gt;.&lt;/strong&gt; In this setup rspack reads the version out of the package it is sharing, and does not manage it for a package behind such a map, which React Navigation has. Treat that as behaviour verified here, on these versions, rather than a general rule about &lt;code&gt;exports&lt;/code&gt; maps. Without the explicit &lt;code&gt;version&lt;/code&gt; the bundler silently skips the provide: nothing lands in the share scope, and the app dies at launch on RUNTIME-006 (eager) or quietly bundles a duplicate (not eager). The navigation packages need it; &lt;code&gt;react-native-screens&lt;/code&gt; doesn't. Checking for an &lt;code&gt;exports&lt;/code&gt; field in a package's package.json tells you which treatment it needs.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Run it and the product change is visible: tap a row, the detail pushes inside the tab, and the tab bar stays on screen.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🎞️ &lt;strong&gt;Animated demo:&lt;/strong&gt; &lt;a href="https://warrendeleon.com/blog/typing-the-seam-between-remotes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-contracts#each-tab-grows-a-stack" rel="noopener noreferrer"&gt;watch it on warrendeleon.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Where does the shared screen live?
&lt;/h2&gt;

&lt;p&gt;Both stacks mount the same detail screen. In this series' product it is one screen; in a real app it is the sort of thing two teams genuinely share. Where does it go?&lt;/p&gt;

&lt;p&gt;The federated answer suggests itself: a third remote, a &lt;code&gt;detailApp&lt;/code&gt; on its own port, declared in both consumers' &lt;code&gt;remotes&lt;/code&gt; maps. It works mechanically (Module Federation resolves nested remotes without complaint) and it keeps the screen updatable without touching either consumer. It is also the wrong cut. A federation boundary is a deployable, with a dev server, a deploy pipeline, and a failure mode all its own. Teams cut those boundaries along domains: one team, one domain, one remote, several screens inside it. &lt;strong&gt;Screen count is the wrong measure.&lt;/strong&gt; What earns a federation boundary is a team that owns the thing and needs to release it on its own schedule, and one screen rarely comes with either. Here the detail screen belongs to the same domain as the list, so it travels as a package rather than a deployable.&lt;/p&gt;

&lt;p&gt;A screen two domains share is a component, and shared components have had a delivery mechanism for decades: a package. So the detail screen ships as &lt;code&gt;@pokedex/detail&lt;/code&gt;, published to a registry and installed by both tab apps, each of which mounts it in its own stack wherever it sees fit. The tabs stay the deployables. The screen is a dependency. That rule gets &lt;a href="https://warrendeleon.com/blog/who-owns-what-federated-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-contracts" rel="noopener noreferrer"&gt;argued in full two posts from here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;packages/detail/src/PokemonDetailScreen.tsx&lt;/code&gt;, the parts that matter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;DetailParams&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@pokedex/contracts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;POKEMON&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;types&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bulbasaur&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Grass&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Poison&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Charmander&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Fire&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Squirtle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Water&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Pikachu&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Electric&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="mi"&gt;133&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Eevee&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Normal&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;PokemonDetailScreenProps&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;route&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DetailParams&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;PokemonDetailScreen&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;route&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;PokemonDetailScreenProps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pokemon&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;POKEMON&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="c1"&gt;// renders the dex number, name, sprite and type chips&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things are deliberate here. The screen carries its own copy of the Pokémon data: the list app has one too, and the two have already drifted, because the list needs names and this screen needs types as well. Two copies of the same facts is a smell, and it is planted on purpose: live data deletes both in &lt;a href="https://warrendeleon.com/blog/one-shared-store-across-remotes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-contracts" rel="noopener noreferrer"&gt;the next post&lt;/a&gt;. And the props are typed structurally (&lt;code&gt;{ route: { params: DetailParams } }&lt;/code&gt;) rather than by importing React Navigation's types. The screen is mounted by two stacks it has never seen, so it states the shape it needs and stays free of a navigation dependency. Its &lt;code&gt;DetailParams&lt;/code&gt; import is the contract package, which this post now needs to explain.&lt;/p&gt;

&lt;h2&gt;
  
  
  The agreement no app can own
&lt;/h2&gt;

&lt;p&gt;Look at what has to line up. The list app pushes &lt;code&gt;PokemonDetail&lt;/code&gt; with &lt;code&gt;{ id }&lt;/code&gt;. The party app will push the same route once it has members to tap. The screen reads &lt;code&gt;route.params.id&lt;/code&gt;. Three codebases touch one shape, and none of them can define it for the others: a definition in the list app is invisible to the screen's package, and hand-copying the type into each codebase is drift with a delay on it.&lt;/p&gt;

&lt;p&gt;The same problem exists one level up, where the host consumes the remotes. The host's ambient declarations describe modules that have no file to point at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;declare&lt;/span&gt; &lt;span class="kr"&gt;module&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;listApp/ListStack&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ListStackModule&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@pokedex/contracts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ListStack&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ListStackModule&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;ListStack&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An ambient declaration is a hand-written promise. TypeScript believes it because there is nothing to check it against: the module it describes resolves at runtime, from a server, long after the compiler exits. Pointing the declaration at a type from an installed package does not make the promise checkable; it makes the promise &lt;strong&gt;shared&lt;/strong&gt;, so the host and the remote at least describe the surface from one definition instead of two guesses. What it still cannot catch, honestly: if the remote renames its expose, the declaration goes stale and the compiler stays green. The check on that seam does not exist at build time. &lt;em&gt;Now break it: the version ladder&lt;/em&gt; is built on that missing check.&lt;/p&gt;

&lt;p&gt;So the agreements live in &lt;code&gt;@pokedex/contracts&lt;/code&gt;, a package that holds types and nothing else:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// packages/contracts/src/params.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;DetailParams&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;DetailParamList&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;PokemonDetail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DetailParams&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each stack embeds the fragment in its own param list (&lt;code&gt;type ListParamList = DetailParamList &amp;amp; { PokedexList: undefined }&lt;/code&gt;), the screen package reads &lt;code&gt;DetailParams&lt;/code&gt;, and the host types its module declarations from the same place. Everything in the package is erased at build. Nothing from it reaches a bundle; it exists so that every &lt;code&gt;tsc&lt;/code&gt; in the repo checks against one definition.&lt;/p&gt;

&lt;h2&gt;
  
  
  Publish both to a registry
&lt;/h2&gt;

&lt;p&gt;A package needs a registry, and &lt;code&gt;file:&lt;/code&gt; paths won't do: a path is a location on one machine, and the point of both packages is that separately built apps (in a real org, separately owned repos) install the same artefact by name and version. &lt;a href="https://verdaccio.org/" rel="noopener noreferrer"&gt;Verdaccio&lt;/a&gt; is an npm registry in a single process, good enough to make everything about the flow real:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx verdaccio                                    &lt;span class="c"&gt;# :4873, stays up&lt;/span&gt;
npm adduser &lt;span class="nt"&gt;--registry&lt;/span&gt; http://localhost:4873     &lt;span class="c"&gt;# any username, password and email&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A one-line &lt;code&gt;.npmrc&lt;/code&gt; at the repo root points the scope at it (&lt;code&gt;@pokedex:registry=http://localhost:4873/&lt;/code&gt;) and both packages publish:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;packages/contracts &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run build &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm publish &lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;packages/detail &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run build &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm publish &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;+ @pokedex/contracts@1.0.0
+ @pokedex/detail@1.0.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The detail package declares its needs as peers rather than bundling them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"peerDependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"@pokedex/contracts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;gt;=1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"react"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"react-native"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"react-native-safe-area-context"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;gt;=5"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A peer range is a claim about compatibility, and &lt;code&gt;&amp;gt;=1&lt;/code&gt; is a loose one: it says this screen works with any contract from 1.0.0 up. Loose ranges buy consumers flexibility to upgrade at their own pace. What they cost is precision, and &lt;em&gt;Now break it: the version ladder&lt;/em&gt; shows the price.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The two tab remotes install both packages; the host installs only the contract&lt;/strong&gt;, because nothing in the shell renders a detail screen. A real install, by version, from the registry, each app named:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/host &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install&lt;/span&gt; @pokedex/contracts@1.0.0 &lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/list &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install&lt;/span&gt; @pokedex/contracts@1.0.0 @pokedex/detail@1.0.0 &lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/party &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install&lt;/span&gt; @pokedex/contracts@1.0.0 @pokedex/detail@1.0.0 &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every consumer unpacks its own copy into &lt;code&gt;node_modules&lt;/code&gt;, holding the version it asked for. Counting the checkout: the host, the list app and the party app each hold one, and the detail package holds one for its own build, so four copies of the contract sit on disk. That sounds like the drift problem again, until you notice the difference: these copies have version numbers on them, and version numbers are what the rest of this post is about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Now break it: the version ladder
&lt;/h2&gt;

&lt;p&gt;The setup works. Both tabs mount the installed screen, the pushes type-check against the installed contract, and the simulator behaves. The interesting question is what happens when the packages move and the apps don't, because in a real org they won't, not in step. Three rungs.&lt;/p&gt;

&lt;h3&gt;
  
  
  1.1.0, an addition
&lt;/h3&gt;

&lt;p&gt;The party will eventually pass a slot-instance id when a tapped Pokémon came from a party slot. Optional field, additive change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;DetailParams&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bump to &lt;code&gt;1.1.0&lt;/code&gt;, build, publish. Then, in the list app, the part most explanations skip:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;up to &lt;span class="nb"&gt;date&lt;/span&gt;, audited 1188 packages &lt;span class="k"&gt;in &lt;/span&gt;926ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing happens. The lockfile pins &lt;code&gt;1.0.0&lt;/code&gt;, and &lt;code&gt;npm install&lt;/code&gt; honours the lockfile. The caret in &lt;code&gt;package.json&lt;/code&gt; says what this app would &lt;em&gt;accept&lt;/em&gt;; the lockfile decides what it &lt;em&gt;gets&lt;/em&gt;. Taking the minor is a deliberate act:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm update @pokedex/contracts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;changed 1 package, and audited 1188 packages &lt;span class="k"&gt;in &lt;/span&gt;868ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;npm ls @pokedex/contracts&lt;/code&gt; shows the new resolution, including the screen package accepting it through its loose peer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;+-- @pokedex/contracts@1.1.0
&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="nt"&gt;--&lt;/span&gt; @pokedex/detail@1.0.0
  &lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="nt"&gt;--&lt;/span&gt; @pokedex/contracts@1.1.0 deduped
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;tsc&lt;/code&gt; passes with no source change anywhere: a consumer built against &lt;code&gt;1.0.0&lt;/code&gt; never passes &lt;code&gt;uid&lt;/code&gt;, and still satisfies the type. That is what makes an additive change safe to roll out unevenly.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.0.0, a field that has to be there
&lt;/h3&gt;

&lt;p&gt;The next change is the kind that breaks. The detail screen wants to show where it was opened from, so the param becomes required, and the screen learns to render it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;DetailParams&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pokedex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;party&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A required field invalidates the old shape, so the contract goes to &lt;code&gt;2.0.0&lt;/code&gt;. The screen package follows it there: &lt;code&gt;@pokedex/detail@2.0.0&lt;/code&gt;, built against the new contract, rendering a small origin line from &lt;code&gt;route.params.source&lt;/code&gt;. Back in the list app, with both on the registry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;up to &lt;span class="nb"&gt;date&lt;/span&gt;, audited 1188 packages &lt;span class="k"&gt;in &lt;/span&gt;886ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It stays on &lt;code&gt;1.1.0&lt;/code&gt;. The caret refuses the major without being asked, which is the one piece of this whole arrangement that works by default.&lt;/p&gt;

&lt;h3&gt;
  
  
  The blind spot
&lt;/h3&gt;

&lt;p&gt;Three teams ship at three speeds. The party team adopts both &lt;code&gt;2.0.0&lt;/code&gt;s and compiles clean: it pushes nothing yet, so the new requirement costs it nothing. The list team has a release out and stays on the &lt;code&gt;1.1.0&lt;/code&gt; contract. But the screen package is just a dependency, and dependencies get bumped: someone in the list team takes the new screen without taking the new contract.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @pokedex/detail@2.0.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;changed 1 package, and audited 1188 packages &lt;span class="k"&gt;in &lt;/span&gt;1s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No warning. The screen's peer range says &lt;code&gt;&amp;gt;=1&lt;/code&gt;, the list app holds &lt;code&gt;1.1.0&lt;/code&gt;, and &lt;code&gt;1.1.0 &amp;gt;= 1&lt;/code&gt;. npm is satisfied:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;+-- @pokedex/contracts@1.1.0
&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="nt"&gt;--&lt;/span&gt; @pokedex/detail@2.0.0
  &lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="nt"&gt;--&lt;/span&gt; @pokedex/contracts@1.1.0 deduped
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is the part worth slowing down for. The list app now bundles a screen whose &lt;em&gt;runtime code&lt;/em&gt; reads &lt;code&gt;route.params.source&lt;/code&gt;. Ask what its &lt;em&gt;compiler&lt;/em&gt; checked, and the answer is: the screen's published type declarations say the props take &lt;code&gt;DetailParams&lt;/code&gt;, imported from &lt;code&gt;@pokedex/contracts&lt;/code&gt; and resolved to whatever copy the consumer installed. The list app resolved it to &lt;code&gt;1.1.0&lt;/code&gt;, where &lt;code&gt;source&lt;/code&gt; does not exist. So the list app's push of &lt;code&gt;{ id }&lt;/code&gt; type-checks perfectly against the very screen that is about to read a field it never sent. Every &lt;code&gt;tsc&lt;/code&gt; in the codebase is green, and every one of them is checking a different sentence than the one the runtime will speak.&lt;/p&gt;

&lt;p&gt;At runtime, the list tab pushes Bulbasaur:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8up7q9gcd4ooiaqmsje1.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8up7q9gcd4ooiaqmsje1.webp" alt="The Pokémon detail screen reading 'Opened from the' with nothing after it, because the list app holds an older contract version and never passed the field the newer screen renders" width="600" height="1305"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;"Opened from the ", and then nothing. No crash, no red box, no warning in any terminal. A sentence with a hole in it, shipped to a user.&lt;/p&gt;

&lt;p&gt;The contract did not fail. It did what a compile-time contract can do, which is hold each app to the version that app installed. The loose peer did not fail either; it did exactly what &lt;code&gt;&amp;gt;=1&lt;/code&gt; says. The hole is between them: a type is a compile-time promise, and it is gone by the time a value actually crosses. Checking the value that arrives is a different job, done by different tools, at a different time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you built, and what's next
&lt;/h2&gt;

&lt;p&gt;Navigation moved down into the remotes. The shared screen ships as a versioned package, because a federation boundary is a team's domain and a screen is a component. The agreements (params and module shapes) live in a contract package every side installs, and semver governs how the three codebases drift: minors roll out unevenly and harmlessly, majors wait for consent, and the one failure that slipped through did it silently, in the gap between a loose peer range and an erased type.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Land exactly on this post's finished state.&lt;/strong&gt; The walkthrough prints the load-bearing files; manifests, configs, tests and the smaller edits live in the tag. To finish with a tree byte-equal to the tag, sweep the reference copy over yours. Most files you typed are overwritten with themselves, and the sweep fills in what the prose did not print:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx degit@3.8.0 &lt;span class="nt"&gt;--force&lt;/span&gt; warrendeleon/react-native-module-federation#post-05-contracts /tmp/pokedex-ref-05
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; /tmp/pokedex-ref-05/. &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That fills in the package manifests, the packages' exports and TypeScript configuration, and the host, list and party edits the prose summarised.&lt;/p&gt;

&lt;p&gt;One part of your tree does change. The version ladder is a demonstration, not a step the series builds on: &lt;code&gt;1.1.0&lt;/code&gt; and &lt;code&gt;2.0.0&lt;/code&gt; stay in your registry, but the repo carries &lt;code&gt;DetailParams&lt;/code&gt; forward as &lt;code&gt;{ id: number }&lt;/code&gt;. So the sweep returns the contract source, the detail screen and the three app manifests to their &lt;code&gt;1.0.0&lt;/code&gt; shape, and post 6 starts from there.&lt;/p&gt;

&lt;p&gt;The finished code for this post is the &lt;code&gt;post-05-contracts&lt;/code&gt; tag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git checkout post-05-contracts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://warrendeleon.com/blog/one-shared-store-across-remotes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-contracts" rel="noopener noreferrer"&gt;Next in the series&lt;/a&gt;: real data. The hardcoded Pokémon lists disappear, both copies, and every screen reads from one store fed by PokéAPI. That is where the hole in that sentence gets a proper answer, because a type checked at build time is no help against a value that shows up at runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://module-federation.io/" rel="noopener noreferrer"&gt;Module Federation 2.0&lt;/a&gt; — the shared-module runtime, and the &lt;a href="https://module-federation.io/guide/troubleshooting/runtime.html" rel="noopener noreferrer"&gt;RUNTIME-006 reference&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://verdaccio.org/" rel="noopener noreferrer"&gt;Verdaccio&lt;/a&gt; — the local npm registry both packages are published to&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://reactnavigation.org/" rel="noopener noreferrer"&gt;React Navigation&lt;/a&gt; — the native stack each remote now owns&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.npmjs.com/cli/v10/configuring-npm/package-json#dependencies" rel="noopener noreferrer"&gt;npm semver ranges&lt;/a&gt; — what a caret does and does not accept, and what a peer range claims&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/warrendeleon/react-native-module-federation" rel="noopener noreferrer"&gt;react-native-module-federation&lt;/a&gt; — the companion repo, at the tag &lt;code&gt;post-05-contracts&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>reactnative</category>
      <category>modulefederation</category>
      <category>repack</category>
      <category>rspack</category>
    </item>
    <item>
      <title>The host shell: federated remotes as tabs in React Native</title>
      <dc:creator>Warren de Leon</dc:creator>
      <pubDate>Mon, 22 Jun 2026 13:03:33 +0000</pubDate>
      <link>https://dev.to/warrendeleon/the-host-shell-federated-remotes-as-tabs-in-react-native-38k3</link>
      <guid>https://dev.to/warrendeleon/the-host-shell-federated-remotes-as-tabs-in-react-native-38k3</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📚 &lt;strong&gt;React Native Module Federation&lt;/strong&gt; series — &lt;a href="https://warrendeleon.com/blog/series/react-native-module-federation/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-host-shell" rel="noopener noreferrer"&gt;read it in full on warrendeleon.com&lt;/a&gt;, where new parts land first.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://warrendeleon.com/blog/shared-singleton-contract-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-host-shell" rel="noopener noreferrer"&gt;Post 3&lt;/a&gt; closed on a promise: the host stops being a single screen and becomes a real shell, owning the tab bar while each tab is a remote loaded at runtime. That's this post.&lt;/p&gt;

&lt;p&gt;So far the host has loaded one screen from one remote, fullscreen, with nowhere else to go. A real app has a frame around its features: a tab bar, navigation, the parts that are always on screen. This post gives the host that job, and gives the second tab to a second remote.&lt;/p&gt;

&lt;h2&gt;
  
  
  The division of labour
&lt;/h2&gt;

&lt;p&gt;One sentence carries the whole post: &lt;strong&gt;the host owns the frame, and remotes own the features inside it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The host installs the navigation library, mounts the tab bar and decides which tabs exist. The remotes stay plain screens. They don't import a navigator, don't know they're in a tab, and don't know a second remote exists. Adding a feature should be boring: build another remote, add another tab, ship it.&lt;/p&gt;

&lt;p&gt;Two teams can then own two tabs, build them separately and deploy them on their own schedules, because neither one has to compile against the other.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📊 &lt;strong&gt;Diagram:&lt;/strong&gt; &lt;a href="https://warrendeleon.com/blog/host-shell-federated-tabs-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-host-shell#the-division-of-labour" rel="noopener noreferrer"&gt;view it on warrendeleon.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The starting point is post 3's finished state. If you built along, that's the code you already have. If not:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/warrendeleon/react-native-module-federation
&lt;span class="nb"&gt;cd &lt;/span&gt;react-native-module-federation
git checkout post-03-shared-singleton
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A second remote to fill a second tab
&lt;/h2&gt;

&lt;p&gt;One tab is not a tab bar. So we add a second remote, &lt;code&gt;party&lt;/code&gt;, the same way post 2 built the &lt;code&gt;list&lt;/code&gt; remote: a fresh React Native app on Re.Pack, with no &lt;code&gt;AppRegistry.registerComponent&lt;/code&gt;, exposing one screen. The exact route, mirroring post 2's list steps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @react-native-community/cli@20.1.0 init Party &lt;span class="nt"&gt;--directory&lt;/span&gt; apps/party &lt;span class="nt"&gt;--version&lt;/span&gt; 0.85.3
&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/party &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; @callstack/repack@5.2.5 @rspack/core@2.0.5 @module-federation/enhanced@2.5.0 @swc/helpers@0.5.23 @react-native-community/cli@20.2.0 @react-native-community/cli-platform-android@20.2.0 @react-native-community/cli-platform-ios@20.2.0 &lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;cp &lt;/span&gt;apps/list/react-native.config.js apps/party/
&lt;span class="nb"&gt;cp &lt;/span&gt;apps/list/rspack.config.mjs apps/party/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then give it the same near-empty container entry the list has: copy &lt;code&gt;apps/list/src/index.js&lt;/code&gt; to &lt;code&gt;apps/party/src/index.js&lt;/code&gt;. The root &lt;code&gt;index.js&lt;/code&gt; keeps its registration for standalone runs; the federated entry is the one with nothing to do at startup.&lt;/p&gt;

&lt;p&gt;Four fields change in the copied &lt;code&gt;apps/party/rspack.config.mjs&lt;/code&gt;, and all four matter. Their finished values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// output, near the top:&lt;/span&gt;
      &lt;span class="nx"&gt;uniqueName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PartyApp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

&lt;span class="c1"&gt;// inside the ModuleFederationPluginV2 options:&lt;/span&gt;
        &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;partyApp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;partyApp.container.js.bundle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;exposes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./PartyScreen&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./src/PartyScreen.tsx&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;&lt;code&gt;uniqueName&lt;/code&gt; is the easy one to miss.&lt;/strong&gt; &lt;code&gt;uniqueName&lt;/code&gt; scopes webpack's chunk-loading globals, so two remotes shipping the same value collide inside the host's runtime.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The screen it exposes, &lt;code&gt;apps/party/src/PartyScreen.tsx&lt;/code&gt;, holds six empty slots:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;StyleSheet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;View&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-native&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useSafeAreaInsets&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-native-safe-area-context&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SLOTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;PartyScreen&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;insets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSafeAreaInsets&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;paddingTop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;insets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;top&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Party&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subtitle&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        Your party is empty. Add up to 6 Pokémon from the Pokédex.
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;grid&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;SLOTS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slot&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;slot&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slot&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slotNumber&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;slot&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;styles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;StyleSheet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#fff&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;fontSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;fontWeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;700&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;subtitle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;fontSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#6b7280&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;marginBottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="c1"&gt;// alignItems: 'flex-start' stops the default stretch from overriding each slot's aspectRatio.&lt;/span&gt;
  &lt;span class="na"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;flexDirection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;row&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;flexWrap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;wrap&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;alignItems&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;flex-start&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;slot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;47%&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;aspectRatio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;borderRadius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;borderWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;borderStyle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dashed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;borderColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#e5e7eb&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#f9fafb&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;alignItems&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;center&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;justifyContent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;center&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;slotNumber&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;fontSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;fontWeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;600&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#9ca3af&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing fills those slots yet, and that's deliberate. The app has no store, so the party has nowhere to keep its members; that arrives with the state posts. Today the team owning this feature ships the smallest thing it can put in front of a user: a screen with a shape and no data behind it. A screen with no props and no state is still a feature the shell can load at runtime.&lt;/p&gt;

&lt;p&gt;It reads the safe-area inset through &lt;code&gt;useSafeAreaInsets&lt;/code&gt;, which is post 3's shared singleton earning its keep: the inset comes from the host's provider, measured once, and this remote reads it without carrying its own copy of the library.&lt;/p&gt;

&lt;p&gt;Give it its own dev-server port so it doesn't collide with &lt;code&gt;list&lt;/code&gt; on 8082. In &lt;code&gt;apps/party/package.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"start:remote"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"react-native start --config rspack.config.mjs --port 8083"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now there are two remotes, on 8082 and 8083, each a screen waiting for a host.&lt;/p&gt;

&lt;h2&gt;
  
  
  The host gets navigation
&lt;/h2&gt;

&lt;p&gt;In this build the tab bar belongs to the host, not the remotes. Install the navigation packages in the host only:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/host &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install&lt;/span&gt; @react-navigation/native@7.3.1 @react-navigation/bottom-tabs@7.18.0 react-native-screens@4.25.2 &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/host/ios &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; bundle &lt;span class="nb"&gt;exec &lt;/span&gt;pod &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;That pod install is not optional, and neither is a fresh native build afterwards.&lt;/strong&gt; &lt;code&gt;react-native-screens&lt;/code&gt; ships native code. Skip either step and the build stops with &lt;code&gt;'RNSViewInteractionAware.h' file not found&lt;/code&gt;, which reads like a broken library and is really a missing pod. It is the first native dependency the shell has taken on since post 2, and a preview of a constraint the series returns to later: JavaScript can arrive over the wire, native code cannot.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The federation part is post 3's contract read backwards. The shared singletons stay exactly what they were: &lt;code&gt;react&lt;/code&gt;, &lt;code&gt;react-native&lt;/code&gt;, and &lt;code&gt;react-native-safe-area-context&lt;/code&gt;. React Navigation and &lt;code&gt;react-native-screens&lt;/code&gt; are &lt;strong&gt;not&lt;/strong&gt; shared, because no remote imports them.&lt;/p&gt;

&lt;p&gt;Sharing everything by default is a defensible habit: one fewer decision per dependency, and no chance of forgetting something you needed. But sharing keeps two copies of a library from both loading in one runtime, and navigation never leaves the host, so there's no second copy to collide with.&lt;/p&gt;

&lt;p&gt;Both sides touching a library is what makes sharing worth considering, not what makes it necessary. What decides it is whether the library carries identity: module-level state, a React context, a native module registered once per process. &lt;a href="https://warrendeleon.com/blog/shared-singleton-contract-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-host-shell" rel="noopener noreferrer"&gt;Post 3&lt;/a&gt; is where that test came from, and &lt;a href="https://warrendeleon.com/blog/who-owns-what-federated-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-host-shell" rel="noopener noreferrer"&gt;post 7&lt;/a&gt; is where it turns into a rule about who owns which boundary. Those have to be one copy. A value-only library that both sides import, with no instance to disagree about, is safe to duplicate and often cheaper to.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shell
&lt;/h2&gt;

&lt;p&gt;Rewrite &lt;code&gt;apps/host/App.tsx&lt;/code&gt;. The host now owns a &lt;code&gt;SafeAreaProvider&lt;/code&gt;, a &lt;code&gt;NavigationContainer&lt;/code&gt; and a bottom tab navigator, and each tab's content is a lazily-loaded remote:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Suspense&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ActivityIndicator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;StyleSheet&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-native&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;SafeAreaProvider&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-native-safe-area-context&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NavigationContainer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@react-navigation/native&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createBottomTabNavigator&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@react-navigation/bottom-tabs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PokedexScreen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lazy&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;listApp/PokedexScreen&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PartyScreen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lazy&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;partyApp/PartyScreen&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="c1"&gt;// A remote downloads the first time its tab is opened, so each tab renders behind a Suspense&lt;/span&gt;
&lt;span class="c1"&gt;// spinner. Wrapping once here keeps the lazy boundary out of the remotes.&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;withSuspense&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Remote&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ComponentType&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Tab&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Suspense&lt;/span&gt; &lt;span class="na"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ActivityIndicator&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;loader&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"large"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Remote&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Suspense&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PokedexTab&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;withSuspense&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PokedexScreen&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PartyTab&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;withSuspense&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PartyScreen&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Tab&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createBottomTabNavigator&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;App&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;SafeAreaProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;NavigationContainer&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Tab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Navigator&lt;/span&gt; &lt;span class="na"&gt;screenOptions&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;headerShown&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Tab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Screen&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Pokédex"&lt;/span&gt; &lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;PokedexTab&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Tab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Screen&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Party"&lt;/span&gt; &lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;PartyTab&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Tab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Navigator&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;NavigationContainer&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;SafeAreaProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;styles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;StyleSheet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each tab is a remote behind &lt;code&gt;React.lazy&lt;/code&gt; and &lt;code&gt;Suspense&lt;/code&gt;, so the app fetches only the tab it opens on and leaves the other until you press it.&lt;/p&gt;

&lt;p&gt;The host needs to know where the second remote lives. Add it to &lt;code&gt;remotes&lt;/code&gt; in &lt;code&gt;apps/host/rspack.config.mjs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;remotes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;listApp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`listApp@http://localhost:8082/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/mf-manifest.json`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;partyApp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`partyApp@http://localhost:8083/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/mf-manifest.json`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And tell TypeScript the shape of the new federated import, in &lt;code&gt;apps/host/mf-modules.d.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;declare&lt;/span&gt; &lt;span class="kr"&gt;module&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;partyApp/PartyScreen&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PartyScreen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ComponentType&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;PartyScreen&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One more file, or the tests break before they run. React Navigation and &lt;code&gt;react-native-screens&lt;/code&gt; ship ES modules, and the base React Native jest preset only sends React Native's own packages through Babel: its &lt;code&gt;transformIgnorePatterns&lt;/code&gt; lists &lt;code&gt;react-native&lt;/code&gt;, &lt;code&gt;@react-native&lt;/code&gt; and &lt;code&gt;@react-native-community&lt;/code&gt;, and nothing else. In &lt;code&gt;apps/host/jest.config.js&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;preset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@react-native/jest-preset&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;transformIgnorePatterns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node_modules/(?!((jest-)?react-native|@react-native(-community)?|@react-navigation|react-native-screens)/)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Run it
&lt;/h2&gt;

&lt;p&gt;Four terminals now, one per remote, one for the host, plus the build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;apps/list &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run start:remote      &lt;span class="c"&gt;# :8082&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;apps/party &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run start:remote     &lt;span class="c"&gt;# :8083&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;apps/host &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm start                 &lt;span class="c"&gt;# :8081&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;apps/host &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run ios
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The host boots on the Pokédex tab and renders the &lt;code&gt;list&lt;/code&gt; remote. Press &lt;strong&gt;Party&lt;/strong&gt; and watch the 8083 terminal: its first request arrives at that moment, not at launch. The host fetches the container, runs it, and the empty grid appears.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🎞️ &lt;strong&gt;Animated demo:&lt;/strong&gt; &lt;a href="https://warrendeleon.com/blog/host-shell-federated-tabs-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-host-shell#run-it" rel="noopener noreferrer"&gt;watch it on warrendeleon.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Two features, built and served by two separate apps, sitting in one tab bar that belongs to neither of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Now break it
&lt;/h2&gt;

&lt;p&gt;The host and the remote agree on one name in two separate files, and nothing checks that they still match. Break the agreement on purpose. In &lt;code&gt;apps/party/rspack.config.mjs&lt;/code&gt;, rename the container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Repack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ModuleFederationPluginV2&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;partyRemote&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;// the host still asks for partyApp&lt;/span&gt;
  &lt;span class="na"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;partyApp.container.js.bundle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Restart the party dev server. An rspack config change doesn't hot-reload, so a plain refresh keeps serving the old manifest and hides the fault. Then relaunch the app and press Party:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ Federation Runtime ]: Unhandled error. ([ScriptManager] Failed while resolving script locator:)
while loading "./PartyScreen" from webpack/container/reference/partyApp
  args: [ { scriptId: 'partyRemote', caller: undefined } ]
  originalError: [Error: No resolver was able to resolve script partyRemote]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read it backwards and the chain is clear. The host asked for &lt;code&gt;partyApp&lt;/code&gt;, because that's the key in its &lt;code&gt;remotes&lt;/code&gt; map. It fetched the manifest at 8083, and the manifest declared a container called &lt;code&gt;partyRemote&lt;/code&gt;. The runtime then looked for a script by that name, and the host has no &lt;code&gt;partyRemote&lt;/code&gt; registered, so its resolver has nothing to match. Nothing was missing and nothing was offline. The rename was applied in one of the two files that have to agree, not both.&lt;/p&gt;

&lt;p&gt;Notice where the failure surfaces. You get a red box, not the spinner: an uncaught error rather than a tab that waits forever. &lt;code&gt;Suspense&lt;/code&gt; handles a promise that hasn't settled yet; it doesn't catch one that rejects. Nothing in this app watches for that, which is fine on a laptop with two dev servers and much less fine when the remote comes off a CDN (a content delivery network). Catching it properly needs an error boundary around the lazy tab. The series does not give that its own post, so treat it as work this shell still owes: the fallback and recovery story arrives with CDN delivery, where a missing remote stops being a laptop problem.&lt;/p&gt;

&lt;p&gt;Put the name back, restart the server, press Party, and the grid returns.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you built, and what's next
&lt;/h2&gt;

&lt;p&gt;The host is a shell now. It owns navigation and the tab bar; each tab is a remote that was built and deployed on its own and loaded at runtime. The remotes stay simple: they render a screen and know nothing about how they are arranged. A third feature is a third remote and a third tab, with no change to the ones already shipped.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Land exactly on this post's finished state.&lt;/strong&gt; The walkthrough prints the load-bearing files; manifests, configs and the smaller edits live in the tag. To finish with a tree byte-equal to the tag, sweep the reference copy over yours. A file you typed correctly is overwritten with itself, and the sweep fills in what the prose did not print:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx degit@3.8.0 &lt;span class="nt"&gt;--force&lt;/span&gt; warrendeleon/react-native-module-federation#post-04-host-shell /tmp/pokedex-ref-04
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; /tmp/pokedex-ref-04/. &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The finished code for this post is the &lt;code&gt;post-04-host-shell&lt;/code&gt; tag, if you cloned rather than building along:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git checkout post-04-host-shell
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://warrendeleon.com/blog/typing-the-seam-between-remotes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-host-shell" rel="noopener noreferrer"&gt;Next in the series&lt;/a&gt;: both tabs will want to open the same Pokémon detail screen. A screen two teams share is a component, not another deployable. The first thing two independently built apps have to agree on is what to pass it. Neither side can own that agreement, so it becomes a package: a contract, published and installed by version.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://reactnavigation.org/" rel="noopener noreferrer"&gt;React Navigation&lt;/a&gt; — the bottom tab navigator the host shell is built on&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://module-federation.io/" rel="noopener noreferrer"&gt;Module Federation 2.0&lt;/a&gt; — the &lt;code&gt;name@url&lt;/code&gt; remotes that load each tab at runtime&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/software-mansion/react-native-screens" rel="noopener noreferrer"&gt;react-native-screens&lt;/a&gt; — the native dependency React Navigation builds on&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/warrendeleon/react-native-module-federation" rel="noopener noreferrer"&gt;react-native-module-federation&lt;/a&gt; — the companion repo, at the tag &lt;code&gt;post-04-host-shell&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>reactnative</category>
      <category>modulefederation</category>
      <category>repack</category>
      <category>rspack</category>
    </item>
    <item>
      <title>The shared-singleton contract in React Native Module Federation</title>
      <dc:creator>Warren de Leon</dc:creator>
      <pubDate>Mon, 22 Jun 2026 12:40:25 +0000</pubDate>
      <link>https://dev.to/warrendeleon/the-shared-singleton-contract-in-react-native-module-federation-4382</link>
      <guid>https://dev.to/warrendeleon/the-shared-singleton-contract-in-react-native-module-federation-4382</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📚 &lt;strong&gt;React Native Module Federation&lt;/strong&gt; series — &lt;a href="https://warrendeleon.com/blog/series/react-native-module-federation/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-shared-singleton" rel="noopener noreferrer"&gt;read it in full on warrendeleon.com&lt;/a&gt;, where new parts land first.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The &lt;a href="https://warrendeleon.com/blog/your-first-federated-remote-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-shared-singleton" rel="noopener noreferrer"&gt;last post&lt;/a&gt; ended by pointing here: the shared-singleton contract, and the mistake that crashes the app on launch. This post covers both. What &lt;code&gt;shared&lt;/code&gt; actually means, the three options that control it, and the failure a remote hits when it breaks the contract on a library with a native side. That failure is loud, immediate, and names itself, which makes it one of the easier ones to fix.&lt;/p&gt;

&lt;p&gt;We pick up exactly where post 2 left off. If you built along then, stay on your own code. If not, start from post 2's finished state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/warrendeleon/react-native-module-federation
&lt;span class="nb"&gt;cd &lt;/span&gt;react-native-module-federation
git checkout post-02-first-remote
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What "shared" actually meant in post 2
&lt;/h2&gt;

&lt;p&gt;Post 2 declared &lt;code&gt;react&lt;/code&gt; and &lt;code&gt;react-native&lt;/code&gt; as shared singletons and moved on. Here is the host's half of that, from &lt;code&gt;apps/host/rspack.config.mjs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;react&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;singleton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;eager&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;requiredVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dependencies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;react&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-native&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;singleton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;eager&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;requiredVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dependencies&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-native&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three options do the work, and each one answers a different question.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;singleton: true&lt;/code&gt; answers "how many copies may exist at runtime?"&lt;/strong&gt; One. When the host and the remote both ask for &lt;code&gt;react&lt;/code&gt;, Module Federation hands them the same instance instead of letting each load its own. This is the load-bearing option. React keeps its hooks state in module-level variables, so two copies of React in one app means two separate piles of state, and any hook called against the wrong pile throws.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;eager: true&lt;/code&gt; answers "is this copy ready before the app's first line runs?"&lt;/strong&gt; On the host, yes. A normal Module Federation entry is asynchronous: it sets up the share scope, then starts your code. React Native does not give you that gap. Its entry is synchronous, so the host marks its shared copies &lt;code&gt;eager&lt;/code&gt; to load them into the share scope up front, before &lt;code&gt;AppRegistry&lt;/code&gt; renders anything. The remote does not need &lt;code&gt;eager&lt;/code&gt;, because by the time it loads, the host has already filled the scope.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;requiredVersion&lt;/code&gt; answers "which versions count as the same?"&lt;/strong&gt; It pins the acceptable range. Stating it explicitly is a habit worth keeping, but leaving it out is not the same as switching the check off. The one case where the check really does disappear is the failure this post ends on.&lt;/p&gt;

&lt;p&gt;So far this is post 2 with the reasoning filled in. The contract starts to matter the moment a remote depends on a third library, not just React.&lt;/p&gt;

&lt;h2&gt;
  
  
  A real dependency: the safe area
&lt;/h2&gt;

&lt;p&gt;React Native's built-in &lt;code&gt;SafeAreaView&lt;/code&gt; is deprecated. The maintained replacement is &lt;a href="https://github.com/AppAndFlow/react-native-safe-area-context" rel="noopener noreferrer"&gt;react-native-safe-area-context&lt;/a&gt;, and it ships with the current React Native template, so both apps in the repo already have it. It is a good test of the contract because it works through React context: a &lt;code&gt;SafeAreaProvider&lt;/code&gt; mounted near the root measures the device's safe area, and any component below reads it with &lt;code&gt;useSafeAreaInsets&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In a federated app, the provider and the consumer live in different bundles. The host owns the shell, so the host mounts the provider. Rewrite &lt;code&gt;apps/host/App.tsx&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Suspense&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ActivityIndicator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;StyleSheet&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-native&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;SafeAreaProvider&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-native-safe-area-context&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PokedexScreen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lazy&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;listApp/PokedexScreen&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;App&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;SafeAreaProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Suspense&lt;/span&gt; &lt;span class="na"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ActivityIndicator&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;loader&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"large"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PokedexScreen&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Suspense&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;SafeAreaProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;styles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;StyleSheet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The host no longer pads the screen itself. It provides the safe-area context and hands the whole canvas to the remote. Now the remote reads the inset and keeps its own title clear of the notch. Update &lt;code&gt;apps/list/src/PokedexScreen.tsx&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;FlatList&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;StyleSheet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;View&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-native&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useSafeAreaInsets&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-native-safe-area-context&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;POKEMON&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bulbasaur&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Charmander&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Squirtle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Pikachu&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;133&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Eevee&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;PokedexScreen&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;insets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSafeAreaInsets&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;paddingTop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;insets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;top&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Pokédex&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subtitle&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Served by the list remote&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FlatList&lt;/span&gt;
        &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;POKEMON&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;keyExtractor&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;renderItem&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;#&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;padStart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;styles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;StyleSheet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#fff&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;fontSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;fontWeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;700&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;subtitle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;fontSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#6b7280&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;marginBottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;row&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;flexDirection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;row&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;paddingVertical&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;borderBottomWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;StyleSheet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hairlineWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;borderBottomColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#e5e7eb&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;number&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;56&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#9ca3af&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;fontVariant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tabular-nums&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;fontSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;fontWeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;500&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is now a context handshake that crosses the bundle boundary: the provider is in the host, the &lt;code&gt;useSafeAreaInsets&lt;/code&gt; call is in the remote. For that to connect, both apps need the &lt;em&gt;same&lt;/em&gt; &lt;code&gt;SafeAreaProvider&lt;/code&gt;, from the same copy of the library. A React context is identified by the object that creates it. Two copies of the library make two different context objects, and a consumer reading copy B will never see a provider mounted from copy A.&lt;/p&gt;

&lt;p&gt;That is what the contract is for. Add the library to &lt;code&gt;shared&lt;/code&gt; in both configs as a singleton. The host (&lt;code&gt;apps/host/rspack.config.mjs&lt;/code&gt;), eager like its other shared copies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;react&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;singleton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;eager&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;requiredVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dependencies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;react&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-native&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;singleton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;eager&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;requiredVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dependencies&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-native&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-native-safe-area-context&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;singleton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;eager&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;requiredVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dependencies&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-native-safe-area-context&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the remote (&lt;code&gt;apps/list/rspack.config.mjs&lt;/code&gt;), singleton but not eager:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;react&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;singleton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;requiredVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dependencies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;react&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-native&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;singleton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;requiredVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dependencies&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-native&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-native-safe-area-context&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;singleton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;requiredVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dependencies&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-native-safe-area-context&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start both dev servers and run the host (the &lt;a href="https://warrendeleon.com/blog/your-first-federated-remote-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-shared-singleton" rel="noopener noreferrer"&gt;three-terminal routine from post 2&lt;/a&gt;). The Pokédex renders with its title sitting below the Dynamic Island, padded by the inset the remote read from the host's provider. One library, one provider, one context object, shared across two apps that were built and shipped on their own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Now break it
&lt;/h2&gt;

&lt;p&gt;Delete one entry. Take &lt;code&gt;react-native-safe-area-context&lt;/code&gt; out of the &lt;em&gt;remote's&lt;/em&gt; &lt;code&gt;shared&lt;/code&gt; block, leaving it in the host's. This is the realistic version of the mistake: the host author shared it, the remote author forgot. Restart the remote's dev server and reload the host.&lt;/p&gt;

&lt;p&gt;The app does not render a slightly-wrong screen. It red-boxes on launch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Uncaught Error: Tried to register two views with the same name RNCSafeAreaProvider
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv7vtxz0ctfsvyfytup1a.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv7vtxz0ctfsvyfytup1a.webp" alt="The app red-boxing on launch with the error: Tried to register two views with the same name RNCSafeAreaProvider"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here is why it is loud rather than quiet. &lt;code&gt;react-native-safe-area-context&lt;/code&gt; is not pure JavaScript. It ships a native view, &lt;code&gt;RNCSafeAreaProvider&lt;/code&gt;, that it registers with React Native's view registry at startup. The host's copy registers it once. When the remote drops the share, it bundles its own copy, and that copy tries to register the same native name a second time. React Native keeps one registry per app and refuses the duplicate. The crash fires before a single Pokémon reaches the screen.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📊 &lt;strong&gt;Diagram:&lt;/strong&gt; &lt;a href="https://warrendeleon.com/blog/shared-singleton-contract-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-shared-singleton#now-break-it" rel="noopener noreferrer"&gt;view it on warrendeleon.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is the pattern for a library whose JavaScript registers something with the native layer, the way safe-area context registers its provider view: a navigation library, a gesture handler, anything that calls &lt;code&gt;requireNativeComponent&lt;/code&gt; or keeps native-facing identity in module scope. Share it from one place and it works. Let two bundles each carry their own and both copies register the same native view, and they collide early and obviously. Not every native-backed library fails like this: a thin wrapper that only &lt;em&gt;calls&lt;/em&gt; into a native module can survive duplication, because nothing identity-shaped collides. The ones that break register views or hold state, and the error even names the view, so the fix points back at the missing share.&lt;/p&gt;

&lt;p&gt;Put that entry back in the remote's &lt;code&gt;shared&lt;/code&gt; block, and the app builds and runs again.&lt;/p&gt;

&lt;p&gt;React itself fails just as loudly, for a different reason. Drop &lt;code&gt;react&lt;/code&gt; from a remote's &lt;code&gt;shared&lt;/code&gt; and the remote bundles its own React. The first hook the remote runs gets checked against the wrong copy, and you get the familiar &lt;code&gt;Invalid hook call&lt;/code&gt; red box. Same lesson: a library that keeps identity in module scope does not survive being loaded twice. That is a property of those libraries rather than a rule the runtime enforces everywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure that does stay quiet
&lt;/h2&gt;

&lt;p&gt;One case earns the "quiet" label, and it is &lt;code&gt;requiredVersion&lt;/code&gt;, though not the way you might expect. Drop the field and, most of the time, nothing changes: Module Federation falls back to the version recorded for that dependency in the consuming package's own &lt;code&gt;package.json&lt;/code&gt;, so a range still gets checked. The quiet failure needs one more ingredient: no version for the bundler to fall back on. That happens when the shared package is missing from the consumer's &lt;code&gt;package.json&lt;/code&gt;, or when the package's layout stops the bundler reading a version for it (a trap &lt;a href="https://warrendeleon.com/blog/typing-the-seam-between-remotes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-shared-singleton" rel="noopener noreferrer"&gt;the contract-package post&lt;/a&gt; walks straight into). With no range from either source, Module Federation has nothing to check: it loads whichever copy wins and runs on. That is the one to be careful with, because it builds and ships clean, and only shows up once two teams end up on different versions of a dependency. Pin &lt;code&gt;requiredVersion&lt;/code&gt; explicitly, as the configs above do, and the check stops depending on what the bundler managed to infer.&lt;/p&gt;

&lt;p&gt;So the general rule is the reassuring one. Most ways of breaking the shared contract announce themselves on launch. The quiet one is narrow, and a single field closes it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you built, and what's next
&lt;/h2&gt;

&lt;p&gt;The host owns one &lt;code&gt;SafeAreaProvider&lt;/code&gt;. The remote reads its insets across the bundle boundary, because both apps resolve to one shared copy of the library. You saw the contract hold, then watched it crash when a remote forgot its half, and you know now that the crash is the friendly outcome.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Land exactly on this post's finished state.&lt;/strong&gt; The walkthrough prints the load-bearing files; manifests, configs and the smaller edits live in the tag. To finish with a tree byte-equal to the tag, sweep the reference copy over yours. A file you typed correctly is overwritten with itself, and the sweep fills in what the prose did not print:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx degit@3.8.0 &lt;span class="nt"&gt;--force&lt;/span&gt; warrendeleon/react-native-module-federation#post-03-shared-singleton /tmp/pokedex-ref-03
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; /tmp/pokedex-ref-03/. &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://warrendeleon.com/blog/host-shell-federated-tabs-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-shared-singleton" rel="noopener noreferrer"&gt;Next in the series&lt;/a&gt;: the host stops being a single screen and becomes a real shell, owning the tab bar while each tab is a remote loaded at runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/AppAndFlow/react-native-safe-area-context" rel="noopener noreferrer"&gt;react-native-safe-area-context&lt;/a&gt;: the maintained safe-area library, and the &lt;code&gt;RNCSafeAreaProvider&lt;/code&gt; native view in the crash&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://module-federation.io/" rel="noopener noreferrer"&gt;Module Federation 2.0&lt;/a&gt;: the &lt;code&gt;shared&lt;/code&gt; contract: &lt;code&gt;singleton&lt;/code&gt;, &lt;code&gt;eager&lt;/code&gt;, and &lt;code&gt;requiredVersion&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/warrendeleon/react-native-module-federation" rel="noopener noreferrer"&gt;react-native-module-federation&lt;/a&gt;: the companion repo, at the tag &lt;code&gt;post-03-shared-singleton&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>reactnative</category>
      <category>modulefederation</category>
      <category>repack</category>
      <category>rspack</category>
    </item>
    <item>
      <title>Your first federated remote in React Native</title>
      <dc:creator>Warren de Leon</dc:creator>
      <pubDate>Mon, 22 Jun 2026 12:39:49 +0000</pubDate>
      <link>https://dev.to/warrendeleon/your-first-federated-remote-in-react-native-4omg</link>
      <guid>https://dev.to/warrendeleon/your-first-federated-remote-in-react-native-4omg</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📚 &lt;strong&gt;React Native Module Federation&lt;/strong&gt; series — &lt;a href="https://warrendeleon.com/blog/series/react-native-module-federation/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-first-remote" rel="noopener noreferrer"&gt;read it in full on warrendeleon.com&lt;/a&gt;, where new parts land first.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The &lt;a href="https://warrendeleon.com/blog/why-module-federation-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-first-remote" rel="noopener noreferrer"&gt;first post&lt;/a&gt; made the case for Module Federation. This one builds the smallest real version of it: two separate React Native apps, where one app (the host) loads a screen from the other (a remote) while it's running. We go from an empty folder to a running app, every step shown.&lt;/p&gt;

&lt;p&gt;The finished code is in the companion repo at the tag for this post, so you can clone it and diff against your own if something drifts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/warrendeleon/react-native-module-federation
&lt;span class="nb"&gt;cd &lt;/span&gt;react-native-module-federation
git checkout post-02-first-remote
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is what you'll have at the end. The list of Pokémon on screen lives in, and is served by, a &lt;em&gt;different app&lt;/em&gt; from the one running:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj5un5fhgg53jvzerqg1c.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj5un5fhgg53jvzerqg1c.webp" alt="The host app showing a Pokédex screen served by the list remote" width="720" height="1566"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Two apps
&lt;/h2&gt;

&lt;p&gt;A federation needs a host and at least one remote. Create two fresh React Native apps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;react-native-module-federation &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;react-native-module-federation
npx @react-native-community/cli@20.1.0 init Host &lt;span class="nt"&gt;--directory&lt;/span&gt; apps/host &lt;span class="nt"&gt;--version&lt;/span&gt; 0.85.3
npx @react-native-community/cli@20.1.0 init List &lt;span class="nt"&gt;--directory&lt;/span&gt; apps/list &lt;span class="nt"&gt;--version&lt;/span&gt; 0.85.3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The versions are pinned on purpose: this series is built and verified on RN 0.85.3 with Re.Pack 5.2.5, on the iOS simulator. A newer RN scaffold will probably work, but you'd be first to find out. (Android runs the same configs, with one wrinkle: the &lt;code&gt;localhost&lt;/code&gt; manifest URLs need &lt;code&gt;adb reverse&lt;/code&gt;, or &lt;code&gt;10.0.2.2&lt;/code&gt; in their place.)&lt;/p&gt;

&lt;p&gt;&lt;code&gt;host&lt;/code&gt; is &lt;a href="https://warrendeleon.com/blog/host-shell-federated-tabs-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-first-remote" rel="noopener noreferrer"&gt;the shell&lt;/a&gt; the user launches. &lt;code&gt;list&lt;/code&gt; is a feature that will be loaded into it at runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put both on Re.Pack
&lt;/h2&gt;

&lt;p&gt;React Native ships with Metro. Module Federation 2.0 runs on &lt;a href="https://re-pack.dev/" rel="noopener noreferrer"&gt;Re.Pack&lt;/a&gt; (Rspack underneath), so the first job is to swap the bundler. The steps are identical in both apps.&lt;/p&gt;

&lt;p&gt;Install the bundler and the federation packages in each app, from the repo root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/host &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; @callstack/repack@5.2.5 @rspack/core@2.0.5 @module-federation/enhanced@2.5.0 @swc/helpers@0.5.23 @react-native-community/cli@20.2.0 @react-native-community/cli-platform-android@20.2.0 @react-native-community/cli-platform-ios@20.2.0 &lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/list &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; @callstack/repack@5.2.5 @rspack/core@2.0.5 @module-federation/enhanced@2.5.0 @swc/helpers@0.5.23 @react-native-community/cli@20.2.0 @react-native-community/cli-platform-android@20.2.0 @react-native-community/cli-platform-ios@20.2.0 &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An unpinned install answers to whatever the registry serves on the day you run it, which is why every version here is named. The CLI trio sits past the version the scaffold installs, which clears the fixable &lt;code&gt;npm audit&lt;/code&gt; advisories the template ships with.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;&lt;code&gt;@swc/helpers&lt;/code&gt; is the easy one to miss.&lt;/strong&gt; Re.Pack compiles your code with SWC (the Speedy Web Compiler, a Rust-based alternative to Babel that it uses underneath). When SWC compiles modern syntax down, it emits &lt;code&gt;require("@swc/helpers/…")&lt;/code&gt; calls to a small shared helper library rather than inlining the same boilerplate everywhere. Miss the package and the build fails with a screen of "can't resolve" errors that give no hint of the real cause.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now point the React Native CLI at Re.Pack. In &lt;strong&gt;both&lt;/strong&gt; apps, add &lt;code&gt;react-native.config.js&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// apps/host/react-native.config.js  AND  apps/list/react-native.config.js&lt;/span&gt;
&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@callstack/repack/commands/rspack&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That single file is what makes &lt;code&gt;react-native start&lt;/code&gt; and &lt;code&gt;react-native bundle&lt;/code&gt; use Rspack instead of Metro. The bundler is swapped. The host and the remote differ in the &lt;code&gt;rspack.config.mjs&lt;/code&gt; each one gets, and that file is where federation is configured.&lt;/p&gt;

&lt;h2&gt;
  
  
  The remote: expose a screen
&lt;/h2&gt;

&lt;p&gt;In this project a remote is an app with no &lt;code&gt;AppRegistry.registerComponent&lt;/code&gt;. That is a choice about how these remotes are built rather than a definition of the term: a remote can register a root component and run standalone too, and some setups keep that for development. Here it doesn't boot itself, it waits to be pulled into a host, and it declares a name and what it hands out.&lt;/p&gt;

&lt;p&gt;First the screen it hands out, &lt;code&gt;apps/list/src/PokedexScreen.tsx&lt;/code&gt;. Plain React Native on purpose, this post is about loading it, not styling it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;FlatList&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;StyleSheet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;View&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-native&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;POKEMON&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bulbasaur&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Charmander&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Squirtle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Pikachu&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;133&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Eevee&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;PokedexScreen&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Pokédex&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subtitle&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Served by the list remote&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FlatList&lt;/span&gt;
        &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;POKEMON&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;keyExtractor&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;renderItem&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;#&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;padStart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;styles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;StyleSheet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#fff&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;fontSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;fontWeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;700&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;subtitle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;fontSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#6b7280&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;marginBottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;row&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;flexDirection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;row&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;paddingVertical&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;borderBottomWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;StyleSheet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hairlineWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;borderBottomColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#e5e7eb&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;number&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;56&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#9ca3af&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;fontVariant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tabular-nums&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;fontSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;fontWeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;500&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The container's entry point, &lt;code&gt;apps/list/src/index.js&lt;/code&gt;, is almost empty. A remote registers no root component, so it has nothing to do at startup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// apps/list/src/index.js&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the config, &lt;code&gt;apps/list/rspack.config.mjs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:path&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;fileURLToPath&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Repack&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@callstack/repack&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;pkg&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./package.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="kd"&gt;with&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;__dirname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dirname&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;fileURLToPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;Repack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;defineRspackConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;platform&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./src/index.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Lets the resolver read each package's `exports` map, which the Module Federation&lt;/span&gt;
      &lt;span class="c1"&gt;// runtime needs for subpath imports like '@module-federation/runtime/helpers'.&lt;/span&gt;
      &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;Repack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getResolveOptions&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;enablePackageExports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/build/[platform]`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;uniqueName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ListApp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;module&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\.[&lt;/span&gt;&lt;span class="sr"&gt;cm&lt;/span&gt;&lt;span class="se"&gt;]?[&lt;/span&gt;&lt;span class="sr"&gt;jt&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;sx&lt;/span&gt;&lt;span class="se"&gt;?&lt;/span&gt;&lt;span class="sr"&gt;$/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;javascript/auto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;use&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@callstack/repack/babel-swc-loader&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;parallel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;Repack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAssetTransformRules&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Repack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;RepackPlugin&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;extraChunks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/.*/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;remote&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;outputPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`build/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/remote`&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="p"&gt;}),&lt;/span&gt;
      &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Repack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ModuleFederationPluginV2&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;listApp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;listApp.container.js.bundle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;exposes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./PokedexScreen&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./src/PokedexScreen.tsx&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;dts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;react&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;singleton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;requiredVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dependencies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;react&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-native&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;singleton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;requiredVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dependencies&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-native&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
          &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things in there matter. &lt;code&gt;exposes&lt;/code&gt; maps a public key, &lt;code&gt;./PokedexScreen&lt;/code&gt;, to a file. That key is the remote's entire public surface. &lt;code&gt;shared&lt;/code&gt; declares react and react-native as singletons, so the remote renders against the host's one copy instead of bundling its own (two Reacts in one runtime would break hooks). And &lt;code&gt;enablePackageExports: true&lt;/code&gt; is not optional: without it the federation runtime can't resolve its own subpath imports and the build fails.&lt;/p&gt;

&lt;p&gt;Add a dev-server script to &lt;code&gt;apps/list/package.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"start:remote"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"react-native start --config rspack.config.mjs --port 8082"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;apps/list &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run start:remote
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It serves a manifest at &lt;code&gt;http://localhost:8082/ios/mf-manifest.json&lt;/code&gt; describing the container and the screen it exposes. Open that URL and you'll see &lt;code&gt;./PokedexScreen&lt;/code&gt; listed. The remote renders nothing on its own, it's a feature waiting for an app.&lt;/p&gt;

&lt;h2&gt;
  
  
  The host: load the remote
&lt;/h2&gt;

&lt;p&gt;The host is an ordinary React Native app. Its &lt;code&gt;apps/host/rspack.config.mjs&lt;/code&gt; consumes the remote:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:path&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;fileURLToPath&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Repack&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@callstack/repack&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;pkg&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./package.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="kd"&gt;with&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;__dirname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dirname&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;fileURLToPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;Repack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;defineRspackConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;platform&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./index.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;Repack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getResolveOptions&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;enablePackageExports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/build/[platform]`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;uniqueName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Host&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;module&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\.[&lt;/span&gt;&lt;span class="sr"&gt;cm&lt;/span&gt;&lt;span class="se"&gt;]?[&lt;/span&gt;&lt;span class="sr"&gt;jt&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;sx&lt;/span&gt;&lt;span class="se"&gt;?&lt;/span&gt;&lt;span class="sr"&gt;$/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;javascript/auto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;use&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@callstack/repack/babel-swc-loader&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;parallel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;Repack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAssetTransformRules&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Repack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;RepackPlugin&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Repack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ModuleFederationPluginV2&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;host&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;host.container.js.bundle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;remotes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="c1"&gt;// name@url: the host knows listApp lives at this manifest URL. In dev that is the&lt;/span&gt;
          &lt;span class="c1"&gt;// remote's own dev server on :8082.&lt;/span&gt;
          &lt;span class="na"&gt;listApp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`listApp@http://localhost:8082/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/mf-manifest.json`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;dts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;react&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;singleton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;eager&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;requiredVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dependencies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;react&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-native&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;singleton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;eager&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;requiredVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dependencies&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-native&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
          &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;name@url&lt;/code&gt; line is the whole wiring: the host knows a remote called &lt;code&gt;listApp&lt;/code&gt; sits at that manifest URL. The host's &lt;code&gt;shared&lt;/code&gt; adds &lt;code&gt;eager: true&lt;/code&gt;, because the host is the one copy everyone renders against, and &lt;code&gt;eager&lt;/code&gt; makes the share scope ready before the synchronous app entry runs, so no bootstrap file is needed.&lt;/p&gt;

&lt;p&gt;Now load it. Replace &lt;code&gt;apps/host/App.tsx&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Suspense&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ActivityIndicator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;StyleSheet&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-native&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;SafeAreaProvider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;SafeAreaView&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-native-safe-area-context&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PokedexScreen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lazy&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;listApp/PokedexScreen&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;App&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;SafeAreaProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;SafeAreaView&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Suspense&lt;/span&gt; &lt;span class="na"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ActivityIndicator&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;loader&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"large"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PokedexScreen&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Suspense&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;SafeAreaView&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;SafeAreaProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;styles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;StyleSheet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;root&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;listApp/PokedexScreen&lt;/code&gt; is not a package on disk. It's the &lt;code&gt;listApp&lt;/code&gt; from the host's &lt;code&gt;remotes&lt;/code&gt;, then the &lt;code&gt;./PokedexScreen&lt;/code&gt; that remote exposed. At runtime, Module Federation turns that import into "fetch listApp's container from its URL, start it, return its &lt;code&gt;PokedexScreen&lt;/code&gt; export". Because it's a dynamic import returning a promise, it fits directly into &lt;code&gt;React.lazy&lt;/code&gt; with a &lt;code&gt;Suspense&lt;/code&gt; spinner while the remote downloads.&lt;/p&gt;

&lt;p&gt;TypeScript doesn't know that specifier, so tell it the shape. Add &lt;code&gt;apps/host/mf-modules.d.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;declare&lt;/span&gt; &lt;span class="kr"&gt;module&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;listApp/PokedexScreen&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PokedexScreen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ComponentType&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;PokedexScreen&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  ScriptManager: the part that's different on native
&lt;/h2&gt;

&lt;p&gt;Everything above would look familiar to anyone who's done Module Federation on the web. React Native is where it diverges.&lt;/p&gt;

&lt;p&gt;On the web, &lt;code&gt;import('listApp/PokedexScreen')&lt;/code&gt; ends in the browser fetching a script over HTTP and the engine running it. A browser does that constantly. Loading code from a URL is routine for it. A React Native runtime has no equivalent. No DOM, no &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag, no built-in way to pull in more code on demand once the app has booted. A standard RN app is one self-contained bundle, loaded at launch, with nothing in it that knows how to go and fetch another chunk later.&lt;/p&gt;

&lt;p&gt;Re.Pack fills that gap with ScriptManager: the piece that turns a request the federation runtime makes ("I need listApp's container") into the real steps: work out the URL, fetch the script, hand it to the engine to run, cache it. On native, every federated import goes through it.&lt;/p&gt;

&lt;p&gt;The good news for this post: in dev you write none of it. The Module Federation plugin you already added auto-wires ScriptManager and a default resolver that knows how to reach the remote's dev server. So the full loop is just:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📊 &lt;strong&gt;Diagram:&lt;/strong&gt; &lt;a href="https://warrendeleon.com/blog/your-first-federated-remote-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-first-remote#scriptmanager-the-part-thats-different-on-native" rel="noopener noreferrer"&gt;view it on warrendeleon.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When you move to production, ScriptManager is where the real work is: resolving versioned CDN URLs, verifying a signature before running anything, falling back to an embedded copy when the network fails. All later in the series. For now it's enough to know it's the bridge between "import a remote" and "code arrives over the wire and runs" that the browser gave web federation for free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run it
&lt;/h2&gt;

&lt;p&gt;The host is the only app whose native project gets built; the remote keeps its &lt;code&gt;ios/&lt;/code&gt; folder from the scaffold, but nothing ever compiles it. So pods are installed for the host only:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;apps/host/ios &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; bundle &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; bundle &lt;span class="nb"&gt;exec &lt;/span&gt;pod &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, in three terminals:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. the remote's dev server (leave the one from earlier running, or start it)&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;apps/list &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run start:remote     &lt;span class="c"&gt;# :8082&lt;/span&gt;

&lt;span class="c"&gt;# 2. the host's dev server&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;apps/host &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm start                &lt;span class="c"&gt;# :8081&lt;/span&gt;

&lt;span class="c"&gt;# 3. build and launch the host on a simulator&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;apps/host &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run ios
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The host boots, shows the spinner briefly while it fetches &lt;code&gt;listApp&lt;/code&gt; from &lt;code&gt;:8082&lt;/code&gt;, then renders the Pokédex screen, served by a separately built app. Separately built and separately deployed, but not separately installed: what ships on its own is the remote's JavaScript bundle, and it runs inside the host's binary.&lt;/p&gt;

&lt;p&gt;To prove they really are separate, stop the list dev server and reload the host. The screen can't load. Making that degrade to something safe, an offline copy built into the host, is its own post later in the series; &lt;a href="https://warrendeleon.com/blog/one-shared-store-across-remotes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-first-remote" rel="noopener noreferrer"&gt;the shared store post&lt;/a&gt; meets a softer version of the same failure much sooner. Right now the basic loop is what matters, and it runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you built, and what's still minimal
&lt;/h2&gt;

&lt;p&gt;You have two apps that build and deploy on their own, joined at runtime. The host imports a screen by name, and the code arrives over the network and runs inside it. The remote compiled nothing into the host.&lt;/p&gt;

&lt;p&gt;Two things were kept deliberately minimal, each its own post:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The shared libraries.&lt;/strong&gt; react and react-native are shared so the remote renders against the host's copy. The full contract, eager versus lazy, version skew, and the mistake that crashes the app on launch, is &lt;a href="https://warrendeleon.com/blog/shared-singleton-contract-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-first-remote" rel="noopener noreferrer"&gt;the next post&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Everything production needs.&lt;/strong&gt; Versioned CDN loads, signing, an offline fallback, and what happens when a remote is gone. The second half of the series.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Land exactly on this post's finished state.&lt;/strong&gt; The walkthrough prints the load-bearing files; manifests, configs and the smaller edits live in the tag. To finish with a tree byte-equal to the tag, sweep the reference copy over yours. A file you typed correctly is overwritten with itself, and the sweep fills in what the prose did not print:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx degit@3.8.0 &lt;span class="nt"&gt;--force&lt;/span&gt; warrendeleon/react-native-module-federation#post-02-first-remote /tmp/pokedex-ref-02
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; /tmp/pokedex-ref-02/. &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://re-pack.dev/" rel="noopener noreferrer"&gt;Re.Pack&lt;/a&gt;: the React Native bundler that wraps Rspack and provides ScriptManager and Module Federation support&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://module-federation.io/" rel="noopener noreferrer"&gt;Module Federation 2.0&lt;/a&gt;: the runtime architecture behind &lt;code&gt;name@url&lt;/code&gt; remotes and &lt;code&gt;exposes&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/warrendeleon/react-native-module-federation" rel="noopener noreferrer"&gt;react-native-module-federation&lt;/a&gt;: the companion repo, at the tag &lt;code&gt;post-02-first-remote&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>reactnative</category>
      <category>modulefederation</category>
      <category>repack</category>
      <category>rspack</category>
    </item>
  </channel>
</rss>
