<?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>shell.navigateTo: native screens from a federated remote in React Native</title>
      <dc:creator>Warren de Leon</dc:creator>
      <pubDate>Mon, 21 Sep 2026 00:05:30 +0000</pubDate>
      <link>https://dev.to/warrendeleon/shellnavigateto-native-screens-from-a-federated-remote-in-react-native-2knl</link>
      <guid>https://dev.to/warrendeleon/shellnavigateto-native-screens-from-a-federated-remote-in-react-native-2knl</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-native-handoff" 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/accessibility-testing-federated-remotes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-native-handoff" rel="noopener noreferrer"&gt;Post 12&lt;/a&gt; ended with the host offering the remotes a loan: its native side, through a bridge that launches a native screen from the Party tab and waits for the result. This post builds the bridge. A button in a federated tab opens a fully native screen, SwiftUI on one platform and Jetpack Compose on the other, and the battle's winner returns as one promise's resolution.&lt;/p&gt;

&lt;p&gt;The constraint underneath has been in the series since &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-native-handoff" rel="noopener noreferrer"&gt;post 1&lt;/a&gt;: runtime delivery moves JavaScript and nothing else. &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-native-handoff" rel="noopener noreferrer"&gt;Post 11&lt;/a&gt; met it at the design system, and Re.Pack's limitations list states the host's half of the deal: "Host application must have native modules used in other containers". A remote that wants a native screen cannot bring one. The host lends its own.&lt;/p&gt;

&lt;p&gt;The loan's shape: &lt;strong&gt;a remote sees exactly one function&lt;/strong&gt;, &lt;code&gt;shellNavigate(destination, params?)&lt;/code&gt; from the contract, returning a promise. The title's &lt;code&gt;shell.navigateTo&lt;/code&gt; is this function, under the contract's exported name. Behind it sit the host's routing table and the blog's first TurboModule (React Native's typed native-module system), with its first Swift and Kotlin beyond the template. The round trip:&lt;/p&gt;

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

&lt;p&gt;One practical difference before the first command. Until now the clone was a convenience: everything was typed, and a tree built by hand since &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-native-handoff" rel="noopener noreferrer"&gt;post 2&lt;/a&gt; served just as well. This post is the first where &lt;strong&gt;the companion repo is required input&lt;/strong&gt;: the native screens arrive from its tag, and by the time a TurboModule registers, your native project has to match the tag's. Start from post 12's finished state, the start 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-12-a11y-testing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The finished state is the end tag, &lt;code&gt;post-13-native-handoff&lt;/code&gt;; a fetch later in the build reaches into it, and everything else you type.&lt;/p&gt;

&lt;h2&gt;
  
  
  One function in the contract
&lt;/h2&gt;

&lt;p&gt;The contract gains one file, the remote's entire view of the native world. Create &lt;code&gt;packages/contracts/src/shellNavigation.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="kd"&gt;type&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="k"&gt;from&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="c1"&gt;// --- The shell's routing surface, and the whole of what a remote knows about native. A remote&lt;/span&gt;
&lt;span class="c1"&gt;// imports `shellNavigate` and nothing else: no TurboModule, no native types, no idea whether the&lt;/span&gt;
&lt;span class="c1"&gt;// destination it names is another micro-app or a fully native screen. The host owns the table that&lt;/span&gt;
&lt;span class="c1"&gt;// decides, so migrating a native flow to React Native later is one row edit here and no change in&lt;/span&gt;
&lt;span class="c1"&gt;// any remote. ---&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;RouteEntry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="cm"&gt;/** Hands off to native: the host calls openNative, a native screen presents, and the promise
   *  resolves with whatever that screen passed back. */&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;native&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;nativeId&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="c1"&gt;// One entry, because one thing dispatches. The union above is written as a union of one so a&lt;/span&gt;
&lt;span class="c1"&gt;// micro-app variant can join it without the readers of `entry.type` changing shape; a registry row&lt;/span&gt;
&lt;span class="c1"&gt;// with nothing behind it is a dead constant, and this series has already paid for one of those.&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;ROUTE_REGISTRY&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;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;RouteEntry&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="na"&gt;QuickBattle&lt;/span&gt;&lt;span class="p"&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;native&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;nativeId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;quickBattle&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="c1"&gt;// --- What crosses the boundary, in both directions. These types live in the contract because the&lt;/span&gt;
&lt;span class="c1"&gt;// HOST has to serialise them, not because another app dispatches anything: the battle's outcome is&lt;/span&gt;
&lt;span class="c1"&gt;// the party's own business and the party's own reducer handles it. ---&lt;/span&gt;

&lt;span class="cm"&gt;/** RN -&amp;gt; native. The party hands over its members; the native screen shows and battles them.
 *
 *  The colour scheme travels with them because it has to. Every federated surface reads the one
 *  styling runtime the host mounts, and a native screen is the one consumer that cannot subscribe
 *  to it: there is no bundle to share and no provider to sit under. So the theme stops being
 *  ambient at this boundary and becomes an argument, sent at the moment of the call. */&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;QuickBattleParams&lt;/span&gt; &lt;span class="kd"&gt;extends&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;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&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;PartyMember&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="nl"&gt;colourScheme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;light&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;dark&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="cm"&gt;/** Native -&amp;gt; RN. The uid, not the id: two copies of the same Pokémon are two contestants, and the
 *  party has stamped a uid on each member since 3.1.0 for exactly that reason. Absent when the
 *  screen closed without a battle. */&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;QuickBattleResult&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;winnerUid&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="cm"&gt;/** Native resolves with an object, or with nothing when the flow ended without a result. */&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;ShellNavigateResult&lt;/span&gt; &lt;span class="o"&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;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ShellNavigateFn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;destination&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="nx"&gt;params&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;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&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="o"&gt;=&amp;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;ShellNavigateResult&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;// --- The slot the host fills and every remote reads. It is a globalThis key rather than a React&lt;/span&gt;
&lt;span class="c1"&gt;// context because this package is bundled into the host AND into each remote: a context created&lt;/span&gt;
&lt;span class="c1"&gt;// here would be a different object in every bundle, so a remote's useContext would never find the&lt;/span&gt;
&lt;span class="c1"&gt;// host's provider. The module identity that posts 3 and 6 made a singleton for is exactly what is&lt;/span&gt;
&lt;span class="c1"&gt;// missing at this seam, and globalThis is the one slot all three bundles genuinely share. ---&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;GLOBAL_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;__POKEDEX_SHELL_NAVIGATE__&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;function&lt;/span&gt; &lt;span class="nf"&gt;registerShellNavigateHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ShellNavigateFn&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;globalThis&lt;/span&gt; &lt;span class="k"&gt;as&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;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&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;GLOBAL_KEY&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fn&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;shellNavigate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;destination&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="nx"&gt;params&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;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&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="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ShellNavigateResult&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;fn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;globalThis&lt;/span&gt; &lt;span class="k"&gt;as&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;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&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;GLOBAL_KEY&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;ShellNavigateFn&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;fn&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;function&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="c1"&gt;// No handler: warn and resolve. A remote that awaits this gets undefined and renders something&lt;/span&gt;
    &lt;span class="c1"&gt;// honest, which is the same tolerance the party slice's read shape is built on.&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="s2"&gt;`[shellNavigate] no handler registered (called with &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;destination&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="nb"&gt;Promise&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="kc"&gt;undefined&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;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;destination&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The comments carry the three decisions: one registry entry, since only one thing dispatches; the theme as an argument, since a native screen cannot subscribe to the styling runtime; and a &lt;code&gt;globalThis&lt;/code&gt; slot rather than a React context, which bundling into every app would make a different object in each. &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-native-handoff" rel="noopener noreferrer"&gt;Post 8&lt;/a&gt;'s prepare-stamped uid gets its first cross-boundary consumer here: the value that leaves the app and comes back.&lt;/p&gt;

&lt;p&gt;React Navigation's &lt;a href="https://reactnavigation.org/docs/native-stack-navigator/" rel="noopener noreferrer"&gt;native stack&lt;/a&gt; deserves naming first, and for native-backed React Native screens inside a navigator it is the right tool. What it does not model is a screen the host owns natively, outside React's tree, taking input from one remote and returning a value to it. That round trip is the bridge's reason to exist.&lt;/p&gt;

&lt;p&gt;Export it, bump, and publish through the same &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-native-handoff" rel="noopener noreferrer"&gt;Verdaccio flow post 5 established&lt;/a&gt;. In &lt;code&gt;packages/contracts/src/index.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;export&lt;/span&gt; &lt;span class="o"&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;./party&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="o"&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;./shellNavigation&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;In &lt;code&gt;packages/contracts/package.json&lt;/code&gt;, &lt;code&gt;"version": "3.3.0"&lt;/code&gt;: an additive minor on post 12's 3.2.2. Two tooling edits come with it: the new file calls &lt;code&gt;console.warn&lt;/code&gt;, so the contracts &lt;code&gt;tsconfig.json&lt;/code&gt; adds &lt;code&gt;"DOM"&lt;/code&gt; to its &lt;code&gt;lib&lt;/code&gt; array as &lt;code&gt;ui&lt;/code&gt; and &lt;code&gt;a11y-testing&lt;/code&gt; already do, and the &lt;code&gt;build&lt;/code&gt; script gains a dist clean so stale declaration files cannot survive into a publish:&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;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node -e &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;require('fs').rmSync('dist',{recursive:true,force:true})&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; &amp;amp;&amp;amp; tsc"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Publish, then take it in all three 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="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;/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="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="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/&lt;span class="nv"&gt;$app&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install&lt;/span&gt; @pokedex/contracts@3.3.0 &lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The spec and the codegen
&lt;/h2&gt;

&lt;p&gt;A TurboModule starts as a TypeScript spec, read at build time by React Native's code generation, which writes the base classes the native implementations extend. Create &lt;code&gt;apps/host/specs/NativeShellNavigationModule.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="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;TurboModule&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;TurboModuleRegistry&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="c1"&gt;// --- The TurboModule spec: the one place the host's JavaScript and its native code agree on a&lt;/span&gt;
&lt;span class="c1"&gt;// signature. Codegen reads this file at build time and writes the C++/ObjC++ and Kotlin base&lt;/span&gt;
&lt;span class="c1"&gt;// classes the native implementations extend, which is why the file lives outside src/ and why&lt;/span&gt;
&lt;span class="c1"&gt;// its name has to start with `Native`.&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// One method, and it is asynchronous by shape: openNative hands a native flow its input and&lt;/span&gt;
&lt;span class="c1"&gt;// resolves with whatever that flow passed back. The promise is the whole design — the party's&lt;/span&gt;
&lt;span class="c1"&gt;// await does not return until the native screen has finished, and every exit path of that screen&lt;/span&gt;
&lt;span class="c1"&gt;// has to settle it exactly once.&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// The boundary is JSON strings in both directions rather than codegen object types. Codegen can&lt;/span&gt;
&lt;span class="c1"&gt;// carry structured objects, and a bigger bridge should let it; a string keeps the serialisation&lt;/span&gt;
&lt;span class="c1"&gt;// visible in one place on each side, which is easier to follow while the bridge is one method&lt;/span&gt;
&lt;span class="c1"&gt;// wide. ---&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;Spec&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;TurboModule&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;openNative&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nativeId&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="nx"&gt;paramsJson&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="nb"&gt;Promise&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="c1"&gt;// getEnforcing throws when no native module answers to the name: a binary built without the&lt;/span&gt;
&lt;span class="c1"&gt;// native half, or a codegen mismatch. React Native's own examples import a spec like this at&lt;/span&gt;
&lt;span class="c1"&gt;// module scope, and with the module in the binary that is fine. The host's handler requires this&lt;/span&gt;
&lt;span class="c1"&gt;// file lazily instead, as a choice about where that throw is allowed to land. Imported at boot,&lt;/span&gt;
&lt;span class="c1"&gt;// a missing native half kills the shell during bundle evaluation, before any error boundary&lt;/span&gt;
&lt;span class="c1"&gt;// exists; required at the call, the same fault surfaces at the button press, in a running app&lt;/span&gt;
&lt;span class="c1"&gt;// that can log it and carry on. Runtime delivery is what makes the skew real: JavaScript can&lt;/span&gt;
&lt;span class="c1"&gt;// arrive on a binary that never built the native side.&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;TurboModuleRegistry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getEnforcing&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Spec&lt;/span&gt;&lt;span class="o"&gt;&amp;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;ShellNavigationModule&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;&lt;strong&gt;The boundary is JSON strings in both directions&lt;/strong&gt;; the spec's opening comment ends on the why. Tell codegen where the spec lives, in &lt;code&gt;apps/host/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;"codegenConfig"&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;"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;"HostSpecs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"modules"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"jsSrcsDir"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"specs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"android"&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;"javaPackageName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"com.host.specs"&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;Codegen runs during pod install, so run it now:&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/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;p&gt;The host's handler is the routing table's reader. Create &lt;code&gt;apps/host/src/shell/shellNavigation.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;ROUTE_REGISTRY&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;ShellNavigateFn&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;ShellNavigateResult&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="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Spec&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;ShellNavigationSpec&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;../../specs/NativeShellNavigationModule&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's half of shell.navigateTo. A remote calls the contract's shellNavigate; this is&lt;/span&gt;
&lt;span class="c1"&gt;// what runs. It looks the destination up in the routing table and hands native destinations to the&lt;/span&gt;
&lt;span class="c1"&gt;// TurboModule, JSON in and JSON out. The remote never learns which branch it took. ---&lt;/span&gt;

&lt;span class="c1"&gt;// The spec module calls TurboModuleRegistry.getEnforcing at import, and that throws when no&lt;/span&gt;
&lt;span class="c1"&gt;// native module answers to the name. Requiring it here, inside the call, decides where that&lt;/span&gt;
&lt;span class="c1"&gt;// throw can land: at the tap, in a running shell that logs it and carries on, rather than during&lt;/span&gt;
&lt;span class="c1"&gt;// boot evaluation, where a missing native half would kill the app with no error boundary.&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;nativeModule&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;ShellNavigationSpec&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;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;../../specs/NativeShellNavigationModule&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;ShellNavigationSpec&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;shellNavigateHandler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ShellNavigateFn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;destination&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="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;entry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ROUTE_REGISTRY&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;destination&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;entry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// An unknown destination is a caller's bug, not a crash: warn and resolve, so a remote built&lt;/span&gt;
    &lt;span class="c1"&gt;// against a newer contract than the host degrades to nothing happening.&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="s2"&gt;`[shellNavigate] unknown destination: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;destination&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="kc"&gt;undefined&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;resultJson&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;nativeModule&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;openNative&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;nativeId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="o"&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;resultJson&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="kc"&gt;undefined&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="nx"&gt;JSON&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;resultJson&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;ShellNavigateResult&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="c1"&gt;// A native result that does not parse is a native-side bug, and not a reason to detonate the&lt;/span&gt;
    &lt;span class="c1"&gt;// remote's await: warn and resolve, the same tolerance every other edge of this seam shows.&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="s2"&gt;`[shellNavigate] unparseable native result for &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;destination&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="kc"&gt;undefined&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;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;The lazy &lt;code&gt;require()&lt;/code&gt; chooses where a missing native half fails: at the tap, not at boot.&lt;/strong&gt; &lt;code&gt;getEnforcing&lt;/code&gt; throws when no module answers to the name. React Native's own examples import a spec at module scope, which works while the module is in the binary; runtime delivery makes the skew real. At boot the fault is a white screen with no error boundary; at the call, a loggable failure on one button.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Registration is two lines in &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;partyStateReady&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;registerShellNavigateHandler&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="c1"&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;shellNavigateHandler&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/shell/shellNavigation&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 fills the contract's navigation slot once, at module scope, before any remote can&lt;/span&gt;
&lt;span class="c1"&gt;// render and call shellNavigate. Registering the handler touches no native code — the TurboModule&lt;/span&gt;
&lt;span class="c1"&gt;// is not reached until a destination is actually navigated to — so this is safe at import in a way&lt;/span&gt;
&lt;span class="c1"&gt;// that requiring the spec here would not be.&lt;/span&gt;

&lt;span class="nf"&gt;registerShellNavigateHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;shellNavigateHandler&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Type the bridge, fetch the screens
&lt;/h2&gt;

&lt;p&gt;From here the work splits by what each half teaches. The bridge is federation, and you type all of it: the module holding a promise open across the boundary, its registration, the threading rules. The screens are SwiftUI and Compose UI code, so they arrive from this post's own end tag. Around a thousand lines stay off your keyboard, none unexplained.&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-13-native-handoff /tmp/pokedex-ref-13
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-ref-13/apps/host/ios/Host/QuickBattle.swift apps/host/ios/Host/QuickBattle.swift
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-ref-13/apps/host/android/app/src/main/java/com/host/QuickBattleActivity.kt apps/host/android/app/src/main/java/com/host/QuickBattleActivity.kt
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-ref-13/apps/host/__tests__/shellNavigation.test.ts apps/host/__tests__/shellNavigation.test.ts
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-ref-13/apps/party/__tests__/quickBattle.test.tsx apps/party/__tests__/quickBattle.test.tsx
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; apps/host/android/app/src/test/java/com/host apps/host/ios/HostTests
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-ref-13/apps/host/android/app/src/test/java/com/host/ShellNavigationModuleTest.kt apps/host/android/app/src/test/java/com/host/ShellNavigationModuleTest.kt
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-ref-13/apps/host/ios/HostTests/QuickBattlePresenterTests.swift apps/host/ios/HostTests/QuickBattlePresenterTests.swift
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both screens wear the design system. No bundle crosses this boundary, so each file mirrors &lt;code&gt;colours.ts&lt;/code&gt; and the eighteen type colours by hand, rebuilt into the card language the Pokédex grid renders: tinted sprite disc, hashed number pill, type badges, accent foot. Each resolves the &lt;code&gt;colourScheme&lt;/code&gt; the params carry against the same pairs the &lt;code&gt;dark:&lt;/code&gt; rules use; the Compose buttons declare &lt;code&gt;Role.Button&lt;/code&gt;, since a styled &lt;code&gt;Text&lt;/code&gt; otherwise announces to TalkBack as plain text; and each screen wears a purple NATIVE badge. No federated screen carries one, so a screenshot declares its side of the boundary on its own.&lt;/p&gt;

&lt;p&gt;Four suites arrive with the screens. The JavaScript pair pins the bridge's seam: the host's, the contract slot and the handler, unparseable results included; the party's, the button's gating and the winner landing through the private action. The native pair holds the promise guarantees the next two sections build, wired in after the exit table.&lt;/p&gt;

&lt;h2&gt;
  
  
  iOS: every exit resolves
&lt;/h2&gt;

&lt;p&gt;The iOS module is Objective-C++, and the extension is the point: &lt;code&gt;getTurboModule&lt;/code&gt; returns the generated JSI module (JavaScript Interface, the C++ layer under the new architecture) as a &lt;code&gt;std::shared_ptr&lt;/code&gt;, which Swift cannot express. The screen stays pure SwiftUI; the &lt;code&gt;.mm&lt;/code&gt; is a thin adapter. In Xcode, create &lt;code&gt;ShellNavigationModule.h&lt;/code&gt; and &lt;code&gt;ShellNavigationModule.mm&lt;/code&gt; in the &lt;code&gt;Host&lt;/code&gt; group, which puts them in the target, and add the fetched &lt;code&gt;QuickBattle.swift&lt;/code&gt; with File → Add Files to "Host".&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight objective_c"&gt;&lt;code&gt;&lt;span class="cp"&gt;#import &amp;lt;Foundation/Foundation.h&amp;gt;
#import &amp;lt;React/RCTBridgeModule.h&amp;gt;
#import &amp;lt;HostSpecs/HostSpecs.h&amp;gt;
&lt;/span&gt;
&lt;span class="c1"&gt;// --- The host's native half of shell.navigateTo on iOS. Codegen read specs/&lt;/span&gt;
&lt;span class="c1"&gt;// NativeShellNavigationModule.ts and wrote NativeShellNavigationModuleSpecBase and the&lt;/span&gt;
&lt;span class="c1"&gt;// NativeShellNavigationModuleSpec protocol into HostSpecs; this class subclasses the one and&lt;/span&gt;
&lt;span class="c1"&gt;// conforms to the other, which is what makes the JavaScript `openNative` land here.&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// The header exists because the implementation is ObjC++ (.mm) rather than Swift: returning the&lt;/span&gt;
&lt;span class="c1"&gt;// generated C++ JSI module from getTurboModule needs C++, and Swift cannot. The screen it&lt;/span&gt;
&lt;span class="c1"&gt;// presents is pure SwiftUI — see QuickBattle.swift. ---&lt;/span&gt;
&lt;span class="k"&gt;@interface&lt;/span&gt; &lt;span class="nc"&gt;ShellNavigationModule&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;NativeShellNavigationModuleSpecBase&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;NativeShellNavigationModuleSpec&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;@end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight objective_c"&gt;&lt;code&gt;&lt;span class="cp"&gt;#import "ShellNavigationModule.h"
#import &amp;lt;HostSpecs/HostSpecs.h&amp;gt;
&lt;/span&gt;&lt;span class="c1"&gt;// Host-Swift.h declares every @objc Swift class in this app, which is how this ObjC++ file reaches&lt;/span&gt;
&lt;span class="c1"&gt;// QuickBattlePresenter. Imported after the app-delegate header so the generated header's forward&lt;/span&gt;
&lt;span class="c1"&gt;// references resolve in this translation unit.&lt;/span&gt;
&lt;span class="cp"&gt;#import &amp;lt;React-RCTAppDelegate/RCTDefaultReactNativeFactoryDelegate.h&amp;gt;
#import "Host-Swift.h"
&lt;/span&gt;
&lt;span class="c1"&gt;// --- The TurboModule implementation. Two things live here and nothing else: the registration&lt;/span&gt;
&lt;span class="c1"&gt;// that puts the module in the registry under the name the spec asked for, and openNative.&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// openNative is where the promise starts. It hands the resolve block to the presenter and returns&lt;/span&gt;
&lt;span class="c1"&gt;// immediately; the block is called later, from the native screen, whenever that screen finishes.&lt;/span&gt;
&lt;span class="c1"&gt;// Until then the party's `await` is genuinely suspended. That is the whole handoff, and it is also&lt;/span&gt;
&lt;span class="c1"&gt;// the whole risk: a path through the native screen that never calls the block leaves the promise&lt;/span&gt;
&lt;span class="c1"&gt;// pending for the life of the process. ---&lt;/span&gt;
&lt;span class="k"&gt;@implementation&lt;/span&gt; &lt;span class="nc"&gt;ShellNavigationModule&lt;/span&gt;

&lt;span class="n"&gt;RCT_EXPORT_MODULE&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;openNative&lt;/span&gt;&lt;span class="p"&gt;:(&lt;/span&gt;&lt;span class="n"&gt;NSString&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nv"&gt;nativeId&lt;/span&gt;
        &lt;span class="nf"&gt;paramsJson&lt;/span&gt;&lt;span class="p"&gt;:(&lt;/span&gt;&lt;span class="n"&gt;NSString&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nv"&gt;paramsJson&lt;/span&gt;
           &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;:(&lt;/span&gt;&lt;span class="n"&gt;RCTPromiseResolveBlock&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nv"&gt;resolve&lt;/span&gt;
            &lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;:(&lt;/span&gt;&lt;span class="n"&gt;RCTPromiseRejectBlock&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nv"&gt;reject&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;QuickBattlePresenter&lt;/span&gt; &lt;span class="nf"&gt;presentWithNativeId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;nativeId&lt;/span&gt;
                                 &lt;span class="nl"&gt;paramsJson:&lt;/span&gt;&lt;span class="n"&gt;paramsJson&lt;/span&gt;
                                 &lt;span class="nl"&gt;completion:&lt;/span&gt;&lt;span class="o"&gt;^&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NSString&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;_Nonnull&lt;/span&gt; &lt;span class="n"&gt;resultJson&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                                   &lt;span class="n"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resultJson&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="c1"&gt;// The codegen C++ module. This method is the reason the file is ObjC++ rather than Swift: it&lt;/span&gt;
&lt;span class="c1"&gt;// returns a std::shared_ptr, which Swift has no way to express.&lt;/span&gt;
&lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;shared_ptr&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;facebook&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;react&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;TurboModule&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;getTurboModule&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;facebook&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;react&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ObjCTurboModule&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;InitParams&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nv"&gt;params&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;make_shared&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;facebook&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;react&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;NativeShellNavigationModuleSpecJSI&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What happens to that &lt;code&gt;resolve&lt;/code&gt; block is the post's core. &lt;strong&gt;Every exit of the native screen settles the promise, exactly once&lt;/strong&gt;, and every path lands in a settle-once wrapper. The call arrives off the main thread, so presentation hops before touching UIKit; the host controller comes from &lt;code&gt;hostProvider&lt;/code&gt;, a static variable defaulting to a key-window scan, reassigned only by the unit bundle; a re-entrancy guard settles a refused second presentation; the sheet blocks interactive dismissal; and two safety nets close the exits no button took. One catches a sheet that left the screen outside &lt;code&gt;finish()&lt;/code&gt;. The other catches a presentation UIKit silently refused, and its mechanism assumes nothing: whether the completion of a refused &lt;code&gt;present()&lt;/code&gt; runs is not documented. One main-queue turn later a refused controller still has no &lt;code&gt;presentingViewController&lt;/code&gt;, and none means nobody will ever dismiss the sheet, so the wrapper settles it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Every path below funnels into this, and it settles at most once. The battle's own exits&lt;/span&gt;
&lt;span class="c1"&gt;// race the two safety nets further down, and whichever lands first wins; the rest become&lt;/span&gt;
&lt;span class="c1"&gt;// no-ops instead of double settles.&lt;/span&gt;
&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;settled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;settle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&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="kt"&gt;Void&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;resultJson&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
  &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;settled&lt;/span&gt; &lt;span class="k"&gt;else&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="n"&gt;settled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="n"&gt;isPresenting&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="nf"&gt;completion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resultJson&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// openNative arrives on the TurboModule's own queue, not the main thread. Every UIKit call&lt;/span&gt;
&lt;span class="c1"&gt;// below has to be on main, so hop before touching anything.&lt;/span&gt;
&lt;span class="kt"&gt;DispatchQueue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;Self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hostProvider&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;isPresenting&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Nothing to present from, or one flow already up: settle rather than wedge.&lt;/span&gt;
    &lt;span class="nf"&gt;completion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&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="n"&gt;isPresenting&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

  &lt;span class="c1"&gt;// The result settles BEFORE the dismissal starts, not in its completion. viewDidDisappear&lt;/span&gt;
  &lt;span class="c1"&gt;// fires while the sheet is still animating out, so a settle scheduled after the transition&lt;/span&gt;
  &lt;span class="c1"&gt;// would lose the race to safety net one and the winner would arrive as "{}".&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;view&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;QuickBattleView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;contestants&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;contestants&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;isDark&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;isDark&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;resultJson&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
    &lt;span class="nf"&gt;settle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resultJson&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dismiss&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;animated&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="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;controller&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;QuickBattleHostingController&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;rootView&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;controller&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;modalPresentationStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pageSheet&lt;/span&gt;
  &lt;span class="c1"&gt;// Exit only through the screen's own controls. An interactive swipe-to-dismiss would tear&lt;/span&gt;
  &lt;span class="c1"&gt;// the sheet away without reaching the settle above, leaving the promise pending.&lt;/span&gt;
  &lt;span class="n"&gt;controller&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isModalInPresentation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="c1"&gt;// Safety net one: the sheet leaving the screen by any route the buttons did not take —&lt;/span&gt;
  &lt;span class="c1"&gt;// an ancestor being torn down is enough. A resultless settle beats a pending promise.&lt;/span&gt;
  &lt;span class="n"&gt;controller&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;onDisappear&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;settle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// Safety net two: present() silently does nothing when the host is mid-transition for&lt;/span&gt;
  &lt;span class="c1"&gt;// reasons this file cannot see. Whether UIKit runs the completion of a refused present is&lt;/span&gt;
  &lt;span class="c1"&gt;// not documented, so nothing here depends on it: one main-queue turn later, a presented&lt;/span&gt;
  &lt;span class="c1"&gt;// controller has a presentingViewController and a refused one still has none, and nil&lt;/span&gt;
  &lt;span class="c1"&gt;// means nobody will ever dismiss this sheet, so settle now.&lt;/span&gt;
  &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;present&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;animated&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="kt"&gt;DispatchQueue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;controller&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;presentingViewController&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;settle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&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;One order matters: the result settles before the dismissal starts, because &lt;code&gt;viewDidDisappear&lt;/code&gt; fires mid-animation. In the view, both exits, Close and Done, land in one &lt;code&gt;finish()&lt;/code&gt;, and a battle-free exit is a result rather than an error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;finish&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;winnerId&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;onDone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&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="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;json&lt;/span&gt; &lt;span class="o"&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="kt"&gt;JSONSerialization&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;withJSONObject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"winnerUid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;winnerId&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flatMap&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;encoding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utf8&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="s"&gt;"{}"&lt;/span&gt;
  &lt;span class="nf"&gt;onDone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;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;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;A never-settled promise has no safety net.&lt;/strong&gt; No timeout on the bridge, no garbage-collector rescue, no error to observe: the &lt;code&gt;await&lt;/code&gt; simply never returns, and the resolve block and everything it captures stay retained for the life of the process. The opposite fault costs a log line at worst, since the bridge ignores a second settle. Resolving on every exit is mandatory; resolving twice is survivable.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Android: every exit is a result
&lt;/h2&gt;

&lt;p&gt;Android gets most of the guarantee from structure: the screen is an Activity started for a result, the platform reports back every exit of a screen that opened, and the module's listener is the single place a delivered result settles it. What structure does not cover is the launch itself (a start that throws, or a teardown racing the UI-thread hop), so the module closes both by hand. Create &lt;code&gt;apps/host/android/app/src/main/java/com/host/ShellNavigationModule.kt&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="nn"&gt;com.host&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;android.app.Activity&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;android.content.Intent&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.facebook.react.bridge.BaseActivityEventListener&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.facebook.react.bridge.Promise&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.facebook.react.bridge.ReactApplicationContext&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.host.specs.NativeShellNavigationModuleSpec&lt;/span&gt;

&lt;span class="c1"&gt;// --- The host's native half of shell.navigateTo on Android, and the mirror of the iOS&lt;/span&gt;
&lt;span class="c1"&gt;// ShellNavigationModule. Codegen read the same spec file and wrote NativeShellNavigationModuleSpec&lt;/span&gt;
&lt;span class="c1"&gt;// into com.host.specs; this class extends it, so the JavaScript `openNative` lands here.&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// Android's shape does most of the promise discipline for it. The screen is an Activity started&lt;/span&gt;
&lt;span class="c1"&gt;// for result, so the platform delivers a result for every exit of a screen that opened — the Done&lt;/span&gt;
&lt;span class="c1"&gt;// button, the system back gesture — and onActivityResult is the single place a delivered result&lt;/span&gt;
&lt;span class="c1"&gt;// settles the promise. What structure does not cover is the launch itself: a start that throws,&lt;/span&gt;
&lt;span class="c1"&gt;// or a teardown racing the UI-thread hop. Those two windows are closed by hand below. ---&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ShellNavigationModule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reactContext&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ReactApplicationContext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="nc"&gt;NativeShellNavigationModuleSpec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reactContext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

  &lt;span class="c1"&gt;// Held from the moment the Activity is launched until it returns. One at a time, which is the&lt;/span&gt;
  &lt;span class="c1"&gt;// counterpart of the iOS presenter's isPresenting flag. Both fields are touched from the&lt;/span&gt;
  &lt;span class="c1"&gt;// native-modules thread (openNative, invalidate) and the UI thread (the launch block, the&lt;/span&gt;
  &lt;span class="c1"&gt;// result listener), so every mutation sits inside synchronized(this).&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;pendingPromise&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;

  &lt;span class="c1"&gt;// Flipped once in invalidate and never back: a launch queued before teardown must not open a&lt;/span&gt;
  &lt;span class="c1"&gt;// screen after it, because nobody is listening and its result could land on a later battle.&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;invalidated&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;

  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;activityEventListener&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="kd"&gt;object&lt;/span&gt; &lt;span class="err"&gt;: &lt;/span&gt;&lt;span class="nc"&gt;BaseActivityEventListener&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onActivityResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;activity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Activity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;requestCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;resultCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Intent&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;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requestCode&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="nc"&gt;QUICK_BATTLE_REQUEST&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;
        &lt;span class="c1"&gt;// Every exit lands here. A battle that finished carries its JSON; a back gesture carries&lt;/span&gt;
        &lt;span class="c1"&gt;// no data at all, and an empty object is the honest answer for it — the party reads a&lt;/span&gt;
        &lt;span class="c1"&gt;// missing winnerUid as "nothing happened".&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;getStringExtra&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;QuickBattleActivity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;EXTRA_RESULT_JSON&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="s"&gt;"{}"&lt;/span&gt;
        &lt;span class="nf"&gt;synchronized&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="nd"&gt;@ShellNavigationModule&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="n"&gt;pendingPromise&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="n"&gt;pendingPromise&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&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="nf"&gt;init&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;reactApplicationContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addActivityEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;activityEventListener&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;openNative&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nativeId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;paramsJson&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;promise&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;activity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;reactApplicationContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;currentActivity&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;activity&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Nothing to launch from: settle now rather than leave the caller waiting on a result&lt;/span&gt;
      &lt;span class="c1"&gt;// that will never be delivered.&lt;/span&gt;
      &lt;span class="n"&gt;promise&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="s"&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="nf"&gt;synchronized&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&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="n"&gt;pendingPromise&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;invalidated&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// A battle already running, or the module already torn down: same answer.&lt;/span&gt;
        &lt;span class="n"&gt;promise&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="s"&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="n"&gt;pendingPromise&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;promise&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// nativeId goes unread: one native flow exists today, and a second registry row would need a&lt;/span&gt;
    &lt;span class="c1"&gt;// switch here first. openNative arrives on a background queue; hop to the UI thread to start&lt;/span&gt;
    &lt;span class="c1"&gt;// the Activity rather than assume the queue is safe to start it from.&lt;/span&gt;
    &lt;span class="n"&gt;activity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;runOnUiThread&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;synchronized&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// invalidate can run between the queueing above and this block: the promise is settled&lt;/span&gt;
        &lt;span class="c1"&gt;// by then, and launching anyway would open a screen nobody is listening to.&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invalidated&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;pendingPromise&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="nd"&gt;@runOnUiThread&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;val&lt;/span&gt; &lt;span class="py"&gt;intent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
            &lt;span class="nc"&gt;Intent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;activity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;QuickBattleActivity&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;java&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="nf"&gt;putExtra&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;QuickBattleActivity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;EXTRA_PARAMS_JSON&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;paramsJson&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="n"&gt;activity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startActivityForResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;QUICK_BATTLE_REQUEST&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="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="c1"&gt;// A launch that throws leaves no Activity to deliver a result. Settle now, or the&lt;/span&gt;
          &lt;span class="c1"&gt;// await outlives a screen that never opened.&lt;/span&gt;
          &lt;span class="n"&gt;pendingPromise&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="n"&gt;pendingPromise&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&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;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;invalidate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// A dev reload or host teardown while a battle is open would strand the caller's await:&lt;/span&gt;
    &lt;span class="c1"&gt;// the listener is about to go away, so settle the pending promise the way a resultless&lt;/span&gt;
    &lt;span class="c1"&gt;// exit does before it can no longer be settled at all, and refuse any launch still queued.&lt;/span&gt;
    &lt;span class="nf"&gt;synchronized&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;invalidated&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
      &lt;span class="n"&gt;pendingPromise&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;pendingPromise&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;reactApplicationContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeActivityEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;activityEventListener&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invalidate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;companion&lt;/span&gt; &lt;span class="k"&gt;object&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;QUICK_BATTLE_REQUEST&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mh"&gt;0xB47&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;iOS registered itself with one macro; Android writes the registration down, in a package class the template leaves a slot for. The &lt;code&gt;com.host.specs&lt;/code&gt; base class appears at the first Gradle build, when Android's codegen runs, so an editor complaining before then is early rather than wrong. Create &lt;code&gt;apps/host/android/app/src/main/java/com/host/HostNativePackage.kt&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="nn"&gt;com.host&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.facebook.react.BaseReactPackage&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.facebook.react.bridge.NativeModule&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.facebook.react.bridge.ReactApplicationContext&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.facebook.react.module.model.ReactModuleInfo&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.facebook.react.module.model.ReactModuleInfoProvider&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.host.specs.NativeShellNavigationModuleSpec&lt;/span&gt;

&lt;span class="c1"&gt;// --- Registration. iOS gets this for free from RCT_EXPORT_MODULE and autolinking; Android wants&lt;/span&gt;
&lt;span class="c1"&gt;// it written down, in two halves. getModule builds the instance when the registry asks for it by&lt;/span&gt;
&lt;span class="c1"&gt;// name, and getReactModuleInfoProvider declares what that name is and how it behaves — the last&lt;/span&gt;
&lt;span class="c1"&gt;// flag is the one that matters here, because it is what marks the module as a TurboModule rather&lt;/span&gt;
&lt;span class="c1"&gt;// than a legacy bridge module.&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// The package is added to MainApplication's list, in the slot the React Native template leaves&lt;/span&gt;
&lt;span class="c1"&gt;// commented for exactly this. ---&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HostNativePackage&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;BaseReactPackage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

  &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;getModule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reactContext&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ReactApplicationContext&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;NativeModule&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nc"&gt;NativeShellNavigationModuleSpec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;NAME&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;ShellNavigationModule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reactContext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;getReactModuleInfoProvider&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ReactModuleInfoProvider&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;mapOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nc"&gt;NativeShellNavigationModuleSpec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;NAME&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt;
        &lt;span class="nc"&gt;ReactModuleInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nc"&gt;NativeShellNavigationModuleSpec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nc"&gt;ShellNavigationModule&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;java&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// canOverrideExistingModule&lt;/span&gt;
          &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// needsEagerInit&lt;/span&gt;
          &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// isCxxModule&lt;/span&gt;
          &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// isTurboModule&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;In &lt;code&gt;MainApplication.kt&lt;/code&gt;, the template's commented slot finally gets used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nc"&gt;PackageList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;packages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// The host's own native modules. Autolinking finds packages in node_modules; a module&lt;/span&gt;
  &lt;span class="c1"&gt;// that lives in the app itself is added here, in the slot the template leaves for it.&lt;/span&gt;
  &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HostNativePackage&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 fetched screen is the repo's first Jetpack Compose, so the build declares the compiler and the libraries. In &lt;code&gt;apps/host/android/build.gradle&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="n"&gt;classpath&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"org.jetbrains.kotlin:kotlin-gradle-plugin"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// Kotlin 2.x ships the Compose compiler as a separate plugin; the native Quick Battle&lt;/span&gt;
&lt;span class="c1"&gt;// screen is the only thing in the app that needs it.&lt;/span&gt;
&lt;span class="n"&gt;classpath&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"org.jetbrains.kotlin:compose-compiler-gradle-plugin:$kotlinVersion"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In &lt;code&gt;apps/host/android/app/build.gradle&lt;/code&gt;, the plugin, the build feature, and the dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="n"&gt;apply&lt;/span&gt; &lt;span class="nl"&gt;plugin:&lt;/span&gt; &lt;span class="s2"&gt;"org.jetbrains.kotlin.android"&lt;/span&gt;
&lt;span class="n"&gt;apply&lt;/span&gt; &lt;span class="nl"&gt;plugin:&lt;/span&gt; &lt;span class="s2"&gt;"org.jetbrains.kotlin.plugin.compose"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Jetpack Compose, for the native Quick Battle screen (the SwiftUI counterpart on iOS).&lt;/span&gt;
&lt;span class="n"&gt;buildFeatures&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;compose&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&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 groovy"&gt;&lt;code&gt;&lt;span class="c1"&gt;// --- Jetpack Compose, for QuickBattleActivity. The BOM pins the Compose family from one&lt;/span&gt;
&lt;span class="c1"&gt;// version, so those artefacts carry none of their own; activity-compose sits outside the&lt;/span&gt;
&lt;span class="c1"&gt;// BOM and pins itself. ---&lt;/span&gt;
&lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="n"&gt;composeBom&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;platform&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"androidx.compose:compose-bom:2024.09.03"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;implementation&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;composeBom&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;implementation&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"androidx.compose.ui:ui"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;implementation&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"androidx.compose.foundation:foundation"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;implementation&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"androidx.compose.material3:material3"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;implementation&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"androidx.activity:activity-compose:1.9.3"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Activity joins &lt;code&gt;AndroidManifest.xml&lt;/code&gt;, unexported:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- The native Quick Battle screen. exported=false: nothing outside this app launches it;
     the shell's TurboModule does, with startActivityForResult. --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;activity&lt;/span&gt;
  &lt;span class="na"&gt;android:name=&lt;/span&gt;&lt;span class="s"&gt;".QuickBattleActivity"&lt;/span&gt;
  &lt;span class="na"&gt;android:exported=&lt;/span&gt;&lt;span class="s"&gt;"false"&lt;/span&gt;
  &lt;span class="na"&gt;android:theme=&lt;/span&gt;&lt;span class="s"&gt;"@style/AppTheme"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read the fetched Activity's exit against the Swift version: Done calls &lt;code&gt;deliverResult&lt;/code&gt;; the system back button calls nothing, and needs nothing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// RESULT_OK with the JSON extra is what ShellNavigationModule's ActivityEventListener reads.&lt;/span&gt;
&lt;span class="c1"&gt;// A system back never reaches this method, and does not need to: the Activity finishes with no&lt;/span&gt;
&lt;span class="c1"&gt;// result, and the listener reads a null Intent as an empty object.&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;deliverResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resultJson&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;setResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Activity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;RESULT_OK&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Intent&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;putExtra&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;EXTRA_RESULT_JSON&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resultJson&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="nf"&gt;finish&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;On iOS every exit resolves because the code closed each one; on Android, because the platform reports back every exit of a screen that opened, and the module closes the launch paths by hand. The full set:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Exit&lt;/th&gt;
&lt;th&gt;iOS&lt;/th&gt;
&lt;th&gt;Android&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Done after a battle&lt;/td&gt;
&lt;td&gt;resolves &lt;code&gt;{"winnerUid": …}&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;RESULT_OK&lt;/code&gt; extra, resolves &lt;code&gt;{"winnerUid": …}&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Close before battling&lt;/td&gt;
&lt;td&gt;resolves &lt;code&gt;{}&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;RESULT_OK&lt;/code&gt; extra, resolves &lt;code&gt;{}&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Swipe to dismiss&lt;/td&gt;
&lt;td&gt;blocked by &lt;code&gt;isModalInPresentation&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;not applicable, full-screen Activity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;System back&lt;/td&gt;
&lt;td&gt;no such gesture on a sheet&lt;/td&gt;
&lt;td&gt;null Intent, resolves &lt;code&gt;{}&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Second &lt;code&gt;openNative&lt;/code&gt; while one is open&lt;/td&gt;
&lt;td&gt;the re-entrancy guard resolves it with &lt;code&gt;{}&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;the pending-promise guard resolves it with &lt;code&gt;{}&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The launch itself fails&lt;/td&gt;
&lt;td&gt;net two's nil check settles &lt;code&gt;{}&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;the &lt;code&gt;catch&lt;/code&gt; around the start resolves &lt;code&gt;{}&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shell torn down mid-battle&lt;/td&gt;
&lt;td&gt;the disappearance net settles &lt;code&gt;{}&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;invalidate()&lt;/code&gt; settles &lt;code&gt;{}&lt;/code&gt; and refuses a queued launch&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The second-launch row is defence in depth: the party's in-flight flag, coming next, already blocks it from the UI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pin the guarantees
&lt;/h2&gt;

&lt;p&gt;Half the table covers moments a simulator run cannot stage: a launch that throws, a teardown racing the hop, a presentation UIKit refuses. The two native suites fetched earlier hold them.&lt;/p&gt;

&lt;p&gt;The Android suite is plain JVM: &lt;code&gt;ShellNavigationModuleTest.kt&lt;/code&gt; drives the real module against mocked edges and walks the promise through eight paths. The mock Promise records its settlements; the mock Activity's &lt;code&gt;runOnUiThread&lt;/code&gt; runs inline, or is captured and fired after &lt;code&gt;invalidate()&lt;/code&gt;, which stages the teardown race. Two &lt;code&gt;apps/host/android/app/build.gradle&lt;/code&gt; edits wire it up. In the &lt;code&gt;android&lt;/code&gt; block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="c1"&gt;// JVM unit tests for ShellNavigationModule run against the SDK's stub android.jar; default&lt;/span&gt;
&lt;span class="c1"&gt;// values instead of "not mocked" throws let a real Intent be constructed as a no-op while&lt;/span&gt;
&lt;span class="c1"&gt;// the mocks around it carry the assertions.&lt;/span&gt;
&lt;span class="n"&gt;testOptions&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;unitTests&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;returnDefaultValues&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And in &lt;code&gt;dependencies&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="c1"&gt;// --- JVM unit tests for the bridge module: the promise-settling contract under launch&lt;/span&gt;
&lt;span class="c1"&gt;// failure, teardown, and the result listener. Mockito 5 mocks finals by default, which is&lt;/span&gt;
&lt;span class="c1"&gt;// what lets Activity and the React context stand in without wrappers. ---&lt;/span&gt;
&lt;span class="n"&gt;testImplementation&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"junit:junit:4.13.2"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;testImplementation&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"org.mockito:mockito-core:5.14.2"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;testImplementation&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"org.mockito.kotlin:mockito-kotlin:5.4.0"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The iOS suite needs one piece of project surgery, because the app is not its host. In Xcode: File → New → Target → Unit Testing Bundle, named &lt;code&gt;HostTests&lt;/code&gt;, with "Target to be Tested" set to None; then select the fetched &lt;code&gt;QuickBattlePresenterTests.swift&lt;/code&gt; and &lt;code&gt;QuickBattle.swift&lt;/code&gt; and tick &lt;code&gt;HostTests&lt;/code&gt; under Target Membership. If the new bundle is not in the Host scheme's Test action (Product → Scheme → Edit Scheme → Test), add it. The bundle compiles the presenter directly instead of loading the app, so its three tests need no React Native boot, dev server, or window hierarchy: no host to present from, a refused second presentation, and a presentation UIKit turns down. That last is the nil-check net observed as a test rather than trusted as a comment. The tests stage each case by reassigning the &lt;code&gt;hostProvider&lt;/code&gt; seam.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wire the button
&lt;/h2&gt;

&lt;p&gt;The party's half starts in its slice; both new pieces stay private, in no contract, since nothing outside the party dispatches them. &lt;code&gt;apps/party/src/partySlice.ts&lt;/code&gt; in full:&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="c1"&gt;// --- The party's state, owned outright by the party app. The contract carries the action creator&lt;/span&gt;
&lt;span class="c1"&gt;// and the read shape; this file carries what they mean. Nothing outside this app imports it — the&lt;/span&gt;
&lt;span class="c1"&gt;// host loads it as a federated module at boot, for the side effect at the bottom. ---&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="na"&gt;lastBattleWinnerUid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&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="c1"&gt;// A removed member cannot go on being the last winner; the banner would name a Pokémon&lt;/span&gt;
      &lt;span class="c1"&gt;// that is no longer in the party.&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;lastBattleWinnerUid&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="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lastBattleWinnerUid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&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="c1"&gt;// Also private, and deliberately so. The winner comes back from a native screen the HOST&lt;/span&gt;
    &lt;span class="c1"&gt;// presented, but it lands in the party's own state and nothing outside this app dispatches&lt;/span&gt;
    &lt;span class="c1"&gt;// it — so it appears in no contract. The handoff's params and result types are at the seam&lt;/span&gt;
    &lt;span class="c1"&gt;// because the host has to serialise them, which is a different reason from crossing.&lt;/span&gt;
    &lt;span class="nf"&gt;setLastBattleWinner&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;lastBattleWinnerUid&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="c1"&gt;// The crossing interaction. The list app dispatches the contract's addToParty, and this case&lt;/span&gt;
    &lt;span class="c1"&gt;// matches it on the type string: addCase reads actionCreator.type and keys the reducer by&lt;/span&gt;
    &lt;span class="c1"&gt;// `party/add`, so agreement on that string is what makes the match work, not the identity of&lt;/span&gt;
    &lt;span class="c1"&gt;// the creator object. Sharing @pokedex/contracts as a singleton is what stops the two sides&lt;/span&gt;
    &lt;span class="c1"&gt;// from retyping that string, the cap and the read shape separately.&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="nx"&gt;setLastBattleWinner&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. rootReducer is the same&lt;/span&gt;
&lt;span class="c1"&gt;// object the host's configureStore wired in — that is why injection from a separately-built app&lt;/span&gt;
&lt;span class="c1"&gt;// works at all.&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 of the party's own tests assert the slice's exact shape, so they grow the same field. In &lt;code&gt;__tests__/partyStateReady.test.ts&lt;/code&gt; and &lt;code&gt;__tests__/partySelfRecovery.test.tsx&lt;/code&gt;, the &lt;code&gt;toEqual({ members: [] })&lt;/code&gt; assertion becomes:&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="nf"&gt;expect&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="nf"&gt;getState&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;PartySliceShape&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="nf"&gt;toEqual&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;lastBattleWinnerUid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&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;PartyScreen.tsx&lt;/code&gt; grows in four places. First the imports:&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;useColorScheme&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;nativewind&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="nx"&gt;partyStateReady&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;shellNavigate&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="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="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;QuickBattleResult&lt;/span&gt;&lt;span class="p"&gt;,&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;Box&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ButtonText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;EmptySlot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;PokemonCard&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ScreenContainer&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;toast&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/ui&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="nx"&gt;setLastBattleWinner&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At module level, the owner's local read shape and the battle's floor:&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="c1"&gt;// The owner's own view of its slice. The contract carries `members`, because foreign modules read&lt;/span&gt;
&lt;span class="c1"&gt;// it; the last winner is read here and nowhere else, so it stays out of the contract and is added&lt;/span&gt;
&lt;span class="c1"&gt;// to the shape locally instead.&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;PartyOwnShape&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;PartySliceShape&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;lastBattleWinnerUid&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// Two contestants is the floor for a battle. Stated as a constant because the button's disabled&lt;/span&gt;
&lt;span class="c1"&gt;// state and the hint under it are two readings of the same rule.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MIN_CONTESTANTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside the component, the selectors and the handler:&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;colorScheme&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useColorScheme&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;PartyOwnShape&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="nx"&gt;EMPTY_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;lastBattleWinnerUid&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="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;PartyOwnShape&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;lastBattleWinnerUid&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&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;lastWinner&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="nf"&gt;find&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;lastBattleWinnerUid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&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="c1"&gt;// The one call a remote makes to reach native. `shellNavigate` is the whole of what this app&lt;/span&gt;
&lt;span class="c1"&gt;// knows about the other side: no TurboModule import, no native types, no idea that the screen&lt;/span&gt;
&lt;span class="c1"&gt;// it opens is SwiftUI on one platform and Compose on the other. The host resolves the&lt;/span&gt;
&lt;span class="c1"&gt;// destination and owns everything past it.&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// The await is the point. It does not return until the native flow has finished, and what comes&lt;/span&gt;
&lt;span class="c1"&gt;// back lands in this app's own state through this app's own action — the round trip starts and&lt;/span&gt;
&lt;span class="c1"&gt;// ends inside the party, so nothing about the battle needs to cross the contract.&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;battleInFlight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setBattleInFlight&lt;/span&gt;&lt;span class="p"&gt;]&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;useState&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onQuickBattle&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;useCallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &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;battleInFlight&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="nf"&gt;setBattleInFlight&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="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// The theme is read at the moment of the call, from the same NativeWind observable every&lt;/span&gt;
    &lt;span class="c1"&gt;// dark: class in the federation subscribes to. It is sent rather than observed because the&lt;/span&gt;
    &lt;span class="c1"&gt;// native screen has no way to subscribe: a toggle while the battle is open will not reach it.&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;shellNavigate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;QuickBattle&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;members&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;colourScheme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;colorScheme&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dark&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;dark&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;light&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;as&lt;/span&gt; &lt;span class="nx"&gt;QuickBattleResult&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="c1"&gt;// No winner is a real outcome, not a failure: the screen can be closed without battling,&lt;/span&gt;
    &lt;span class="c1"&gt;// and the native side resolves with an empty object when it is.&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;result&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;winnerUid&lt;/span&gt;&lt;span class="p"&gt;)&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;setLastBattleWinner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;winnerUid&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;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setBattleInFlight&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="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;battleInFlight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;colorScheme&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;span class="nx"&gt;members&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The render gains the button, its hint, and the banner, after the slot grid:&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;{&lt;/span&gt;&lt;span class="cm"&gt;/* The handoff's one control. Disabled below two members, because a battle needs two
    contestants; the hint under it says which rule it is rather than leaving a dead
    button to explain itself. */&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;Button&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="nx"&gt;onQuickBattle&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&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;lt;&lt;/span&gt; &lt;span class="nx"&gt;MIN_CONTESTANTS&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;battleInFlight&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;"lg"&lt;/span&gt;
  &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`mt-6 rounded-xl &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;lt;&lt;/span&gt; &lt;span class="nx"&gt;MIN_CONTESTANTS&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bg-lightGrey dark:bg-white/10&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;bg-purple&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// The 44pt minimum is declared, not inherited from the size variant, the same way the&lt;/span&gt;
  &lt;span class="c1"&gt;// detail's Add button declares it: the accessibility suite can only check what the&lt;/span&gt;
  &lt;span class="c1"&gt;// control states about itself.&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="na"&gt;alignSelf&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stretch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;minWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;44&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;minHeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;44&lt;/span&gt; &lt;span class="p"&gt;}&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="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ButtonText&lt;/span&gt;
    &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&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;lt;&lt;/span&gt; &lt;span class="nx"&gt;MIN_CONTESTANTS&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text-midGrey&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;text-white&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="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;battleInFlight&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Battling…&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;Quick Battle&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;lt;/&lt;/span&gt;&lt;span class="nc"&gt;ButtonText&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;Button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&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;lt;&lt;/span&gt; &lt;span class="nx"&gt;MIN_CONTESTANTS&lt;/span&gt; &lt;span class="p"&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;Text&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;"xs"&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"mt-2 text-center text-darkGrey dark:text-lightGrey"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    Add at least 2 Pokémon to battle.
  &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;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* What came back from native, rendered by the app that owns the 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;lastWinner&lt;/span&gt; &lt;span class="p"&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;Text&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;"sm"&lt;/span&gt;
    &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"mt-3 text-center font-semi text-darkGrey dark:text-lightGrey"&lt;/span&gt;
    &lt;span class="na"&gt;accessibilityLiveRegion&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"polite"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    Last battle winner: &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;lastWinner&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;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One change is easy to miss, and it cost this build a debugging session. The filled slots sit in a module-level &lt;code&gt;React.memo&lt;/code&gt;, and a theme change alters none of their props, so the memo skips the subtree and the card keeps the old scheme's style objects: an empty white rectangle. The slot's key carries the scheme, so a toggle becomes a remount of six small cards:&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="c1"&gt;// The key carries the colour scheme, and it has to. A theme change alters none of&lt;/span&gt;
&lt;span class="c1"&gt;// this slot's props, so the memo above finds them identical and skips the subtree;&lt;/span&gt;
&lt;span class="c1"&gt;// the card then keeps the style objects the styling runtime resolved for the old&lt;/span&gt;
&lt;span class="c1"&gt;// scheme and paints as an empty white rectangle. Keying by scheme makes a toggle a&lt;/span&gt;
&lt;span class="c1"&gt;// remount of six small cards, while the memo goes on doing its job within a theme.&lt;/span&gt;
&lt;span class="c1"&gt;// The Pokédex grid does not memoise its cards, which is why it never showed this.&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PartySlot&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="s2"&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="nx"&gt;uid&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;colorScheme&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;member&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;member&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;onOpen&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;openDetail&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;onRemoveMember&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;removeMember&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;h2&gt;
  
  
  Run it
&lt;/h2&gt;

&lt;p&gt;Native code changed on both platforms, so both need real builds, as &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-native-handoff" rel="noopener noreferrer"&gt;post 4's screens&lt;/a&gt; and post 11's animation pods did. This time the compiled code is the series' own. Three dev servers, each in 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/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;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;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Restart the host's dev server with &lt;code&gt;--reset-cache&lt;/code&gt; if it was already running.&lt;/strong&gt; A server started before contracts 3.3.0 was installed keeps serving the old resolution, and boot dies with &lt;code&gt;TypeError: undefined is not a function&lt;/code&gt; at &lt;code&gt;App.tsx&lt;/code&gt;: a cached contracts build with no &lt;code&gt;registerShellNavigateHandler&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then the builds, and the suites with 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;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;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 android &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="k"&gt;for &lt;/span&gt;d &lt;span class="k"&gt;in &lt;/span&gt;apps/host apps/party&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="nv"&gt;$d&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npx jest &lt;span class="nt"&gt;--silent&lt;/span&gt; &lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the native suites, swapping the simulator name for one you have installed:&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/android &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; ./gradlew :app:testDebugUnitTest &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; xcodebuild &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;-workspace&lt;/span&gt; Host.xcworkspace &lt;span class="nt"&gt;-scheme&lt;/span&gt; Host &lt;span class="nt"&gt;-only-testing&lt;/span&gt;:HostTests &lt;span class="nt"&gt;-destination&lt;/span&gt; &lt;span class="s1"&gt;'platform=iOS Simulator,name=iPhone 17 Pro'&lt;/span&gt; &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add two Pokémon, open the Party tab, press Quick Battle. The native sheet rises wearing the grid's own cards, badge aside; Battle rings the winner in its type colour; Done dismisses; the banner names the winner the promise carried back. Flip the theme first and the native screen follows, from the scheme the call sent:&lt;/p&gt;

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

&lt;p&gt;The badge is the only giveaway. That is the design system holding across a boundary no bundle crosses:&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%2Ftqfk0la1phexhw1wz72q.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%2Ftqfk0la1phexhw1wz72q.webp" alt="The native Quick Battle sheet in dark mode: two party members drawn in the design system's card language, the winner ringed and haloed in its type colour, under a purple NATIVE iOS badge" width="600" height="1305"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;With the round trip working, the bridge's dangerous property deserves one deliberate viewing. The presenter funnels every exit into its settle-once wrapper, so the honest sabotage is the one line every path trusts. In the fetched &lt;code&gt;QuickBattle.swift&lt;/code&gt;, find the wrapper and comment out its last call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;settled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;settle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&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="kt"&gt;Void&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;resultJson&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
  &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;settled&lt;/span&gt; &lt;span class="k"&gt;else&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="n"&gt;settled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="n"&gt;isPresenting&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="c1"&gt;// completion(resultJson)   &amp;lt;- break-it: the resolve never happens&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rebuild, open Quick Battle, battle, tap Done. The sheet dismisses normally, and back on the Party tab the button reads "Battling…" for the life of the process: the &lt;code&gt;await&lt;/code&gt; never returns, so the &lt;code&gt;finally&lt;/code&gt; that clears &lt;code&gt;battleInFlight&lt;/code&gt; never runs. A second tap does nothing, blocked by the in-flight guard. No red box, no log line, no rejected promise. Neither safety net helped: net one fired its &lt;code&gt;settle("{}")&lt;/code&gt; straight into the flipped &lt;code&gt;settled&lt;/code&gt; guard, and net two had nothing to catch, since the presentation succeeded. The nets guard the paths into the wrapper, and nothing guards the wrapper's own last line. That is worth a moment: defence in depth ends somewhere, and wherever it ends is the line a review has to read hardest.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Nothing crashed, which is the problem.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The series has a small taxonomy of quiet failures, and this is the quietest. &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-native-handoff" rel="noopener noreferrer"&gt;Post 8's lost add&lt;/a&gt; at least dispatched into a store and showed in devtools; this one leaves no trace, because nothing happened. Android cannot reproduce the same fault: empty out &lt;code&gt;deliverResult&lt;/code&gt;, and the system back gesture still lands in &lt;code&gt;onActivityResult&lt;/code&gt; with a null Intent and resolves &lt;code&gt;{}&lt;/code&gt;. That is the exit table's asymmetry, observed. Restore the line and rebuild.&lt;/p&gt;

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

&lt;p&gt;A federated remote asked for a native screen and got one, without learning anything about native. The contract exposes one promise-returning function over a one-entry routing table; the host implements it with a TurboModule carrying JSON both ways, presenting SwiftUI on one platform and a Compose Activity on the other, both in the design system's tokens and theme. The winner's uid returns on the promise and lands through a private action, crossing no contract action, as &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-native-handoff" rel="noopener noreferrer"&gt;post 7's ownership rules&lt;/a&gt; predict.&lt;/p&gt;

&lt;p&gt;The honest limits: the bridge is one-way, so a native screen cannot yet ask the shell to navigate anywhere, and deep links do not route through the table; both are deliberate cuts and the bridge's next additions. The theme travels in the params, so a mid-battle toggle reaches the next battle, not the current one.&lt;/p&gt;

&lt;p&gt;Next, the build gets real: a production bundle, and the first federated release artefact.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://v4.re-pack.dev/docs/module-federation" rel="noopener noreferrer"&gt;Re.Pack: Module Federation&lt;/a&gt; — the limitations list, including "Host application must have native modules used in other containers"&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://reactnative.dev/docs/turbo-native-modules-introduction" rel="noopener noreferrer"&gt;React Native: Turbo Native Modules introduction&lt;/a&gt; — the spec-first workflow this post follows on both platforms&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/reactwg/react-native-new-architecture/blob/main/docs/turbo-modules.md" rel="noopener noreferrer"&gt;React Native New Architecture working group: Turbo Modules guide&lt;/a&gt; — the worked promise-returning module example&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/reactwg/react-native-new-architecture/blob/main/docs/appendix.md" rel="noopener noreferrer"&gt;React Native New Architecture working group: appendix&lt;/a&gt; — the type mappings, &lt;code&gt;Promise&amp;lt;T&amp;gt;&lt;/code&gt; included&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://reactnative.dev/docs/legacy/native-modules-android" rel="noopener noreferrer"&gt;React Native: legacy Android native modules&lt;/a&gt; — the deferred-promise pattern around &lt;code&gt;onActivityResult&lt;/code&gt; that the Kotlin module carries into the new architecture&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://reactnavigation.org/docs/native-stack-navigator/" rel="noopener noreferrer"&gt;React Navigation: native stack&lt;/a&gt; — the right tool for native-backed React Native screens, and the alternative this bridge is not competing with&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.android.com/develop/ui/compose/bom" rel="noopener noreferrer"&gt;Jetpack Compose BOM&lt;/a&gt; — the bill of materials pinning the Compose libraries to one version set&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-13-native-handoff&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>Accessibility testing across federated remotes in React Native</title>
      <dc:creator>Warren de Leon</dc:creator>
      <pubDate>Mon, 14 Sep 2026 00:05:03 +0000</pubDate>
      <link>https://dev.to/warrendeleon/accessibility-testing-across-federated-remotes-in-react-native-4i6f</link>
      <guid>https://dev.to/warrendeleon/accessibility-testing-across-federated-remotes-in-react-native-4i6f</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-a11y-testing" 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/federated-design-system-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-a11y-testing" rel="noopener noreferrer"&gt;Post 11&lt;/a&gt; closed on a promise: accessibility across the seam, a shared testing package that checks touch targets, contrast and focus order in remotes the host only meets at runtime. Two of those three fit in a Jest suite. Focus order does not, and &lt;em&gt;What these checks cannot see&lt;/em&gt; says who it belongs to instead.&lt;/p&gt;

&lt;p&gt;The problem is post 11's problem one level up. Three teams ship three bundles on their own schedules, each with its own idea of what counts as accessible, and those ideas drift exactly the way the greys drifted before a design system existed. Writing the bar down in a wiki page does not stop the drift, because a convention has no version and no install step. A package has both.&lt;/p&gt;

&lt;p&gt;So the bar becomes &lt;code&gt;@pokedex/a11y-testing&lt;/code&gt;: a Jest preset, a NativeWind-aware render, a small set of WCAG (Web Content Accessibility Guidelines) assertion helpers, the A and AA criteria catalogue, and a reporter. It publishes to the same local Verdaccio registry as every other package at the seam, and both source packages and both remotes install it. &lt;strong&gt;It is also the first &lt;code&gt;@pokedex&lt;/code&gt; package that never reaches a bundle.&lt;/strong&gt; Every consumer takes it as a devDependency, so no shared map changes and no runtime moves.&lt;/p&gt;

&lt;p&gt;The criteria being checked are not this project's invention. The European Accessibility Act (Directive (EU) 2019/882) has applied since 28 June 2025 to the products and services it names. The Act itself sets functional requirements and names no standard; EN 301 549, which applies WCAG 2.1's A and AA criteria to mobile apps, is the standard that European practice measures against, and its revision to WCAG 2.2 is expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not jest-axe
&lt;/h2&gt;

&lt;p&gt;On the web this is a solved problem with a package attached. &lt;code&gt;jest-axe&lt;/code&gt; wraps axe-core, &lt;code&gt;expect(await axe(container)).toHaveNoViolations()&lt;/code&gt; covers a wide slice of the rules in one line, and for a React web app it is the right first move.&lt;/p&gt;

&lt;p&gt;axe-core tests HTML. &lt;code&gt;jest-axe&lt;/code&gt; needs a jsdom environment, and its own README says colour contrast checks do not work in jsdom, so they are switched off even there. &lt;code&gt;@axe-core/react-native&lt;/code&gt; is not a package at all: the npm registry answers 404. Deque, who maintain axe-core, do sell React Native tooling, including device SDKs that a native test run can call. That is the device-audit layer &lt;em&gt;What these checks cannot see&lt;/em&gt; names, not the Jest one this post builds.&lt;/p&gt;

&lt;p&gt;React Native Testing Library (RNTL) supplies the queries and the matchers and stops short of an audit. Nothing in its API returns a list of violations. Searching for prior art on accessibility testing across Module Federation remotes turned up nothing, in React Native or on the web. Two community packages come up when you search for React Native accessibility assertions, and neither is the shape this needs. &lt;code&gt;react-native-accessibility-engine&lt;/code&gt; last published in 2022. &lt;code&gt;react-native-ama&lt;/code&gt; is alive, though you have to know it moved: the unscoped package stopped at 0.7.5 in 2023 and the work moved to a scope. Seven of the eight &lt;code&gt;@react-native-ama/*&lt;/code&gt; packages shipped 1.2.1 in August 2025, and five of the eight shipped a 2.0 beta in July 2026. It is a component library with dev-time runtime checks rather than a Jest assertion layer, so it answers a different question from the one in this post.&lt;/p&gt;

&lt;p&gt;The bar gets assembled instead: a render that resolves classes, a handful of assertions, and a report.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📊 &lt;strong&gt;Diagram:&lt;/strong&gt; &lt;a href="https://warrendeleon.com/blog/accessibility-testing-federated-remotes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-a11y-testing#why-not-jest-axe" rel="noopener noreferrer"&gt;view it on warrendeleon.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Four arrows, two jobs. Two go to the packages that own the shared pixels, where a check runs once for everybody. Two go to the teams, where each suite covers the screens only that team composes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The package
&lt;/h2&gt;

&lt;p&gt;Start from post 11'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-11-design-system
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things have to be true before anything here runs, and all three are easy to assume you&lt;br&gt;
already have. The local Verdaccio registry needs to be up with &lt;code&gt;@pokedex/contracts&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;@pokedex/ui&lt;/code&gt; and &lt;code&gt;@pokedex/detail&lt;/code&gt; published to it, which is post 11's work. npm needs to know&lt;br&gt;
that the &lt;code&gt;@pokedex&lt;/code&gt; scope lives there. And you need to be logged in to that registry, which is&lt;br&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-a11y-testing" rel="noopener noreferrer"&gt;post 5&lt;/a&gt;'s step rather than post 11's,&lt;br&gt;
because the publishes later in this post are authenticated though the reads are not.&lt;/p&gt;

&lt;p&gt;Start the registry first, because &lt;code&gt;npm adduser&lt;/code&gt; below needs something to log in to:&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@6.2.0                              &lt;span class="c"&gt;# :4873, leave it running&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set the other two once, for your user, because &lt;strong&gt;npm reads its project config from the directory&lt;br&gt;
holding &lt;code&gt;package.json&lt;/code&gt;, not from the repository root&lt;/strong&gt;, so the repo's own &lt;code&gt;.npmrc&lt;/code&gt; is invisible to&lt;br&gt;
a command run inside &lt;code&gt;packages/ui&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm config &lt;span class="nb"&gt;set&lt;/span&gt; @pokedex:registry http://localhost:4873/
npm adduser &lt;span class="nt"&gt;--registry&lt;/span&gt; http://localhost:4873/     &lt;span class="c"&gt;# only if you have not already&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you did not build post 11, its &lt;em&gt;A package for the pixels&lt;/em&gt; and &lt;em&gt;The detail finishes dressing&lt;/em&gt;&lt;br&gt;
sections publish the three packages this one installs. &lt;code&gt;apps/host&lt;/code&gt; needs its dependencies; the two remotes get theirs with their packages further down:&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="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The source is more than a post can usefully retype. Fetch the reference copy once and publish it the way &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-a11y-testing" rel="noopener noreferrer"&gt;post 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-a11y-testing" rel="noopener noreferrer"&gt;post 6&lt;/a&gt; published theirs. The tag is released the day this post goes live, so &lt;code&gt;degit&lt;/code&gt; can reach it:&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-12-a11y-testing /tmp/pokedex-a11y-ref
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; /tmp/pokedex-a11y-ref/packages/a11y-testing packages/
&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;packages/a11y-testing &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;The anatomy follows the packages before it: &lt;code&gt;publishConfig&lt;/code&gt; points at Verdaccio, as it has since &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-a11y-testing" rel="noopener noreferrer"&gt;post 5&lt;/a&gt;, and &lt;code&gt;react-native-builder-bob&lt;/code&gt; builds an ES-module &lt;code&gt;lib/&lt;/code&gt;, the arrangement &lt;code&gt;@pokedex/ui&lt;/code&gt; introduced in post 11. One deliberate omission. &lt;strong&gt;The package ships no &lt;code&gt;exports&lt;/code&gt; map.&lt;/strong&gt; Consumers reach for the root, &lt;code&gt;/jest-preset&lt;/code&gt; in every Jest config, and &lt;code&gt;/reporter.js&lt;/code&gt; in every &lt;code&gt;test:a11y&lt;/code&gt; script, and a map has to name all three. Omit one and Node answers &lt;code&gt;ERR_PACKAGE_PATH_NOT_EXPORTED&lt;/code&gt;, which for the preset means no test run at all.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;jest-preset.js&lt;/code&gt; extends &lt;code&gt;@react-native/jest-preset&lt;/code&gt; rather than replacing it, which is what lets every suite that already existed keep passing untouched. The two additions that matter here are both about NativeWind. &lt;code&gt;nativewind/babel&lt;/code&gt; joins the transform presets, so &lt;code&gt;className&lt;/code&gt; reaches the styling runtime as style instead of sitting there as an inert prop that makes every colour assertion read &lt;code&gt;undefined&lt;/code&gt;. And &lt;code&gt;react-native-css-interop/dist/test/setupAfterEnv.js&lt;/code&gt; joins &lt;code&gt;setupFilesAfterEnv&lt;/code&gt;, which is where the &lt;code&gt;toHaveStyle&lt;/code&gt; matcher lives. Neither line is what makes the checks in this post work (the contrast matrix reads the preset's colour object directly, and the component checks read class strings), but a shared preset has to serve the suite that does assert on a resolved style, and both are asserted present so a future edit cannot quietly drop them.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Two of the three entry points this package &lt;em&gt;imports&lt;/em&gt; are undocumented.&lt;/strong&gt; &lt;code&gt;nativewind/babel&lt;/code&gt; is fine: it is step 3 of NativeWind's own installation guide. The other two are not written down anywhere. &lt;code&gt;react-native-css-interop/dist/test/setupAfterEnv.js&lt;/code&gt;, which the preset adds to &lt;code&gt;setupFilesAfterEnv&lt;/code&gt;, and &lt;code&gt;nativewind/test&lt;/code&gt;, which the render imports, are real and shipped and power NativeWind's own suite, and nativewind.dev has no testing section at all. So the versions this is proven against are recorded rather than assumed: nativewind 4.2.6 and react-native-css-interop 0.2.6, carried on caret ranges and re-checked whenever NativeWind moves.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;RNTL is held below 14 for a related reason: version 14 rewrote &lt;code&gt;render&lt;/code&gt;, &lt;code&gt;fireEvent&lt;/code&gt; and &lt;code&gt;act&lt;/code&gt; as async, and this render path has not been exercised against it. Never reach for &lt;code&gt;@testing-library/jest-native&lt;/code&gt; either; it is deprecated, and RNTL has shipped its own matchers since 12.4.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;createThemedRender&lt;/code&gt; binds a Tailwind preset once and hands back the render every suite in that package uses:&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;renderWithTheme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createThemedRender&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getByRole&lt;/span&gt; &lt;span class="p"&gt;}&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;renderWithTheme&lt;/span&gt;&lt;span class="p"&gt;(&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;SomeButton&lt;/span&gt; &lt;span class="na"&gt;disabled&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;Taking the preset as an argument keeps the accessibility package free of a dependency on the design system. It is a tool, not a member of the federation. Binding it to &lt;code&gt;@pokedex/ui&lt;/code&gt;'s own preset is what lets a suite resolve a class the way the app will, which is what the touch-target checks read and what the contrast helpers are computed against.&lt;/p&gt;

&lt;p&gt;The helpers are plain functions that call the global &lt;code&gt;expect&lt;/code&gt;, not &lt;code&gt;expect.extend&lt;/code&gt; matchers. A helper imported by name needs no setup file, behaves identically in every package, and appears in a stack trace as itself. None of them import React Native: a test element is anything with props. Each one is tied to a success criterion (SC), and to the bar that criterion sets.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What it checks&lt;/th&gt;
&lt;th&gt;Criterion&lt;/th&gt;
&lt;th&gt;Level&lt;/th&gt;
&lt;th&gt;The bar&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Text on a surface&lt;/td&gt;
&lt;td&gt;SC 1.4.3 Contrast (Minimum)&lt;/td&gt;
&lt;td&gt;AA&lt;/td&gt;
&lt;td&gt;4.5:1 normal text, 3:1 large&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A control's own boundary&lt;/td&gt;
&lt;td&gt;SC 1.4.11 Non-text Contrast&lt;/td&gt;
&lt;td&gt;AA&lt;/td&gt;
&lt;td&gt;3:1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What a screen reader reads out&lt;/td&gt;
&lt;td&gt;SC 4.1.2 Name, Role, Value&lt;/td&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;name, role &lt;strong&gt;and&lt;/strong&gt; state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Meaning not carried by colour&lt;/td&gt;
&lt;td&gt;SC 1.4.1 Use of Color&lt;/td&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;the information is in words too&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A change announced without moving focus&lt;/td&gt;
&lt;td&gt;SC 4.1.3 Status Messages&lt;/td&gt;
&lt;td&gt;AA&lt;/td&gt;
&lt;td&gt;a role or live region carries it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Touch target&lt;/td&gt;
&lt;td&gt;SC 2.5.5 Target Size (Enhanced)&lt;/td&gt;
&lt;td&gt;AAA&lt;/td&gt;
&lt;td&gt;44 by 44 CSS pixels&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The touch-target row is the one to read twice. &lt;strong&gt;44pt is this project's bar, and it is Apple's number rather than WCAG's.&lt;/strong&gt; The current Human Interface Guidelines give iOS and iPadOS a default control size of 44 by 44 points and a minimum of 28 by 28, so 44 is the size Apple asks you to design to, not the smallest it will accept. An older Apple page, still widely quoted, says only "at least 44 points x 44 points", which is where the "Apple's minimum" shorthand comes from. On the WCAG side, SC 2.5.5 sits at Level AAA, and WCAG 2.2's AA criterion, SC 2.5.8 Target Size (Minimum), asks for 24 by 24 CSS pixels with five exceptions. Forty-four clears that comfortably. It does not clear everything: Android's guidance asks for at least 48dp, so a project that ships to both platforms and stops at 44 has taken Apple's recommended size and accepted that Android would prefer more. That is a decision worth making on purpose. Calling any of it "what WCAG AA requires" is the mistake to avoid.&lt;/p&gt;

&lt;p&gt;The contrast helper uses &lt;code&gt;0.04045&lt;/code&gt; in the channel linearisation. The relative-luminance definition carried &lt;code&gt;0.03928&lt;/code&gt; before May 2021 and the older constant is still copied around widely. The spec's own note says the change has no practical effect on the results, and the current constant costs nothing.&lt;/p&gt;

&lt;p&gt;The touch-target helper has one behaviour that decides how much the whole layer is worth: it throws where it cannot see, rather than passing. That applies per axis. A control declaring a height and no width has not been measured on its width, and substituting the bar for the axis nobody stated would report it as accessible at exactly the moment the test could not see it. A &lt;code&gt;hitSlop&lt;/code&gt; is measured too, not merely noted as present, because &lt;code&gt;hitSlop: 0&lt;/code&gt; extends nothing.&lt;/p&gt;

&lt;p&gt;That rule is the difference between a suite and a decoration, and it is the one this package got wrong first: an early version borrowed the bar for an undeclared axis, so a button that stated only its height passed on a width nobody had measured. It has its own regression test now, in the package's own suite.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the source
&lt;/h2&gt;

&lt;p&gt;Both source packages install the bar and point Jest at it. &lt;code&gt;@tailwindcss/container-queries&lt;/code&gt; is not optional here: the test render fails with &lt;em&gt;Cannot find module&lt;/em&gt; without 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="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="nt"&gt;-D&lt;/span&gt; @pokedex/a11y-testing@1.0.18 @testing-library/react-native@13.3.3 @tailwindcss/container-queries@0.1.1 @types/jest@29.5.14 @types/react-test-renderer@19.1.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;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="nt"&gt;-D&lt;/span&gt; @pokedex/a11y-testing@1.0.18 @testing-library/react-native@13.3.3 @tailwindcss/container-queries@0.1.1 @types/jest@29.5.14 @types/react-test-renderer@19.1.0 &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The suites, the two Jest configs and the per-suite not-applicable declarations come from the reference copy, and each package gains a &lt;code&gt;test:a11y&lt;/code&gt; script:&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;pkg &lt;span class="k"&gt;in &lt;/span&gt;ui detail&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-a11y-ref/packages/&lt;span class="nv"&gt;$pkg&lt;/span&gt;/jest.config.js &lt;span class="se"&gt;\&lt;/span&gt;
     /tmp/pokedex-a11y-ref/packages/&lt;span class="nv"&gt;$pkg&lt;/span&gt;/a11y-report.config.js &lt;span class="se"&gt;\&lt;/span&gt;
     /tmp/pokedex-a11y-ref/packages/&lt;span class="nv"&gt;$pkg&lt;/span&gt;/.gitignore packages/&lt;span class="nv"&gt;$pkg&lt;/span&gt;/
&lt;span class="k"&gt;done
&lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; packages/ui/src/tokens/__tests__ packages/ui/src/components/__tests__
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-a11y-ref/packages/ui/src/tokens/__tests__/contrast.accessibility.ts packages/ui/src/tokens/__tests__/
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-a11y-ref/packages/ui/src/components/__tests__/components.accessibility.tsx packages/ui/src/components/__tests__/
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-a11y-ref/packages/detail/__tests__/detail-view.accessibility.tsx packages/detail/__tests__/
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-a11y-ref/packages/detail/__tests__/detail-view.test.tsx packages/detail/__tests__/
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-a11y-ref/packages/ui/tsconfig.json packages/ui/
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-a11y-ref/packages/detail/tsconfig.json &lt;span class="se"&gt;\&lt;/span&gt;
   /tmp/pokedex-a11y-ref/packages/detail/tsconfig.build.json packages/detail/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The design-system changes come across the same way. These are the token repairs the rest of this&lt;br&gt;
section is about, and they have to be in the tree before the matrix can pass:&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; /tmp/pokedex-a11y-ref/packages/ui/tailwind.preset.js packages/ui/
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-a11y-ref/packages/ui/src/tokens/colours.ts &lt;span class="se"&gt;\&lt;/span&gt;
   /tmp/pokedex-a11y-ref/packages/ui/src/tokens/typeColours.ts packages/ui/src/tokens/
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-a11y-ref/packages/ui/src/components/type-badge.tsx &lt;span class="se"&gt;\&lt;/span&gt;
   /tmp/pokedex-a11y-ref/packages/ui/src/components/back-pill.tsx &lt;span class="se"&gt;\&lt;/span&gt;
   /tmp/pokedex-a11y-ref/packages/ui/src/components/pokemon-card.tsx &lt;span class="se"&gt;\&lt;/span&gt;
   /tmp/pokedex-a11y-ref/packages/ui/src/components/empty-slot.tsx packages/ui/src/components/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;type-badge.tsx&lt;/code&gt; takes the two-surface decision the matrix is about to prove. The other three are&lt;br&gt;
the same fault on smaller controls: the back pill's dark scrim, which the next section measures&lt;br&gt;
alongside the badge's; the card's remove badge, whose touch target &lt;em&gt;Same bar for every remote&lt;/em&gt;&lt;br&gt;
comes back to; and the two secondary-text lines the card and the empty slot paint, which the&lt;br&gt;
section after that repairs.&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;pkg &lt;span class="k"&gt;in &lt;/span&gt;packages/ui packages/detail&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="nv"&gt;$pkg&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm pkg &lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="s1"&gt;'scripts.test:a11y=jest --testPathPattern accessibility --reporters=default --reporters=@pokedex/a11y-testing/reporter.js'&lt;/span&gt; &lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;packages/detail&lt;/code&gt; needs one more script change, and skipping it publishes a package that cannot be imported. Its suite lives in &lt;code&gt;__tests__&lt;/code&gt;, so its &lt;code&gt;tsconfig.json&lt;/code&gt; now includes that directory, which gives &lt;code&gt;tsc&lt;/code&gt; two roots and moves the output down to &lt;code&gt;dist/src/&lt;/code&gt;. &lt;code&gt;package.json&lt;/code&gt; still declares &lt;code&gt;dist/index.js&lt;/code&gt;. So set &lt;code&gt;packages/detail&lt;/code&gt;'s build script by hand, in its &lt;code&gt;package.json&lt;/code&gt;, to the config that keeps &lt;code&gt;src&lt;/code&gt; as the only root, clearing &lt;code&gt;dist&lt;/code&gt; first so nothing from the old layout survives:&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;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node -e &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;require('fs').rmSync('dist',{recursive:true,force:true})&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; &amp;amp;&amp;amp; tsc -p tsconfig.build.json"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;tsc&lt;/code&gt; that exits 0 and writes the files one directory too deep is the same fault as a green check measuring the wrong thing: nothing complains until whichever app installs the package first stops with &lt;em&gt;Cannot find module&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;packages/ui&lt;/code&gt; already had unit tests for its components. It gains an accessibility layer beside them, and the token matrix is the interesting half. Every foreground and background pair the design system promises is checked once, in the package that owns the tokens:&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="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;WCAG 1.4.3 Contrast (Minimum) — type colours on the solid fill&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;TYPE_NAMES&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;%s badge text clears AA on its type fill&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;type&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="nf"&gt;expectColorContrast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nf"&gt;hexForClass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;textOnTypeClass&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="nf"&gt;hexForClass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;bgClassForType&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;normalText&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;hexForClass&lt;/code&gt; is the interesting part, and it is there because the first version of this file did not have it. That version mapped the class to a literal: &lt;code&gt;'text-black': '#000000'&lt;/code&gt;. All eighteen types passed, and the number it printed was true of a colour the app never painted.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;The design system defines its own &lt;code&gt;black&lt;/code&gt;.&lt;/strong&gt; &lt;code&gt;tailwind.preset.js&lt;/code&gt; sets &lt;code&gt;black: '#2E3138'&lt;/code&gt; for near-black surfaces, which shadows Tailwind's default. So &lt;code&gt;text-black&lt;/code&gt; painted &lt;code&gt;#2E3138&lt;/code&gt;, and on the water fill that is 3.74:1 where the test itself demanded 4.5:1. Two of the eighteen were failing on screen while the matrix reported all eighteen green, because the matrix was measuring a constant instead of the token.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The repair is in two parts, and only the second one is durable. The badge foreground moved to its own token, &lt;code&gt;typeInk&lt;/code&gt;, which is the real black the contrast map was computed against all along. And the matrix stopped writing hex at all: it resolves &lt;strong&gt;both sides of every pair&lt;/strong&gt; out of the preset, so a token that is renamed or shadowed fails the suite rather than slipping past it. Both sides matter. An interim version resolved only the foreground and kept reading backgrounds from the token module, which left the same hole open on the other half: darkening a type fill to 1.64:1 still left all eighteen pairs green. Point &lt;code&gt;typeInk&lt;/code&gt; back at &lt;code&gt;#2E3138&lt;/code&gt; in the preset now and six checks go red immediately: the water and psychic badges on their own fills, the rock, ghost and dragon badges on the hero surface, and the check that pins the preset to the token modules.&lt;/p&gt;

&lt;p&gt;All eighteen fills pass now, and the sentence means something it did not mean before. &lt;strong&gt;A pair that passes here passes in every remote that composes it&lt;/strong&gt;, and nobody re-checks a type badge in the Party app.&lt;/p&gt;

&lt;p&gt;"Every remote that composes it" is carrying weight in that sentence, and it is worth being precise about what composing a pair means. A badge on a card sits on the type colour. A badge on the detail hero does not: the hero &lt;em&gt;is&lt;/em&gt; the type colour, so a solid pill would vanish into it, and the hero variant lays a 30% white scrim down instead. That scrim is translucent but it is not transparent, and the foreground chosen for the solid fill is the wrong one for four of the six types dark enough to take white text on it. Rock, ghost, dragon and steel were landing at 3.17:1, 3.14:1, 3.39:1 and 2.71:1 on the surface they were actually drawn on, while the matrix checked a surface they were not.&lt;/p&gt;

&lt;p&gt;So there are two decisions, not one, and the design system now computes both. The matrix composites the scrim exactly as the runtime composites it and checks the badge against that:&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;scrim&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;composite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;hexForClass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bg-white&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;HERO_SCRIM_ALPHA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;hexForClass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;bgClassForType&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="nf"&gt;expectColorContrast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;hexForClass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;textOnHeroScrimClass&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;scrim&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;normalText&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;Both faults have the same shape: a check measuring something adjacent to what ships. No green suite reveals it.&lt;/p&gt;

&lt;p&gt;The floating back pill is the third of that shape, one control further out. It lays its own&lt;br&gt;
translucent scrim over whichever hero it lands on, and it was painting &lt;code&gt;bg-black/35&lt;/code&gt;, the&lt;br&gt;
near-black neutral again, at 35%. On the three palest fills the white chevron came out at 2.81:1&lt;br&gt;
on flying, 2.83 on ice and 2.84 on electric, under the 3:1 SC 1.4.11 asks of a control's own&lt;br&gt;
boundary. Real black at the same alpha clears every fill, worst case 3.48.&lt;/p&gt;

&lt;p&gt;It does not borrow &lt;code&gt;typeInk&lt;/code&gt; for that. &lt;code&gt;typeInk&lt;/code&gt; is a foreground: every use of it is a &lt;code&gt;text-&lt;/code&gt;&lt;br&gt;
class, and the experiment above reverts it to prove badge text moves. A scrim painted from the&lt;br&gt;
same token would be dragged along by that experiment, which is how a decision about badge ink&lt;br&gt;
would come to own an unrelated control. The background half of the same fact gets its own name:&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;BACK_PILL_SCRIM_ALPHA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.35&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;BACK_PILL_SCRIM_CLASS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bg-scrim/35&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 matrix reads the token and the alpha out of that one class, so the value the pill paints and&lt;br&gt;
the value the map composites cannot drift apart.&lt;/p&gt;

&lt;p&gt;One token did not pass. &lt;code&gt;midGrey&lt;/code&gt; (&lt;code&gt;#9A9AB0&lt;/code&gt;) painted secondary text in four places on three light surfaces: two section headings on the detail sheet (2.60:1), the card's number line inside its grey pill (2.46:1), and the disabled Add button's label on its fill (2.02:1). The matrix recorded each surface separately, through a helper the package exports:&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="nf"&gt;knownFinding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;disabled button label on its fill&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;2.02:1, exempt under 1.4.3 Incidental&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;expectColorContrast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;colours&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;midGrey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;colours&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lightGrey&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;knownFinding&lt;/code&gt; wraps &lt;code&gt;it.failing&lt;/code&gt;, which Jest reports as passing, so the suite stays green while the finding stays visible and the reporter lists it apart from the violations. The helper is there because the first version wrote that call by hand, with a title starting &lt;code&gt;(known&lt;/code&gt; that the reporter matched on. A marker that is only a string is a marker one typo away from silence: misspell it and the reporter stops seeing a tracked finding, &lt;code&gt;it.failing&lt;/code&gt; goes on reporting green, and a live violation reads as an ordinary pass. Writing the title in one place is what makes the marker impossible to misspell.&lt;/p&gt;

&lt;p&gt;Two more surfaces joined the list once the matrix started resolving what it measures. The empty slot's caption is &lt;code&gt;text-midGrey/70&lt;/code&gt;, not the token: composited over the app background it is 1.88:1. And everything so far was the light theme. Both remotes mount a theme toggle in their header, so every one of those surfaces has a dark counterpart the design system also composes, and &lt;code&gt;text-midGrey&lt;/code&gt; carried no &lt;code&gt;dark:&lt;/code&gt; override at either site. The card's number line sits on &lt;code&gt;bg-white/10&lt;/code&gt; over near-black at 3.44:1, and the empty slot's caption on navy at 3.82:1. Both are &lt;code&gt;xs&lt;/code&gt; text, so both need 4.5:1.&lt;/p&gt;

&lt;p&gt;Five pairs, then, every one of them below the bar and on a screen both apps ship. They stayed parked for five rounds behind a reason that reads well and does not survive arithmetic: there is no single darker value that fixes them. Past roughly &lt;code&gt;#5F5F6D&lt;/code&gt; the token clears AA on the three light surfaces, and the detail sheet is &lt;code&gt;dark:bg-navy&lt;/code&gt;, so a pair sitting at a comfortable 6.48:1 would drop to 2.84:1. All true, and none of it load-bearing, because nothing here needs a single value. The design system is theme-aware. It already shipped &lt;code&gt;dark:text-lightGrey&lt;/code&gt; in eight other places, and secondary text had simply never been given the same treatment:&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;Text&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;"xs"&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text-darkGrey dark:text-lightGrey"&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;&lt;code&gt;packages/ui&lt;/code&gt; carries two of those four sites, and they arrive with the components copied earlier: the card's number line and the empty slot's caption, which now paints the token at full strength rather than at 70%. The other two are the detail sheet's section headings, so they are a hand edit in &lt;code&gt;packages/detail/src/PokemonDetailView.tsx&lt;/code&gt;, on both of them:&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="c1"&gt;// Both section headings. Secondary text is themed here like every other secondary&lt;/span&gt;
&lt;span class="c1"&gt;// line in the design system; the parked version had no dark override at all.&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;size&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"xs"&lt;/span&gt; &lt;span class="na"&gt;bold&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"uppercase tracking-widest text-darkGrey dark:text-lightGrey"&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 worst of the six pairs is now 6.94:1. Parking a finding is a real option and this post still keeps one, but "it is a palette decision" turned out to be cover for a change that took one class.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Then the matrix went green, and green was not enough.&lt;/strong&gt; Putting &lt;code&gt;text-midGrey&lt;/code&gt; back on the card afterwards left all one hundred and forty-nine checks passing, because a matrix measures pairs and never sees which class a component paints. That is the same hole the host's tab tints fall through, and it takes the same answer: a check that reads the component's own source.&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="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;secondaryClass&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-card.tsx&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;light&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;darkGrey&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;dark&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lightGrey&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;A render assertion would not have closed it. &lt;code&gt;nativewind/test&lt;/code&gt; compiles only the tree handed to &lt;code&gt;render&lt;/code&gt;, so a class a component picks inside its own render never compiles, and the assertion passes on nothing at all.&lt;/p&gt;

&lt;p&gt;One finding stays tracked, and it is not a criterion failure. SC 1.4.3's Incidental exception exempts "text or images of text that are part of an inactive user interface component", so the disabled Add label has no contrast requirement to fail. It is tracked because this project would rather a disabled control stayed readable, and it sits under a &lt;em&gt;Project bars&lt;/em&gt; heading that says so, rather than under 1.4.3 where it would be the same mis-citation the touch-target framing is careful to avoid.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;packages/detail&lt;/code&gt; gets its own suite next, and the shape matters more there than the assertions: a component library that ships its own accessibility tests. The tests hand &lt;code&gt;PokemonDetailView&lt;/code&gt; its props directly, which is the props seam from &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-a11y-testing" rel="noopener noreferrer"&gt;post 8&lt;/a&gt; again. No store, no query client, no navigator. Both consumers inherit whatever this file proves, and when it finds something, one patch release repairs both of them. One ordering note if you run this suite early: it reads the hero-scrim decision out of &lt;code&gt;@pokedex/ui&lt;/code&gt;, and post 11's lockfile pins a version that predates it. The suite runs, and twenty-four of its thirty checks fail, from four causes. Four throw &lt;code&gt;textOnHeroScrimClass is not a function&lt;/code&gt;, which the design-system release further down repairs. One is the Add button's touch target, and one is the sheet's section headings still on the parked token. The other eighteen are one check per type, all saying the same thing about the hero's dex number, which the next section repairs in a single line. Three of those four causes are findings this post is about, so a red run here is the expected shape rather than a broken clone.&lt;/p&gt;

&lt;h2&gt;
  
  
  A failing test, then the fix
&lt;/h2&gt;

&lt;p&gt;The failure this suite was built to catch did not happen. The expectation was the disabled Add button reporting no &lt;code&gt;accessibilityState&lt;/code&gt;, so a screen reader would meet a button that greys out on screen and says nothing about being unavailable. It reports the state correctly. React Native's &lt;code&gt;Pressable&lt;/code&gt; copies the &lt;code&gt;disabled&lt;/code&gt; prop into &lt;code&gt;accessibilityState&lt;/code&gt; itself:&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;_accessibilityState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nx"&gt;disabled&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{...&lt;/span&gt;&lt;span class="nx"&gt;_accessibilityState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;_accessibilityState&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test stays anyway, as a regression net rather than a fix. The day someone disables that button by swapping the handler for a no-op and greying the fill by hand, the state goes quiet and this test says so.&lt;/p&gt;

&lt;p&gt;What did fail was the check beside it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;● WCAG 2.5.5 Target Size — the Add action › the Add button declares at least 44pt on both axes

  Element has no measurable size and no hitSlop; cannot verify the 44pt touch target.
  Give the control an explicit width/height or a hitSlop, or assert on a parent that
  has one.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things sit behind that line, and both reach past this button.&lt;/p&gt;

&lt;p&gt;The button declared no size the suite could read. Its height came from gluestack's &lt;code&gt;size="lg"&lt;/code&gt; variant, which composes &lt;code&gt;px-6 h-11&lt;/code&gt; while the component renders. In the app that class is fine: Tailwind scans the design system's source, finds the literal &lt;code&gt;h-11&lt;/code&gt;, and the built bundle carries the rule. In the test tree it is not, and the reason is broader than this button. &lt;code&gt;nativewind/test&lt;/code&gt; compiles the class strings it finds on the element tree you hand to &lt;code&gt;render&lt;/code&gt;, and feeds those to Tailwind as its content. A class that appears inside a child component is not on that tree, so it never compiles: not one a variant builds, and not a literal one either. The Add button arrived carrying &lt;code&gt;{"alignSelf": "stretch"}&lt;/code&gt; and nothing else, and the helper throws on nothing, which is the only reason this surfaced.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;And &lt;code&gt;h-11&lt;/code&gt; is not 44 anyway.&lt;/strong&gt; Tailwind generates &lt;code&gt;.h-11 { height: 2.75rem }&lt;/code&gt;, and NativeWind's rem on React Native is 14, not the browser's 16. A literal &lt;code&gt;&amp;lt;View className="h-11" /&amp;gt;&lt;/code&gt; renders &lt;code&gt;height: 38.5&lt;/code&gt;, in the test tree and in the app alike. So the size that variant asks for was under Apple's 44 all along, whether or not any suite could see it. Two separate faults, one control: a size the test could not read, and a size that would not have cleared the bar if it had. Touch-target checks read declared style and props for this reason, rather than deriving a number from a class.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The fix puts the bar where a variant cannot quietly move it. In &lt;code&gt;packages/detail/src/PokemonDetailView.tsx&lt;/code&gt;, on the Add button:&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="c1"&gt;// The 44pt bar is declared here rather than inherited from the size variant.&lt;/span&gt;
&lt;span class="c1"&gt;// A variant is a visual decision that can change; the minimum tappable size of&lt;/span&gt;
&lt;span class="c1"&gt;// the screen's one primary action is a commitment, and the accessibility suite&lt;/span&gt;
&lt;span class="c1"&gt;// can only verify what the control actually declares. `alignSelf: 'stretch'`&lt;/span&gt;
&lt;span class="c1"&gt;// makes the button far wider than 44, but stretch is a layout instruction, not&lt;/span&gt;
&lt;span class="c1"&gt;// a measurement, so the minimum is stated on both axes.&lt;/span&gt;
&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="na"&gt;alignSelf&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stretch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;minWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;44&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;minHeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;44&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same file carried the opposite fault, a hundred lines up, in the hero the button sits under. The&lt;br&gt;
hero paints the type's colour full strength, and a comment above it explains that its text cannot be a&lt;br&gt;
fixed colour, because the design system already decided per type and the hero should ask the token&lt;br&gt;
rather than assume. The dex number under the name did assume. It muted itself:&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="c1"&gt;// Before: a second answer, invented one line under the comment saying not to.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;heroMuted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;heroInk&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text-white/70&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;text-black/60&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;&lt;code&gt;text-black&lt;/code&gt; resolves to &lt;code&gt;#2E3138&lt;/code&gt; here, the neutral the preset warns is not a foreground, and at&lt;br&gt;
60% over the fill it measures 2.21:1 on water. Sixteen of the eighteen types fail. No alpha rescues&lt;br&gt;
it: sweep the value from 1% to 100% and the best case is full strength, where water still sits at&lt;br&gt;
3.74:1, because the ink is the wrong ink. So the line asks what the name asks, in&lt;br&gt;
&lt;code&gt;packages/detail/src/PokemonDetailView.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="c1"&gt;// Delete heroMuted. The dex number takes the same decision the name takes.&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;size&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"sm"&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`font-head &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;onHero&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;dexNumber&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;Worst case becomes steel at 4.71:1, and the pair is one the token matrix already measures, so&lt;br&gt;
nothing new needs adding to it. A muted variant would have needed its own row.&lt;/p&gt;

&lt;p&gt;The Add button's fault sits one level lower too. The design system's &lt;code&gt;ErrorState&lt;/code&gt; builds its retry from &lt;code&gt;size="md"&lt;/code&gt;, which is &lt;code&gt;h-10&lt;/code&gt;, or 35 points, and that retry is the way back from a failed load in all three apps. Its declaration goes into the design system rather than into whichever app noticed first, in &lt;code&gt;packages/ui/src/components/error-state.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="c1"&gt;// The retry is the way back from a failed load, so its tappable size is declared here&lt;/span&gt;
&lt;span class="c1"&gt;// rather than left to the size variant. A variant is a visual decision; the minimum&lt;/span&gt;
&lt;span class="c1"&gt;// target is a commitment, and a declared one is the only kind a suite can verify.&lt;/span&gt;
&lt;span class="c1"&gt;// Both axes are declared: the button is far wider than 44 in every layout it appears in,&lt;/span&gt;
&lt;span class="c1"&gt;// but a width nobody states is a width nobody has measured.&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt;
  &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"primary"&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;"md"&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="nx"&gt;onRetry&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;minWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;44&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;minHeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;44&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 more finding sits beside them, in two components at once, and it is the one a sighted review never catches at all. Neither &lt;code&gt;ErrorState&lt;/code&gt; nor &lt;code&gt;LoadingState&lt;/code&gt; had any status semantics: no role, no live region, nothing. They looked correct, and to a screen reader a failed load simply happened in silence.&lt;/p&gt;

&lt;p&gt;They do not get the same repair, because they are not the same kind of message. A failed load should interrupt; a spinner should not. So &lt;code&gt;error-state.tsx&lt;/code&gt; becomes an alert, which announces on both platforms without a live region:&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;Center&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"flex-1 px-6"&lt;/span&gt; &lt;span class="na"&gt;accessible&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;"alert"&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;and &lt;code&gt;loading-state.tsx&lt;/code&gt; gets the polite variant, which waits for whatever the screen reader is already saying to finish:&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;Center&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"flex-1"&lt;/span&gt; &lt;span class="na"&gt;accessible&lt;/span&gt; &lt;span class="na"&gt;accessibilityLiveRegion&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"polite"&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 design system carries five of these repairs and &lt;code&gt;packages/detail&lt;/code&gt; three, so each is one release&lt;br&gt;
rather than several:&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 version 1.0.13 &lt;span class="nt"&gt;--no-git-tag-version&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;&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; @pokedex/ui@1.0.13 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm version 4.0.12 &lt;span class="nt"&gt;--no-git-tag-version&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;The host takes the release too, and it has a repair of its own, the only fault in this post&lt;br&gt;
that lives in the shell rather than in a package or a remote:&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; /tmp/pokedex-a11y-ref/apps/host/App.tsx apps/host/
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-a11y-ref/apps/host/__tests__/TabBar.test.tsx apps/host/__tests__/
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-a11y-ref/apps/host/tsconfig.json apps/host/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both tab labels are 10pt, and both were failing. The focused one took &lt;code&gt;colours.blue&lt;/code&gt;, the brand&lt;br&gt;
fill: 3.48:1 on the light bar, 3.74:1 on the dark. The unfocused one was never set at all, so&lt;br&gt;
React Navigation derived it by mixing the theme's text halfway into the bar's own colour, which&lt;br&gt;
gives 3.27:1 on white. One tab is always unfocused, so that pair is on screen whenever the&lt;br&gt;
app is. &lt;code&gt;colours.ts&lt;/code&gt; brings two readable blues with it; the greys are the ones every other&lt;br&gt;
secondary line already uses:&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="nx"&gt;tabBarActiveTintColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dark&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;colours&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;blueTextDark&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;colours&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;blueText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="nx"&gt;tabBarInactiveTintColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dark&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;colours&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lightGrey&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;colours&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;darkGrey&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;TabBar.test.tsx&lt;/code&gt; is why the guard is worth its own file. The matrix in &lt;code&gt;@pokedex/ui&lt;/code&gt; proves those&lt;br&gt;
four pairs clear AA, and it cannot prove the host uses them: reverting the active tint to&lt;br&gt;
&lt;code&gt;colours.blue&lt;/code&gt; leaves every one of the design system's checks green. A matrix measures pairs; only&lt;br&gt;
the app can be asked whether it composes them. It reads source rather than a render, because a&lt;br&gt;
navigator option is resolved inside React Navigation and reaches no element a suite can query.&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/ui@1.0.13 &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Two patch releases, three apps, and no coordination between the teams that ship them.&lt;/strong&gt; The caret range every consumer already carried is what makes this a patch rather than a negotiation. The Pokédex suite records what that means in the place it is easiest to misread:&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="c1"&gt;// This passes because the design system declares the size on ErrorState's button, not&lt;/span&gt;
&lt;span class="c1"&gt;// because this app did anything. One package release, every remote's retry covered.&lt;/span&gt;
&lt;span class="nf"&gt;expectMinTouchTarget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;buttonContaining&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Try again&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 renders the same &lt;code&gt;ErrorState&lt;/code&gt; in its remote-load boundary, so it moves to the same version as the two remotes rather than lagging behind them, which for a package the host provides as an eager singleton is not optional.&lt;/p&gt;

&lt;p&gt;How far that retry gets you depends on what failed. A failed data request retries cleanly. A remote that never answered is a harder case: the boundary discards React's cached rejection and starts a fresh import, which is why the button visibly does something, but the same failure comes back every time. The runtime behaves as though it is still holding the failed manifest fetch underneath it. That is an observation rather than a documented guarantee, so treat the behaviour as the finding and not the cause. Clearing it needs cache-busting the runtime, and cache-busting is only worth having once there is something to fall back to, so both arrive together in the resilience post later in this series. What the checks in this post can say is narrower and still worth saying: whatever the retry does, it is reachable, it is named, and it is big enough to hit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same bar for every remote
&lt;/h2&gt;

&lt;p&gt;Both remotes install the same three devDependencies, and take the two fixed packages in the same 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/list &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install&lt;/span&gt; @pokedex/detail@4.0.12 @pokedex/ui@1.0.13 &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; @pokedex/a11y-testing@1.0.18 @testing-library/react-native@13.3.3 @tailwindcss/container-queries@0.1.1 &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 &lt;span class="nb"&gt;install&lt;/span&gt; @pokedex/detail@4.0.12 @pokedex/ui@1.0.13 &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; @pokedex/a11y-testing@1.0.18 @testing-library/react-native@13.3.3 @tailwindcss/container-queries@0.1.1 &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each app takes its own suite, its Jest config and its not-applicable declarations, and the README catches up with 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;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-a11y-ref/apps/&lt;span class="nv"&gt;$app&lt;/span&gt;/jest.config.js &lt;span class="se"&gt;\&lt;/span&gt;
     /tmp/pokedex-a11y-ref/apps/&lt;span class="nv"&gt;$app&lt;/span&gt;/a11y-report.config.js &lt;span class="se"&gt;\&lt;/span&gt;
     /tmp/pokedex-a11y-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="nb"&gt;cat&lt;/span&gt; /tmp/pokedex-a11y-ref/apps/&lt;span class="nv"&gt;$app&lt;/span&gt;/.gitignore &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; apps/&lt;span class="nv"&gt;$app&lt;/span&gt;/.gitignore
&lt;span class="k"&gt;done
&lt;/span&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-a11y-ref/apps/list/__tests__/ListStack.accessibility.tsx apps/list/__tests__/
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-a11y-ref/apps/party/__tests__/PartyStack.accessibility.tsx apps/party/__tests__/
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-a11y-ref/apps/list/src/PokedexScreen.tsx apps/list/src/
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-a11y-ref/apps/party/src/PartyScreen.tsx apps/party/src/
&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-a11y-ref/apps/party/__mocks__/styleMock.js apps/party/__mocks__/
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-a11y-ref/README.md &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two screens carry the counter repairs. &lt;code&gt;PokedexScreen.tsx&lt;/code&gt; has the live-region fix below, and&lt;br&gt;
both screens paint the same count pill, whose numeral was &lt;code&gt;text-darkGreen&lt;/code&gt; on &lt;code&gt;bg-lightGreen&lt;/code&gt;:&lt;br&gt;
&lt;strong&gt;1.53:1&lt;/strong&gt;, 10.5pt bold, in each remote's header. &lt;code&gt;darkGreen&lt;/code&gt; is &lt;code&gt;#A6D3A0&lt;/code&gt;, the same value as&lt;br&gt;
the grass fill, so the name was the only dark thing about it. Both now use &lt;code&gt;text-darkGrey&lt;/code&gt;, the&lt;br&gt;
colour the label beside them already used. At 7.18:1, it is a pair the token matrix has held&lt;br&gt;
all along. The counter was composing a different one nobody had measured.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;styleMock.js&lt;/code&gt; is duller and just as necessary, and it is there because &lt;code&gt;PartyStack&lt;/code&gt; pulls in &lt;code&gt;global.css&lt;/code&gt;, one hop away through &lt;code&gt;./styles&lt;/code&gt;, so the remote's&lt;br&gt;
classes reach the shared styling runtime when the host loads the exposed module. The app's own&lt;br&gt;
entry already covers the standalone build; without the import in &lt;code&gt;PartyStack&lt;/code&gt; the styles work&lt;br&gt;
standalone and quietly do nothing federated. Jest has no CSS loader, so the copied config maps&lt;br&gt;
that import to the stub. Without it the Party suite does not fail; it refuses to run.&lt;/p&gt;

&lt;p&gt;Then the script that runs the accessibility files through the reporter:&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;apps/list apps/party&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="nv"&gt;$app&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm pkg &lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="s1"&gt;'scripts.test:a11y=jest --testPathPattern accessibility --reporters=default --reporters=@pokedex/a11y-testing/reporter.js'&lt;/span&gt; &lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both Jest configs point at the shared preset. Because it extends the React Native preset, the suites each app already had keep running, and the rule is to widen its allowlist rather than replace it. In outline, since the copied files carry more entries than this:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;preset&lt;/span&gt; &lt;span class="o"&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/a11y-testing/jest-preset&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&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;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;@pokedex/a11y-testing&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="s2"&gt;`node_modules/(?!(&lt;/span&gt;&lt;span class="p"&gt;${[...&lt;/span&gt;&lt;span class="nx"&gt;preset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uncompiledPackages&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&lt;/span&gt;&lt;span class="dl"&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="s2"&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;Replacing that array is how a shared preset quietly stops being shared. It does not fail quietly, though: the styling stack ships ES modules, so Jest hits &lt;code&gt;SyntaxError: Cannot use import statement outside a module&lt;/code&gt; and runs nothing at all. That is the good case. The trap is that the error names a file deep in &lt;code&gt;node_modules&lt;/code&gt; and reads like a broken dependency rather than like a config line the app owns. Exporting the list makes widening the easy move.&lt;/p&gt;

&lt;p&gt;Each suite covers only what its team composes, and the way to keep that honest is to render the team's own screens rather than the design system's components. Both files mount the real stack through the real store and the real navigator, the same harness the apps' existing tests use:&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;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="k"&gt;for &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;member&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;party&lt;/span&gt;&lt;span class="p"&gt;)&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="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="nx"&gt;member&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;renderWithTheme&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="na"&gt;initialMetrics&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;metrics&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;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;PartyStack&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;&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is more setup than mounting a &lt;code&gt;PokemonCard&lt;/code&gt; directly, and it is the difference between testing this app and testing somebody else's package. A suite that renders design-system components re-checks what the source already settled, which is exactly the duplication the source contract exists to remove.&lt;/p&gt;

&lt;p&gt;So the Pokédex checks the rows its own query data produces, the states its screen can land in, and its party counter. The Party checks its grid: a filled slot is a button naming its Pokémon, the empty ones are named gaps rather than four identical silences, and removal is an accessibility action on the card rather than a second focus stop beside 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="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;getByLabelText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^Pikachu, number 025/&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;accessibilityActions&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toEqual&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;remove&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Remove from 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;The ✕ badge is small (21 points, because &lt;code&gt;h-6&lt;/code&gt; is 1.5rem and NativeWind's rem is 14, so its &lt;code&gt;hitSlop&lt;/code&gt; carries it to 45 rather than the 41 it had), and making it its own focus stop would double the number of things to swipe past on a full party. The design system hides it and exposes removal as an action instead, so a screen-reader user reaches it through VoiceOver's rotor or TalkBack's actions menu. The check is that the action exists at all, because a hidden control with nothing behind it is simply unreachable.&lt;/p&gt;

&lt;p&gt;Rendering the real screen is also what turned the counter's second fault from a note into a fix. The Pokédex header shows "My Party" beside a count out of six, and that count changes when something is added from a screen away, without focus moving. A sighted user watches the number tick; a screen-reader user was told nothing, because the header was static text. That is the whole of SC 4.1.3, and it is now a live region with a label that spells the ratio out, because a bare "3/6" is left to how a given screen reader says 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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Box&lt;/span&gt;
  &lt;span class="na"&gt;accessible&lt;/span&gt;
  &lt;span class="na"&gt;accessibilityLiveRegion&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"polite"&lt;/span&gt;
  &lt;span class="na"&gt;accessibilityLabel&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`My Party, &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;partyCount&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; of &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="s2"&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;The Party's own header needs the same three props, and that is the cost of a fault living in app code twice over: two screens, two suites, and nothing that makes the second one look.&lt;/p&gt;

&lt;p&gt;A test asserting that against a hand-written object would have proved nothing about the app. Against the rendered screen it fails if the props are removed, which is the only version worth keeping.&lt;/p&gt;

&lt;h2&gt;
  
  
  The report
&lt;/h2&gt;

&lt;p&gt;One &lt;code&gt;accessibility-report.md&lt;/code&gt; comes out of each suite, written by the reporter as the run finishes:&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 run &lt;span class="nb"&gt;test&lt;/span&gt;:a11y &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reporter reads the criterion out of each describe title, so &lt;code&gt;WCAG 1.4.3 …&lt;/code&gt; is all the wiring there is. The design system's run opens like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Accessibility report — ui&lt;/span&gt;

Automated coverage: &lt;span class="gs"&gt;**5 of 13**&lt;/span&gt; WCAG 2.1 A + AA criteria that a Jest suite can decide,
after 2 declared not applicable here.

&lt;span class="gu"&gt;## Violations (0)&lt;/span&gt;

None.

&lt;span class="gu"&gt;## Known findings (0)&lt;/span&gt;

None.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exempt pair does not appear there. It has its own heading, because a threshold a project&lt;br&gt;
chooses is not a result against a criterion, and filing one as the other is how a coverage&lt;br&gt;
number stops meaning anything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Project bars&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; &lt;span class="gs"&gt;**not held**&lt;/span&gt; — pairs held above what the criteria require · disabled button label
  on its fill (known: 2.02:1, exempt under 1.4.3 Incidental)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The denominator is where a coverage number is easiest to inflate, so it is built in two steps. The catalogue carries all 50 of WCAG 2.1's A and AA criteria and tags each one with the layer that can decide it: 15 a Jest process can, and the rest belonging to the device audit, the manual pass, or to nothing here at all, like the audio and video criteria in an app with neither. A suite then removes the ones it declares inapplicable, which is why the design system is measured out of 13. Those declarations live in an &lt;code&gt;a11y-report.config.js&lt;/code&gt; at the suite root, and each carries a reason, whether the criterion was in the counted set or not. Nothing enforces that the reason is any good; it is there so a reviewer can disagree with it.&lt;/p&gt;

&lt;p&gt;The touch-target work appears in the reports of the suites that do it, and never in that fraction. The design system's run says so in its own section:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Checked beyond the counted scope&lt;/span&gt;

These ran and are reported, but they sit outside the WCAG 2.1 A + AA set the coverage
number is measured against, so they do not raise it.
&lt;span class="p"&gt;
-&lt;/span&gt; &lt;span class="gs"&gt;**WCAG 2.5.5**&lt;/span&gt; not in the A + AA catalogue — 4 checks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SC 2.5.5 is Level AAA, so counting it would inflate an A and AA figure. Dropping it silently would hide a check that ran. The report does neither.&lt;/p&gt;

&lt;p&gt;That is roughly the artefact a paid accessibility scanner sells, generated from tests the team already owns.&lt;/p&gt;

&lt;h2&gt;
  
  
  What these checks cannot see
&lt;/h2&gt;

&lt;p&gt;A green suite here proves less than it looks like it proves.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;What it proves&lt;/th&gt;
&lt;th&gt;What it cannot&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Jest suite&lt;/td&gt;
&lt;td&gt;token pairs, declared sizes, the name, role and state a control exposes&lt;/td&gt;
&lt;td&gt;what a device paints, real traversal order, hit regions after clipping&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Device audit&lt;/td&gt;
&lt;td&gt;contrast as drawn, clipped targets, sizing on a real screen&lt;/td&gt;
&lt;td&gt;whether a label reads well out loud&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manual pass&lt;/td&gt;
&lt;td&gt;the actual experience, traversal order included&lt;/td&gt;
&lt;td&gt;catching every regression, on every commit&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The device audit is &lt;code&gt;performAccessibilityAudit&lt;/code&gt; on iOS and the Accessibility Test Framework on Android; the manual pass is a person with VoiceOver or TalkBack.&lt;/p&gt;

&lt;p&gt;Focus order sits with the manual pass, which is the honest answer to the third thing post 11 promised. A Jest suite can say that focusable elements are present. It cannot say what order a screen reader traverses them in, because that order comes out of layout on a real screen. Nor is the device layer a clean home for it: Android's Accessibility Test Framework has a traversal-order check and iOS's &lt;code&gt;performAccessibilityAudit&lt;/code&gt; has no equivalent, and a criterion only one platform can automate is not one a report should claim. So it stays with the person driving the screen reader, which is where the catalogue tags it.&lt;/p&gt;

&lt;p&gt;The layers are not a ladder where the top one contains the ones below it. The token matrix checks the pairs the design system composes, on the surfaces it composes them on, whether or not a screen shows a given pair today; a device audit checks what is on the screen it was pointed at, which is a different set and never the whole palette. &lt;strong&gt;A clean automated run is necessary and not sufficient&lt;/strong&gt;, and the same is true of a clean device pass.&lt;/p&gt;

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

&lt;p&gt;Every suite, including the ones that predate this post:&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/a11y-testing &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;test&lt;/span&gt; &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/ui &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;test&lt;/span&gt; &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;test&lt;/span&gt; &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;test&lt;/span&gt; &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;test&lt;/span&gt; &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/host &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first line is the bar testing itself. A package that tells four workspaces what accessible&lt;br&gt;
means should be able to prove its own helpers, and the cases in there are the ones earlier&lt;br&gt;
versions got wrong.&lt;/p&gt;

&lt;p&gt;Then the accessibility layer on its own, with the report:&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 run &lt;span class="nb"&gt;test&lt;/span&gt;:a11y &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 run &lt;span class="nb"&gt;test&lt;/span&gt;:a11y &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 run &lt;span class="nb"&gt;test&lt;/span&gt;:a11y &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 run &lt;span class="nb"&gt;test&lt;/span&gt;:a11y &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No simulator and no dev servers. Every package was published as it was built, so nothing here republishes anything.&lt;/p&gt;

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

&lt;p&gt;One package now holds the accessibility bar for the whole federation, and five suites run against it: the bar's own helpers, the tokens and components in &lt;code&gt;@pokedex/ui&lt;/code&gt;, the shared screen in &lt;code&gt;@pokedex/detail&lt;/code&gt;, and each remote's own screens.&lt;/p&gt;

&lt;p&gt;What came out of it divides by where the fault lived, and that division is the point. &lt;code&gt;@pokedex/ui&lt;/code&gt; held eleven: two badge foregrounds below AA on their own fill, four more below it on the hero scrim, the back pill's glyph below the non-text bar on three of the eighteen heroes, two secondary-text lines failing in both themes, and two components that announced a failed load in silence. Add the retry button's touch target and the card badge's, and it is thirteen. All three apps took every one of them by a version bump, which is the whole argument for a design system stated as a number.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;@pokedex/detail&lt;/code&gt; held three, and the host does not install it, so all three stopped at the two remotes: the Add button's target, the hero's dex number, and the sheet's two section headings.&lt;/p&gt;

&lt;p&gt;Four could not travel at all, and they are the interesting ones. Two are the host's own tab labels, in the shell that owns no screens. The other two are the party counter: app code, the same header written twice, once in each remote, owned by no package. It carried one fault of each kind, a count that announced nothing and a numeral at 1.53:1 on its own pill. Each of those four had to be fixed where it sat, and two of them had to be fixed twice.&lt;/p&gt;

&lt;p&gt;The faults that mattered more were in the checks. This post showed four: a helper that borrowed the bar for an axis nobody had declared, a matrix reading a constant instead of the token, the same matrix reading the wrong surface, and a marker that was only a string. More turned up afterwards, in the design system's suites and in the bar's own. A scrim guard in the design system compared two constants exported from the same module. In a clamp test the value never reached the ceiling it was clamping to. The bar's own use-of-colour helper read screen-reader metadata rather than the text on screen. And a target guard fell back to a number written in the test when its own pattern stopped matching. Every one of them now has a case that fails when the repair is taken out again, which is the only evidence a guard is real. &lt;strong&gt;A check aimed slightly beside the thing it names reports green, and goes on reporting green.&lt;/strong&gt; Nothing in the output distinguishes it from a check that works.&lt;/p&gt;

&lt;p&gt;Nothing stays open against a criterion. Five pairs did for five rounds, all of them the same secondary-text token, parked on the reasoning that no single darker value clears every surface. That was true and it was answering the wrong question: the design system is theme-aware, and the pairs took a themed class rather than a new token. One finding stays tracked and it is not a criterion result, because SC 1.4.3 exempts an inactive control's label. The report prints it under &lt;em&gt;Project bars&lt;/em&gt;, counted nowhere.&lt;/p&gt;

&lt;p&gt;The honest limit is the layer. This post builds the Jest layer and neither of the other two. The device audit harness is real work that has not been done here, and the manual VoiceOver and TalkBack pass is not automatable by design. This layer exists for the regression: the things you have already fixed staying fixed, on every commit, in every remote, against one definition.&lt;/p&gt;

&lt;p&gt;Next: the host lends the remotes its native side. A navigation bridge launches a native screen from the Party tab and waits for its result.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.w3.org/WAI/WCAG22/Understanding/target-size-enhanced.html" rel="noopener noreferrer"&gt;WCAG 2.2, SC 2.5.5 Target Size (Enhanced)&lt;/a&gt; — Level AAA, 44 by 44 CSS pixels&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.w3.org/WAI/WCAG22/Understanding/target-size-minimum.html" rel="noopener noreferrer"&gt;WCAG 2.2, SC 2.5.8 Target Size (Minimum)&lt;/a&gt; — the AA criterion, 24 by 24 with five exceptions&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; — 4.5:1 for normal text, 3:1 for large&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.w3.org/TR/WCAG22/#dfn-relative-luminance" rel="noopener noreferrer"&gt;WCAG 2.2, relative luminance&lt;/a&gt; — the formula, and the note recording the May 2021 change from 0.03928 to 0.04045&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.apple.com/design/human-interface-guidelines/accessibility" rel="noopener noreferrer"&gt;Apple Human Interface Guidelines: Accessibility&lt;/a&gt; — the current control-size table: 44 by 44 points as the iOS and iPadOS default, 28 by 28 as the minimum&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.apple.com/design/tips/" rel="noopener noreferrer"&gt;Apple: UI Design Dos and Don'ts&lt;/a&gt; — the older page, "at least 44 points x 44 points"&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://support.google.com/accessibility/android/answer/7101858" rel="noopener noreferrer"&gt;Android accessibility help: touch target size&lt;/a&gt; — the 48dp guidance&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/nickcolley/jest-axe" rel="noopener noreferrer"&gt;jest-axe&lt;/a&gt; — the jsdom requirement and the disabled contrast checks&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:32019L0882" rel="noopener noreferrer"&gt;Directive (EU) 2019/882&lt;/a&gt; — the European Accessibility Act, applied from 28 June 2025&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.etsi.org/deliver/etsi_en/301500_301599/301549/03.02.01_60/en_301549v030201p.pdf" rel="noopener noreferrer"&gt;EN 301 549&lt;/a&gt; — the European standard that applies WCAG 2.1 to mobile apps&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://oss.callstack.com/react-native-testing-library/" rel="noopener noreferrer"&gt;React Native Testing Library&lt;/a&gt; — the queries and matchers this builds on&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.npmjs.com/package/nativewind" rel="noopener noreferrer"&gt;nativewind&lt;/a&gt; — &lt;code&gt;nativewind/test&lt;/code&gt;, shipped and undocumented&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.npmjs.com/package/react-native-css-interop" rel="noopener noreferrer"&gt;react-native-css-interop&lt;/a&gt; — the &lt;code&gt;toHaveStyle&lt;/code&gt; matcher, and the rem this stack resolves 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-12-a11y-testing&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 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>
  </channel>
</rss>
