<?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: Famitha M A</title>
    <description>The latest articles on DEV Community by Famitha M A (@famitha_ma_b9c13ab1d324e).</description>
    <link>https://dev.to/famitha_ma_b9c13ab1d324e</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%2F3845749%2Fd010c225-1562-434d-af39-d6eaa878a81c.png</url>
      <title>DEV Community: Famitha M A</title>
      <link>https://dev.to/famitha_ma_b9c13ab1d324e</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/famitha_ma_b9c13ab1d324e"/>
    <language>en</language>
    <item>
      <title>The Design System of a Trustworthy React Native Checkout Screen</title>
      <dc:creator>Famitha M A</dc:creator>
      <pubDate>Thu, 20 Aug 2026 09:59:52 +0000</pubDate>
      <link>https://dev.to/famitha_ma_b9c13ab1d324e/the-design-system-of-a-trustworthy-react-native-checkout-screen-3p33</link>
      <guid>https://dev.to/famitha_ma_b9c13ab1d324e/the-design-system-of-a-trustworthy-react-native-checkout-screen-3p33</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Checkout is the most emotionally loaded screen in your app. Design it like it.&lt;/li&gt;
&lt;li&gt;Five zones: header, amount, line items, payment method, sticky Pay button.&lt;/li&gt;
&lt;li&gt;Two type sizes. Not three. Not five.&lt;/li&gt;
&lt;li&gt;No custom fonts on this screen. They delay the render of the total.&lt;/li&gt;
&lt;li&gt;Never green on the Pay button. Never red outside errors.&lt;/li&gt;
&lt;li&gt;The three states everyone skips: loading, error, success. Specs below.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most React Native checkout screens I audit are functional and forgettable. A stack of &lt;code&gt;&amp;lt;View&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;Text&amp;gt;&lt;/code&gt; in the app's default theme. The user taps through and either succeeds or bounces.&lt;/p&gt;

&lt;p&gt;But this is where the user hands you money. Every pixel choice either builds trust or leaks it.&lt;/p&gt;

&lt;p&gt;Here's the whole system, with code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five zones
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────┐
│  ← Back        Order summary │  Zone 1: Header (32pt height)
├─────────────────────────────┤
│                             │
│  Total                      │
│  $42.87 USD                 │  Zone 2: Amount (dominant type)
│                             │
├─────────────────────────────┤
│  Item 1              $30    │
│  Item 2              $10    │  Zone 3: Line items (collapsed)
│  Tax                 $2.87  │
├─────────────────────────────┤
│                             │
│  [ Apple Pay ]              │  Zone 4: Payment method
│  [ Card ending 4242 ]       │
│  [ + Add payment method ]   │
│                             │
├─────────────────────────────┤
│  [    Pay $42.87 USD    ]   │  Zone 5: Pay button (sticky)
└─────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Zone 5 stays sticky above the keyboard. Zone 2 collapses when the keyboard opens so the Pay button never leaves the viewport.&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;KeyboardAvoidingView&lt;/span&gt;
  &lt;span class="na"&gt;behavior&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;Platform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OS&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ios&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;padding&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;height&lt;/span&gt;&lt;span class="dl"&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="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="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;ScrollView&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;flexGrow&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="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;AmountBlock&lt;/span&gt; &lt;span class="na"&gt;total&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;compact&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;keyboardVisible&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;LineItems&lt;/span&gt; &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;items&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;PaymentMethods&lt;/span&gt; &lt;span class="na"&gt;methods&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;methods&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onSelect&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;setMethod&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;ScrollView&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;PayButton&lt;/span&gt; &lt;span class="na"&gt;total&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;currency&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="nx"&gt;handlePay&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;KeyboardAvoidingView&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;
  
  
  Typography: two sizes, that's it
&lt;/h2&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="kd"&gt;const&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;amount&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;32&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="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;letterSpacing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&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="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;15&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;400&lt;/span&gt;&lt;span class="dl"&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;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Platform system font only. SF Pro Display on iOS, Roboto on Android. Skip &lt;code&gt;expo-font&lt;/code&gt; entirely on this screen. Custom font loading is real work on a cold boot, and anything that delays the render of the total is worth cutting. If you want a number for your own app, profile it instead of trusting a blog post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Color tokens
&lt;/h2&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;PlatformColor&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="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;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;color&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;background&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="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;ios&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;PlatformColor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;systemBackground&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;android&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#FFFFFF&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;text&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="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;ios&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;PlatformColor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;label&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;android&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#111111&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;// Brand primary only if it clears 4.5:1 on white. Otherwise black.&lt;/span&gt;
  &lt;span class="na"&gt;payButton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;contrastRatio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;brandPrimary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#FFFFFF&lt;/span&gt;&lt;span class="dl"&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="mf"&gt;4.5&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;brandPrimary&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#000000&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;errorBg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#FFF4F4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;errorText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#B00020&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;Never use red for anything except errors. Never use green on the Pay button. Users associate green with "Buy" but not with "you're about to spend money," which is a subtle difference that still shows up in testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three states everyone forgets
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Loading.&lt;/strong&gt; Swap the label, disable the button, and escalate the copy on a timer. An indeterminate spinner alone reads as a frozen app.&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;usePayingLabel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;processing&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="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;elapsed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setElapsed&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&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;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;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;processing&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;setElapsed&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="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;setInterval&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;setElapsed&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;e&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;e&lt;/span&gt; &lt;span class="o"&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;1000&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;clearInterval&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="nx"&gt;processing&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;processing&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;null&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;elapsed&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;This is taking longer than usual, please wait.&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;elapsed&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Contacting your bank...&lt;/span&gt;&lt;span class="dl"&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;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// button label already says "Processing..."&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;Error.&lt;/strong&gt; Full-width bar above the Pay button. One human sentence, one recovery action. Never the raw API code.&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="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&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="na"&gt;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;errorBg&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;12&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="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="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;errorText&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;15&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;
      Your card was declined by your bank.
    &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;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;resetToPaymentMethod&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="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="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;errorText&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="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Success.&lt;/strong&gt; Full-screen replacement: large checkmark, order ID, amount, timestamp, email receipt button. Hold 3 seconds minimum before any auto-dismiss so the user feels the transaction land.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two components that carry the screen
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Pay button&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="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;52&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;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payButton&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="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;marginHorizontal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}&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="nx"&gt;handlePay&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ready&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;processing&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="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;white&lt;/span&gt;&lt;span class="dl"&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;17&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="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;processing&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Processing...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Pay &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;formatCurrency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;currency&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;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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Amount block&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="na"&gt;paddingHorizontal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;paddingVertical&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="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="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;13&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="nx"&gt;subtle&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;4&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;Total&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="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&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="nf"&gt;formatCurrency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&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="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="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;fontSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;13&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="nx"&gt;subtle&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="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;currency&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="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the 24pt of vertical padding above the total and the 16pt horizontal margin on the button. Those are the two anchor values on the 8pt grid. Density signals cheapness. Whitespace signals trust.&lt;/p&gt;

&lt;p&gt;If you're scaffolding a fresh app rather than retrofitting one, &lt;a href="https://rapidnative.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=react-native-checkout-design-system" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt; generates the screen structure and theme tokens up front, so you're wiring Stripe on day one instead of rebuilding the amount block by hand again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ship it in a sprint
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Day 1:&lt;/strong&gt; Audit the current checkout against the five zones. Find what's missing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Day 2:&lt;/strong&gt; Rebuild the amount zone and the Pay button. Do these first, they carry most of the perceived trust.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Day 3:&lt;/strong&gt; Build the three states.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Day 4:&lt;/strong&gt; Test with five people. Watch where they pause.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Day 5:&lt;/strong&gt; Ship.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rest is plumbing to Stripe or Razorpay.&lt;/p&gt;

&lt;p&gt;What does your checkout look like right now? Drop a comment with the ugliest part of yours and I'll tell you which zone it belongs to.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>ux</category>
      <category>mobile</category>
    </item>
    <item>
      <title>How AI Is Making Mobile Accessibility Easier Than Ever</title>
      <dc:creator>Famitha M A</dc:creator>
      <pubDate>Mon, 17 Aug 2026 08:27:47 +0000</pubDate>
      <link>https://dev.to/rapidnative-ai/how-ai-is-making-mobile-accessibility-easier-than-ever-25cl</link>
      <guid>https://dev.to/rapidnative-ai/how-ai-is-making-mobile-accessibility-easier-than-ever-25cl</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;95.9% of the top million sites have detectable a11y failures. Mobile apps are not better, they're just harder to audit.&lt;/li&gt;
&lt;li&gt;Four things changed: on-device assistive AI, ML-assisted testing, generated alt text and captions, and AI-assisted code generation.&lt;/li&gt;
&lt;li&gt;On-device AI raises the floor, not the ceiling. VoiceOver guessing "Button, likely a heart" is not a label.&lt;/li&gt;
&lt;li&gt;Rule-based tools catch 30 to 40% of WCAG failures. AI pushes that up, nowhere near 100%.&lt;/li&gt;
&lt;li&gt;The real fix is at generation time: if the model writes the JSX, it can write &lt;code&gt;accessibilityLabel&lt;/code&gt; in the same pass, for free.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The gap between apps shipping and apps shipping &lt;em&gt;accessibly&lt;/em&gt; has been widening for a decade. What changed recently is that AI shows up on both sides of the shipping fence: in the runtime layer (the phone), in the tooling layer (audits), and at the code-generation layer, where the app is actually being written.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why mobile a11y is harder than the web
&lt;/h2&gt;

&lt;p&gt;Native apps don't share a common semantic model. On the web you have HTML, and HTML has a built-in accessibility contract. On mobile you have UIAccessibility on iOS, AccessibilityNodeInfo on Android, two screen readers with different gesture conventions, and a framework translation layer (React Native, Flutter, SwiftUI) sitting on top of both.&lt;/p&gt;

&lt;p&gt;That means a label that works on one platform can be silently dropped on the other. Here's the version everyone writes first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Ships. Announces as "Button". Useless.&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;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;toggleFavorite&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;Icon&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isFavorite&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;heart-filled&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="s2"&gt;heart&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;&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;And the version that actually works with VoiceOver and TalkBack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&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="nx"&gt;toggleFavorite&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;"Add to favorites"&lt;/span&gt;
  &lt;span class="na"&gt;accessibilityHint&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Saves this item to your favorites list"&lt;/span&gt;
  &lt;span class="na"&gt;accessibilityState&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;selected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isFavorite&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="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Icon&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isFavorite&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;heart-filled&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="s2"&gt;heart&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;&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;Four extra props. Nobody argues they're hard. They just never get written, because they're invisible in the simulator and they're the last-day task that gets cut.&lt;/p&gt;

&lt;p&gt;Regulation is closing in anyway: the European Accessibility Act took effect for consumer digital products in June 2025, and ADA Title II requires state and local government apps to hit WCAG 2.1 AA by April 2026. Regulation without automation is a punishment. AI is what makes the automation possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shift 1: the OS started covering for you
&lt;/h2&gt;

&lt;p&gt;iOS runs an on-device model that generates image descriptions for photos and unlabeled UI elements. Ship a button with no label and VoiceOver will guess: "Button, likely a play triangle." Android's TalkBack does the same with Gemini Nano, including for images inside third-party apps that never set &lt;code&gt;contentDescription&lt;/code&gt;. Both platforms now generate Live Captions for any audio playing on the device. Voice Control on iOS understands intent, so "tap the little heart" resolves even when your label is &lt;code&gt;favoriteButton&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The floor is rising. The ceiling is not. A guessed description loses to a real one every time, and the guess is silently wrong often enough to matter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shift 2: audits got cheap
&lt;/h2&gt;

&lt;p&gt;Three categories worth knowing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rule engines plus ML&lt;/strong&gt; (axe DevTools Mobile, Google's Accessibility Scanner). These now catch labels that exist but are meaningless, like &lt;code&gt;contentDescription="image1.jpg"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Visual regression plus ML&lt;/strong&gt; (Applause, BrowserStack, testRigor). Diffs screens against known-good corpora and flags contrast ratios, small tap targets, low-legibility fonts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLM audits.&lt;/strong&gt; Feed in screenshots plus an accessibility tree dump, get back findings in plain English: "The 'Sign in with Apple' button has no accessibility label; VoiceOver will announce it as 'Button'."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can wire the cheap version of the third one into CI yourself:&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;// jest + @testing-library/react-native&lt;/span&gt;
&lt;span class="c1"&gt;// Fails the build when an interactive element ships unlabeled.&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;render&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="s2"&gt;@testing-library/react-native&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;all buttons are labeled&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;UNSAFE_root&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ProductScreen&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;buttons&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;UNSAFE_root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&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;n&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;accessibilityRole&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buttons&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="nf"&gt;toBeGreaterThan&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="nx"&gt;buttons&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;b&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;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&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;accessibilityLabel&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBeTruthy&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;WebAIM's own numbers say automated tooling catches 30 to 40% of WCAG failures. AI raises it, not to 100%. Keep a human in the loop, ideally one who uses the tech.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shift 3: the missing content is now generated
&lt;/h2&gt;

&lt;p&gt;Alt text from vision models is table stakes. Whisper-class models made captions good enough for accessibility use in most languages. LLMs are producing plain-language versions of dense content on the fly, which is a genuine breakthrough for cognitive accessibility. Live cross-language captioning is built into the call stack on both platforms.&lt;/p&gt;

&lt;p&gt;The catch: generated descriptions are only &lt;em&gt;usually&lt;/em&gt; right, and a screen reader confidently reading a subtly wrong description is worse than one that says nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shift 4: fix it at generation time
&lt;/h2&gt;

&lt;p&gt;This is the one that matters. When an LLM writes the screen, the accessibility props are not extra work, they're the same tokens. That flips the default from "no labels, ship it" to "labels came with the generation."&lt;/p&gt;

&lt;p&gt;This is the bet &lt;a href="https://www.rapidnative.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=how-ai-is-making-mobile-accessibility-easier" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt; is built on: prompt in, real React Native and Expo code out, with &lt;code&gt;accessibilityLabel&lt;/code&gt;, &lt;code&gt;accessibilityRole&lt;/code&gt;, &lt;code&gt;accessibilityHint&lt;/code&gt;, and &lt;code&gt;accessibilityState&lt;/code&gt; attached at generation rather than bolted on later. An image comes out as &lt;code&gt;accessibilityLabel="Sunset over the ocean"&lt;/code&gt; instead of a nameless &lt;code&gt;&amp;lt;Image /&amp;gt;&lt;/code&gt; someone will theoretically label next sprint.&lt;/p&gt;

&lt;p&gt;None of this is exclusive to one tool. A well-prompted Copilot, Cursor, or Claude does the same thing in a hand-written codebase. Put it in the repo rules and it applies to every generation:&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="c"&gt;&amp;lt;!-- .cursorrules / CLAUDE.md / copilot-instructions.md --&amp;gt;&lt;/span&gt;
Every interactive element must have accessibilityRole and accessibilityLabel.
Every Image must have accessibilityLabel or accessible={false}.
Touch targets must be &amp;gt;= 44x44 (use hitSlop when the visual is smaller).
Never use placeholder text as the only label for a TextInput.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What AI still can't do
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;It tells you labels exist, not that the flow is &lt;strong&gt;usable&lt;/strong&gt;. Reading order that jumps around passes every automated check.&lt;/li&gt;
&lt;li&gt;Generated alt text is generic. "A person" is compliant. "The founder of the org you're about to donate to" is what a sighted user sees.&lt;/li&gt;
&lt;li&gt;Cognitive accessibility is barely automatable. The fix is design work, not a tag.&lt;/li&gt;
&lt;li&gt;Live captioning fails contextually: wrong medication name, wrong price, wrong date. Plausible-but-wrong is the worst failure mode for exactly the users you were helping.&lt;/li&gt;
&lt;li&gt;Motor accessibility is under-covered because it doesn't map cleanly onto structured tests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Treat AI as the intern who catches the obvious stuff at 10x speed. It makes a specialist's time higher-leverage. It doesn't replace them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist I'd run on any AI-generated app
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Every interactive element has &lt;code&gt;accessibilityLabel&lt;/code&gt;, plus &lt;code&gt;accessibilityRole&lt;/code&gt; and &lt;code&gt;accessibilityHint&lt;/code&gt; where useful.&lt;/li&gt;
&lt;li&gt;Images are labeled or explicitly marked decorative with &lt;code&gt;accessible={false}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Contrast passes WCAG 2.2 AA (4.5:1 body, 3:1 large). Re-check after any palette tweak.&lt;/li&gt;
&lt;li&gt;Font scaling works. Test at 200%.&lt;/li&gt;
&lt;li&gt;Touch targets are 44x44 minimum. Generators over-index on clean small icons, so this is the most-violated rule in generated code.&lt;/li&gt;
&lt;li&gt;Forms have labels, not just placeholders.&lt;/li&gt;
&lt;li&gt;Video has captions.&lt;/li&gt;
&lt;li&gt;You personally turned on VoiceOver and TalkBack and completed the primary flow.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Point 4 is worth a snippet, since it's the one people get wrong quietly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&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;useWindowDimensions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;PixelRatio&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="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&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;// Bad: fixed height locks text out of scaling&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="na"&gt;height&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;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Continue&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;&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="c1"&gt;// Better: let the row grow with the user's font scale&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;PixelRatio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getFontScale&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="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="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;scale&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="s2"&gt;center&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;Text&lt;/span&gt; &lt;span class="na"&gt;maxFontSizeMultiplier&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Continue&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Where this goes next
&lt;/h2&gt;

&lt;p&gt;WCAG 3.0 moves toward outcome-based scoring instead of binary pass/fail, which is far better suited to LLM evaluation. EAA enforcement in Europe starts producing fines and case law in 2026, which changes the internal politics of a11y work fast. And expect compliance tooling that lives inside the AI development pipeline: reviewing PRs, gating deploys, producing an audit trail.&lt;/p&gt;

&lt;p&gt;The direction of travel is that accessibility becomes a property of how the app was built, not a coat of paint applied afterward.&lt;/p&gt;

&lt;p&gt;What's the a11y bug that has burned you hardest in a shipped app? Drop it in the comments. I'm especially interested in the ones no automated audit caught.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>a11y</category>
      <category>ai</category>
      <category>mobile</category>
    </item>
    <item>
      <title>The State of Mobile AI in 2026 — From Code Generation to Full App Creation</title>
      <dc:creator>Famitha M A</dc:creator>
      <pubDate>Thu, 06 Aug 2026 07:55:07 +0000</pubDate>
      <link>https://dev.to/rapidnative-ai/the-state-of-mobile-ai-in-2026-from-code-generation-to-full-app-creation-37ig</link>
      <guid>https://dev.to/rapidnative-ai/the-state-of-mobile-ai-in-2026-from-code-generation-to-full-app-creation-37ig</guid>
      <description>&lt;p&gt;In 2023, AI could complete a line of code. In 2026, AI can build the entire mobile app — screens, backend, auth, camera access, and a signed IPA ready for the App Store.&lt;/p&gt;

&lt;p&gt;That's not marketing copy, it's a specific technical shift, and it happened faster than most of us in mobile expected. This post walks through the four generations of AI coding tools, why mobile lagged the web by three years, and what specifically broke open in 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four generations, briefly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gen 1 (2021-2023): Autocomplete.&lt;/strong&gt; Copilot. AI completes the next line. Human owns the file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gen 2 (2023-2024): Single-file generation.&lt;/strong&gt; Cursor, Claude Code. AI generates entire files. Human is the integrator.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gen 3 (2024-2025): Web full-stack.&lt;/strong&gt; Lovable, v0, Bolt. Prompt to deployed web app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gen 4 (2026): Mobile end-to-end.&lt;/strong&gt; Prompt to submission-ready native React Native + Expo app.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every generation compressed the space between "I have an idea" and "users are running my app." Gen 4 is the first one where a non-engineer can complete that arc entirely for native mobile.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why mobile lagged the web
&lt;/h2&gt;

&lt;p&gt;Web AI moved fast because the web is forgiving. Mobile is not. Each of the following cost the ecosystem roughly a year:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two platforms, not one.&lt;/strong&gt; React Native shares ~80% of code across iOS and Android. The other 20% is where the pain lives: permissions, keyboard behavior, safe areas, gestures, notches. A model has to reason about platform divergence explicitly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Native APIs are gated.&lt;/strong&gt; Camera on web is &lt;code&gt;getUserMedia&lt;/code&gt;. Camera on mobile is &lt;code&gt;expo-camera&lt;/code&gt; + permission strings in &lt;code&gt;app.json&lt;/code&gt; + runtime prompts + a config plugin. Any wrong step and it doesn't build.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Preview is genuinely hard.&lt;/strong&gt; Web preview is an iframe. Mobile preview needs Metro, a matching JS engine (Hermes), and streaming updates without a full rebuild.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The App Store is a wall.&lt;/strong&gt; Apple's Guideline 4.2 rejects "wrapped websites." Web-first tools with a "mobile export" button hit this and bounced. Gen 4 had to output real native code from day one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fullstack means fullstack.&lt;/strong&gt; Real mobile means auth that survives app suspend/resume, secure token storage in the OS keychain, offline sync, and a data layer that handles being genuinely offline. Gen 3 tools papered over most of this — they targeted the web, which rarely needs it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "full app creation" actually produces in 2026
&lt;/h2&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;Output&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Screens&lt;/td&gt;
&lt;td&gt;React Native + navigation, gestures, safe areas&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;State&lt;/td&gt;
&lt;td&gt;Redux / Zustand / Context, chosen by complexity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backend&lt;/td&gt;
&lt;td&gt;Provisioned Supabase project + schema + RLS + types&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth&lt;/td&gt;
&lt;td&gt;Keychain-backed session, deep links, sign-in flows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data&lt;/td&gt;
&lt;td&gt;Migrations, seed data, offline-first where relevant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Device&lt;/td&gt;
&lt;td&gt;Camera, GPS, push, biometrics with correct config plugins&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Preview&lt;/td&gt;
&lt;td&gt;QR-code preview on real iOS and Android devices&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Export&lt;/td&gt;
&lt;td&gt;Clean React Native + Expo source, runs anywhere&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ship&lt;/td&gt;
&lt;td&gt;Signed IPA/AAB, submitted to App Store + Play&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every row above is a place a Gen 3 "mobile export" tool fails. Every row is now Gen 4 table stakes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four unlocks that made 2026 different
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Frontier models got good at multi-file reasoning.&lt;/strong&gt; The current generation of frontier models can hold an entire codebase in context and reason about change propagation. Prerequisite for full app gen.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Streaming output made iteration feel instant.&lt;/strong&gt; SSE + progressive component mounting turns "AI generation" from a batch job into a conversation.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;LLM-native tooling replaced ported IDEs.&lt;/strong&gt; Virtual filesystems the AI writes into, sandboxed runners that typecheck and lint before feedback loops back to the model, multi-step LLM pipelines. The model is the CPU; the surrounding system is the OS.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Someone had to actually build the mobile-specific infrastructure.&lt;/strong&gt; This is the moat. Producing an Expo project that installs, builds, previews on a real device, submits to a store, and comes back with a valid receipt is a stack of ten problems requiring ten specialists.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Checklist: is a tool actually Generation 4?
&lt;/h2&gt;

&lt;p&gt;If you're evaluating tools in 2026, ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does the output run on a real phone via QR code, or just in a browser iframe?&lt;/li&gt;
&lt;li&gt;Does the tool generate backend + auth in the same prompt as the frontend?&lt;/li&gt;
&lt;li&gt;Can you export the code and run it locally without the vendor?&lt;/li&gt;
&lt;li&gt;Does the tool handle App Store submission or stop at "here's your code"?&lt;/li&gt;
&lt;li&gt;Does the tool support device features — camera, push, GPS, biometrics — out of the box?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most tools calling themselves "mobile AI app builders" in 2026 fail at least two. The ones that pass are the ones defining what AI mobile app development means for the rest of the decade.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means for developers
&lt;/h2&gt;

&lt;p&gt;Senior mobile engineers have not been displaced — the ceiling of what you can build solo has moved up, not down. The mechanical work (screen scaffolding, auth boilerplate, Expo config, navigation setup) collapses to a prompt. The architectural work (data modeling, performance profiling, complex state machines, offline sync semantics) is still where craftsmanship compounds.&lt;/p&gt;

&lt;p&gt;The developers who ship the most in 2026 are the ones who treat AI as a code CPU and spend their time on the parts that matter.&lt;/p&gt;




&lt;p&gt;Try it out: &lt;a href="https://www.rapidnative.com/?utm_source=devto&amp;amp;utm_medium=content&amp;amp;utm_campaign=mobile-ai-2026-the-shift-from-code-generation-to-full-app-creation" rel="noopener noreferrer"&gt;rapidnative.com&lt;/a&gt; — describe an app in plain English, get a real React Native + Expo app on your phone in minutes. Free tier is 20 credits, no credit card.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>ai</category>
      <category>mobile</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The Complete Guide to Offline Storage in React Native (2026)</title>
      <dc:creator>Famitha M A</dc:creator>
      <pubDate>Wed, 22 Jul 2026 09:28:13 +0000</pubDate>
      <link>https://dev.to/rapidnative-ai/the-complete-guide-to-offline-storage-in-react-native-2026-1n7j</link>
      <guid>https://dev.to/rapidnative-ai/the-complete-guide-to-offline-storage-in-react-native-2026-1n7j</guid>
      <description>&lt;p&gt;Most React Native apps treat the network as an always-on dependency, and it shows the moment a request stalls. Offline storage isn't one library — it's a stack: key-value stores for preferences, secure enclaves for tokens, structured DBs for domain data, and query caches for server state. This post walks through the whole stack and the offline-first pattern that ties it together.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five categories of RN offline storage
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Key-value&lt;/strong&gt; — AsyncStorage (compat) or MMKV (default). MMKV is ~30x faster and supports sync reads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secure&lt;/strong&gt; — &lt;code&gt;expo-secure-store&lt;/code&gt; or &lt;code&gt;react-native-keychain&lt;/code&gt;. Backed by iOS Keychain / Android Keystore.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structured&lt;/strong&gt; — &lt;code&gt;expo-sqlite&lt;/code&gt;, &lt;code&gt;op-sqlite&lt;/code&gt; (JSI, synchronous), WatermelonDB (reactive + sync), Realm (Atlas Device Sync).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File system&lt;/strong&gt; — &lt;code&gt;expo-file-system&lt;/code&gt; for media. Never base64 blobs into KV storage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server-state cache&lt;/strong&gt; — TanStack Query + &lt;code&gt;persistQueryClient&lt;/code&gt;, backed by MMKV.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Comparison table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Library&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Perf&lt;/th&gt;
&lt;th&gt;Encryption&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AsyncStorage&lt;/td&gt;
&lt;td&gt;KV&lt;/td&gt;
&lt;td&gt;1x&lt;/td&gt;
&lt;td&gt;Community fork&lt;/td&gt;
&lt;td&gt;Legacy/compat&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MMKV&lt;/td&gt;
&lt;td&gt;KV&lt;/td&gt;
&lt;td&gt;~30x&lt;/td&gt;
&lt;td&gt;Built-in&lt;/td&gt;
&lt;td&gt;New default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;expo-secure-store&lt;/td&gt;
&lt;td&gt;Secure KV&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;OS keychain&lt;/td&gt;
&lt;td&gt;Tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;op-sqlite&lt;/td&gt;
&lt;td&gt;SQL&lt;/td&gt;
&lt;td&gt;JSI&lt;/td&gt;
&lt;td&gt;SQLCipher&lt;/td&gt;
&lt;td&gt;Structured data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WatermelonDB&lt;/td&gt;
&lt;td&gt;Reactive DB&lt;/td&gt;
&lt;td&gt;Lazy&lt;/td&gt;
&lt;td&gt;SQLCipher&lt;/td&gt;
&lt;td&gt;Large offline lists + sync&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Realm&lt;/td&gt;
&lt;td&gt;Object DB&lt;/td&gt;
&lt;td&gt;Fast&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Atlas Device Sync&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The offline-first pattern
&lt;/h2&gt;

&lt;p&gt;Four moving parts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Local-first reads&lt;/strong&gt; — UI always reads from local store. Network populates store; store renders UI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optimistic writes&lt;/strong&gt; — UI updates immediately, before the server acks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mutation queue&lt;/strong&gt; — Durable list (SQLite or MMKV) of pending server-side changes with client IDs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sync engine&lt;/strong&gt; — Background worker drains the queue with exponential backoff, reconciles conflicts.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;markMessageAsRead&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;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&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="na"&gt;readAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="na"&gt;pendingSync&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;await&lt;/span&gt; &lt;span class="nx"&gt;mutationQueue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enqueue&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;message.markRead&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;readAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;syncEngine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wake&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;
  
  
  Conflict resolution
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LWW&lt;/strong&gt; — timestamps, newest wins. Fine for most consumer apps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server-authoritative&lt;/strong&gt; — reject stale writes with a version mismatch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CRDTs&lt;/strong&gt; — Yjs, Automerge. Overkill unless you're building a collaborative editor.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Decision cheat sheet
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Prefs / JWT? → MMKV + expo-secure-store&lt;/li&gt;
&lt;li&gt;Instant-open cached lists? → TanStack Query + &lt;code&gt;persistQueryClient&lt;/code&gt; on MMKV&lt;/li&gt;
&lt;li&gt;Thousands of offline records? → op-sqlite (or WatermelonDB for reactive)&lt;/li&gt;
&lt;li&gt;Cross-device sync? → WatermelonDB or Realm&lt;/li&gt;
&lt;li&gt;Media? → expo-file-system + metadata in SQLite&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Pitfalls to skip
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Blocking JS thread with async AsyncStorage at startup&lt;/li&gt;
&lt;li&gt;Blobs in KV stores&lt;/li&gt;
&lt;li&gt;No cache-wipe on logout&lt;/li&gt;
&lt;li&gt;Infinite retry loops in the mutation queue&lt;/li&gt;
&lt;li&gt;Persisting Redux state without a schema version&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;Offline-first isn't a defensive posture — it's a performance strategy. Reading from local storage beats a round-trip every time, and a good sync engine hides the network from your users. Pick each layer deliberately and this becomes a solved problem, not a recurring one.&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://www.rapidnative.com/?utm_source=devto&amp;amp;utm_medium=content&amp;amp;utm_campaign=complete-guide-offline-storage-react-native" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt;, an AI mobile app builder that generates production-ready React Native code you can extend with any of these libraries. Would love feedback from anyone using MMKV + TanStack Query in production.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>mobile</category>
      <category>javascript</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Why 77% of Your App's Users Are Gone in 3 Days (and the real fix isn't another tactic)</title>
      <dc:creator>Famitha M A</dc:creator>
      <pubDate>Mon, 20 Jul 2026 09:19:50 +0000</pubDate>
      <link>https://dev.to/rapidnative-ai/why-77-of-your-apps-users-are-gone-in-3-days-and-the-real-fix-isnt-another-tactic-201n</link>
      <guid>https://dev.to/rapidnative-ai/why-77-of-your-apps-users-are-gone-in-3-days-and-the-real-fix-isnt-another-tactic-201n</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;77% of users who install your app stop using it within 72 hours. Most of that churn happens &lt;em&gt;inside the first session&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;2026 cross-industry medians: Day-1 = 26%, Day-7 = 11%, Day-30 = 5.4%. Subscription apps retain ~2.5x better at D30.&lt;/li&gt;
&lt;li&gt;The five real churn causes: no "aha moment", ad/product mismatch, no external trigger, crashes/slow loads, and one-and-done intent (which is fine).&lt;/li&gt;
&lt;li&gt;The uncomfortable part: you probably already know the tactics. The real bottleneck is that your experiment cycle takes 3 weeks while your cohort churns in 3 days.&lt;/li&gt;
&lt;li&gt;Fix the cycle time, not the tactics list.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The number
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;77% of the users who install your app stop using it within 72 hours.&lt;/strong&gt; Not a month. Not a week. Three days.&lt;/p&gt;

&lt;p&gt;That stat traces back to Andrew Chen's analysis at a16z, and every benchmark report since (Adjust, AppsFlyer, Sensor Tower) lands in the same neighborhood. The 2026 numbers, by vertical:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Vertical&lt;/th&gt;
&lt;th&gt;Day 1&lt;/th&gt;
&lt;th&gt;Day 7&lt;/th&gt;
&lt;th&gt;Day 30&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Social &amp;amp; Communication&lt;/td&gt;
&lt;td&gt;30%&lt;/td&gt;
&lt;td&gt;15%&lt;/td&gt;
&lt;td&gt;8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gaming (Casual)&lt;/td&gt;
&lt;td&gt;27%&lt;/td&gt;
&lt;td&gt;10%&lt;/td&gt;
&lt;td&gt;4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Finance &amp;amp; Fintech&lt;/td&gt;
&lt;td&gt;33%&lt;/td&gt;
&lt;td&gt;18%&lt;/td&gt;
&lt;td&gt;12%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Health &amp;amp; Fitness&lt;/td&gt;
&lt;td&gt;24%&lt;/td&gt;
&lt;td&gt;10%&lt;/td&gt;
&lt;td&gt;6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E-commerce &amp;amp; Retail&lt;/td&gt;
&lt;td&gt;22%&lt;/td&gt;
&lt;td&gt;8%&lt;/td&gt;
&lt;td&gt;4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Productivity&lt;/td&gt;
&lt;td&gt;29%&lt;/td&gt;
&lt;td&gt;14%&lt;/td&gt;
&lt;td&gt;10%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Subscription Apps (all)&lt;/td&gt;
&lt;td&gt;35%&lt;/td&gt;
&lt;td&gt;22%&lt;/td&gt;
&lt;td&gt;14%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cross-category median&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;26%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;11%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5.4%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two things worth noticing: subscription apps retain 2.5x better at Day 30 (paywalls are a commitment device), and fintech beats casual gaming 3x at D30 (utility beats entertainment on retention, almost always).&lt;/p&gt;

&lt;h2&gt;
  
  
  Make sure you're measuring the same thing as the benchmarks
&lt;/h2&gt;

&lt;p&gt;Retention has three variants, and teams routinely fool themselves by measuring one internally and comparing against another:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Classic&lt;/strong&gt; — user came back on &lt;em&gt;exactly&lt;/em&gt; day N. Punitive, but it's what the benchmarks above use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rolling&lt;/strong&gt; — user came back on day N &lt;em&gt;or later&lt;/em&gt;. More forgiving.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bracket&lt;/strong&gt; — user came back anywhere in days N–M. Useful for weekly cohorts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to sanity-check your own numbers, classic Day-N retention is one query away:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Classic Day-N retention per install cohort&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;installs&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;install_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;cohort_day&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;app_installs&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;activity&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;active_day&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;app_events&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cohort_day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;cohort_size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;active_day&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cohort_day&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'1 day'&lt;/span&gt;
        &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;d1_retention&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;active_day&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cohort_day&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'7 day'&lt;/span&gt;
        &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;d7_retention&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;installs&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;activity&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Segment this by acquisition channel before you conclude anything. A cohort with 15% D7 overall might be 30% from organic and 4% from one bad Facebook creative. That's not a retention problem — that's an ad-relevance problem wearing a retention costume.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the 77% actually dies: the first session
&lt;/h2&gt;

&lt;p&gt;The churn isn't spread evenly across 72 hours. Roughly 60–70% of it happens &lt;strong&gt;inside the first session&lt;/strong&gt;. Users don't drift away over three opens — they open once, hit friction, close, never return.&lt;/p&gt;

&lt;p&gt;The consistent offenders, with rough measured costs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Signup wall before showing value: &lt;strong&gt;–30-40%&lt;/strong&gt; of installers on that screen alone&lt;/li&gt;
&lt;li&gt;Phone verification: another &lt;strong&gt;–15-20%&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Push permission prompt on Day 0 (before any value): &lt;strong&gt;~4x lower&lt;/strong&gt; opt-in vs. asking after value&lt;/li&gt;
&lt;li&gt;More than five onboarding screens: &lt;strong&gt;~20% more&lt;/strong&gt; churn vs. three or fewer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The cheapest fix in mobile is deferring everything. Guest mode instead of a signup wall:&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;// Instead of gating the app behind auth, persist a local guest identity&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;AsyncStorage&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-async-storage/async-storage&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;v4&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;uuid&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;uuid&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getOrCreateGuestId&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;AsyncStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;guest_id&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;id&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="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`guest_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;uuid&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;await&lt;/span&gt; &lt;span class="nx"&gt;AsyncStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;guest_id&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="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="c1"&gt;// Later, when the user actually signs up, merge the guest state&lt;/span&gt;
&lt;span class="k"&gt;export&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;upgradeGuestToUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&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;guestId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;AsyncStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;guest_id&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;guestId&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="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mergeAccounts&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;guestId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userId&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;AsyncStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;guest_id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same logic for push permissions — ask &lt;em&gt;after&lt;/em&gt; the user has experienced value, not on launch:&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;// Don't request on app open. Trigger contextually, e.g. after first "aha" action&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;requestPushAfterValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trigger&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;alreadyAsked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;AsyncStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;push_prompted&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;alreadyAsked&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trigger&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;first_workout_completed&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;// Pre-permission soft prompt first — protects your one real OS prompt&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;wants&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;showSoftPrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Want a reminder before your streak breaks?&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;wants&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;requestNotificationPermission&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="nx"&gt;AsyncStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;push_prompted&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;true&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;
  
  
  The five real reasons users churn
&lt;/h2&gt;

&lt;p&gt;Skip the twenty-tactic listicles. Churn comes from a small set of root causes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;They never hit the "aha moment."&lt;/strong&gt; Duolingo: first lesson done. Instagram: first personalized feed. Fintech: first balance after bank link. Users who reach it retain at 3-5x the rate of users who don't. Your entire first session should be a speedrun to that moment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The ad promised something the app isn't.&lt;/strong&gt; Invisible in aggregate, obvious when you segment by channel (see the SQL above).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No external trigger.&lt;/strong&gt; Apps that survive past D7 almost always reach out — a push tied to a &lt;em&gt;real&lt;/em&gt; event (someone messaged you, your streak's about to break), not "hi, we miss you." Apps that rely on being remembered lose to apps that reach out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Crashes and slow screens.&lt;/strong&gt; Users don't file bug reports; they uninstall. And crashes disproportionately hit your heaviest users, because they hit more code paths.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;They got what they came for.&lt;/strong&gt; One-time intent (check a flight, convert a currency once) isn't a retention failure. Segment churn by intent before you panic.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The part nobody writes: your bottleneck is cycle time, not tactics
&lt;/h2&gt;

&lt;p&gt;Here's the uncomfortable truth: you already knew most of the above. Everyone's read the same posts. Everyone has the Notion doc of retention experiments. So why is the median D30 still 5.4%?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Because most teams can't ship a retention experiment in under three weeks.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Week 1: spec + Figma + design review. Week 2: implementation + new tracking event + QA. Week 3: ship to 20%, wait for the cohort to season, analyze. By the time you know whether the shorter signup form moved D1, the cohort you wanted to save has been gone for two and a half weeks. You're permanently fighting the last war — with eighteen more experiments queued behind this one.&lt;/p&gt;

&lt;p&gt;Duolingo runs hundreds of A/B tests a year. Netflix ships micro-experiments continuously. The gap isn't ideas — it's that they ship an experiment in a day and you ship one a month.&lt;/p&gt;

&lt;p&gt;This is where tooling actually changed the math. Building in an AI-native stack like &lt;a href="https://www.rapidnative.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=mobile-app-retention-why-users-churn" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt; means the guest-mode experiment above is a prompt — "make signup optional, add continue-as-guest, persist state locally" — with a working build on a real device via QR code in under a minute, instead of a sprint. When iteration cost collapses that far, the strategy flips: you stop hoarding experiments and start running them in parallel.&lt;/p&gt;

&lt;h2&gt;
  
  
  A weekly experiment cadence that actually compounds
&lt;/h2&gt;

&lt;p&gt;If your iteration cost is low enough, this rhythm works:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Monday AM&lt;/strong&gt; — pull last week's cohorts, segmented by channel and onboarding variant. Find the biggest first-session drop-off.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monday PM&lt;/strong&gt; — one hypothesis, one line: "changing X to Y improves D1 by Z% for cohort A."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tuesday&lt;/strong&gt; — build the variant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wednesday&lt;/strong&gt; — ship to 50% of new installs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Next Monday&lt;/strong&gt; — cohort's seasoned. Ship the winner or kill it. Next experiment.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's ~5 experiments a month, 60 a year. Even at a 1-in-5 hit rate you're compounding at a pace nobody on a three-week cycle can touch.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bottom line
&lt;/h2&gt;

&lt;p&gt;Retention in 2026 isn't a knowledge problem. The benchmarks are public and every founder can recite why users churn. The teams that win are the ones shipping retention experiments &lt;strong&gt;faster than their cohorts churn&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Stop asking "which tactic should we prioritize?" Start asking "what's the smallest experiment we can put in front of real users this week?"&lt;/p&gt;

&lt;p&gt;What's your team's actual experiment cycle time — days or weeks? And what's the one onboarding change you've been meaning to test forever? Drop it in the comments, I'm curious what's stuck in everyone's backlog. 👇&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>mobile</category>
      <category>startup</category>
      <category>analytics</category>
    </item>
    <item>
      <title>How to Build a Grocery Delivery App with React Native + Expo (2026)</title>
      <dc:creator>Famitha M A</dc:creator>
      <pubDate>Fri, 17 Jul 2026 07:00:25 +0000</pubDate>
      <link>https://dev.to/rapidnative-ai/how-to-build-a-grocery-delivery-app-with-react-native-expo-2026-f2d</link>
      <guid>https://dev.to/rapidnative-ai/how-to-build-a-grocery-delivery-app-with-react-native-expo-2026-f2d</guid>
      <description>&lt;h1&gt;
  
  
  How to Build a Grocery Delivery App with React Native + Expo (2026)
&lt;/h1&gt;

&lt;p&gt;If you've ever tried to build an Instacart-style app from scratch, you know the pain: three apps to ship (customer, driver, admin), a real-time tracking layer, payments, dispatch, and the ever-fun "handle a driver going offline mid-delivery."&lt;/p&gt;

&lt;p&gt;Here's a practical stack and a build sequence that keeps the moving parts to a minimum.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three-app reality
&lt;/h2&gt;

&lt;p&gt;A grocery delivery product is three apps sharing one backend:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Customer app&lt;/strong&gt; — browse, cart, checkout, track&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Driver app&lt;/strong&gt; — accept orders, navigate, capture proof of delivery&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Admin dashboard&lt;/strong&gt; — inventory, dispatch, disputes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Combine them into one and you'll spend the next quarter untangling permission logic. Keep them separate.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack that converges
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mobile:&lt;/strong&gt; React Native + Expo (one codebase, iOS + Android, OTA updates)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend:&lt;/strong&gt; Supabase (Postgres, auth, realtime, RLS)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payments:&lt;/strong&gt; Stripe Payment Intents + Stripe Connect for driver payouts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maps:&lt;/strong&gt; Google Maps SDK + Directions API + Distance Matrix&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Push:&lt;/strong&gt; Expo Push → Twilio SMS for critical alerts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Realtime:&lt;/strong&gt; Supabase realtime channels (no custom WebSocket server)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Data model — the parts that matter
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- The critical detail: capture price at purchase time&lt;/span&gt;
&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;order_items&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;primary&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;references&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;product_id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;references&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;quantity&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="k"&gt;not&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;price_at_purchase&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;-- never join back to products.price_cents&lt;/span&gt;
  &lt;span class="n"&gt;substitution_preference&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;check&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;substitution_preference&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'allow'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'contact'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'refund'&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;-- Append-only audit log = free source of truth for status timeline&lt;/span&gt;
&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;order_events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;primary&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;references&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;not&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;metadata&lt;/span&gt; &lt;span class="n"&gt;jsonb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="n"&gt;now&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;Two invariants worth burning into your brain:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Never resolve &lt;code&gt;order_items.price&lt;/code&gt; by joining back to &lt;code&gt;products&lt;/code&gt;. Prices change; receipts must not.&lt;/li&gt;
&lt;li&gt;Never trust the client's clock for &lt;code&gt;delivered_at&lt;/code&gt;. Use &lt;code&gt;now()&lt;/code&gt; in Postgres. You will thank yourself the first time a dispute goes to your inbox.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Real-time tracking without pain
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Driver side:&lt;/strong&gt; capture location every 5–15 seconds, back off when stationary.&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="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Location&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;expo-location&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;supabase&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;./supabase&lt;/span&gt;&lt;span class="dl"&gt;'&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;Location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startLocationUpdatesAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;driver-location&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;accuracy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Accuracy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Balanced&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;timeInterval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;distanceInterval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;foregroundService&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;notificationTitle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Delivery in progress&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;notificationBody&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Sharing your location with the customer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Customer side:&lt;/strong&gt; subscribe to the driver row.&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;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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;channel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`driver:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;driverId&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="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;postgres_changes&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;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;UPDATE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;public&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;table&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;drivers&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`id=eq.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;driverId&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;span class="na"&gt;new&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;driver&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;setDriverPosition&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current_lat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;lng&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current_lng&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;subscribe&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unsubscribe&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;driverId&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Refresh ETA every 30 seconds via Distance Matrix — not on every location ping, or you'll blow through your rate limit before lunch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Payments — the substitution problem
&lt;/h2&gt;

&lt;p&gt;Stripe Payment Intents flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;First order: create a Stripe Customer, attach a &lt;code&gt;SetupIntent&lt;/code&gt; to save the card off-session.&lt;/li&gt;
&lt;li&gt;Checkout: create a &lt;code&gt;PaymentIntent&lt;/code&gt; with &lt;code&gt;confirm: false&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only confirm after the order is packed.&lt;/strong&gt; Never authorize before you know items are available.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The substitution policy question you must answer on day one: pre-authorize subtotal × 1.15 to cover swaps (Instacart's approach) or refund the diff (better UX, more work). Either works; pick one and communicate it at checkout.&lt;/p&gt;

&lt;h2&gt;
  
  
  The screens
&lt;/h2&gt;

&lt;p&gt;Nine screens ship a customer app: Onboarding + Address, Home, Category Listing, Product Detail, Cart, Checkout, Order Tracking, Order History, Profile.&lt;/p&gt;

&lt;p&gt;Writing these by hand from scratch is a 6–8 week grind. If you want to skip the JSX, prompts to an AI mobile app builder generate them in an afternoon and export clean React Native you can extend by hand. I've been using &lt;a href="https://www.rapidnative.com/?utm_source=devto&amp;amp;utm_medium=content&amp;amp;utm_campaign=how-to-build-a-grocery-delivery-app" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt; for this and shipping the boring UI in a day so I can spend the rest of the sprint on dispatch logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  The subtle bugs to avoid
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Client-side timestamps.&lt;/strong&gt; Always &lt;code&gt;now()&lt;/code&gt; in the DB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trusting the driver's app to update order status.&lt;/strong&gt; Use Postgres triggers on &lt;code&gt;order_events&lt;/code&gt; to update &lt;code&gt;orders.status&lt;/code&gt; atomically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reading inventory from &lt;code&gt;products.inventory_count&lt;/code&gt; at checkout.&lt;/strong&gt; Race conditions. Use a serializable transaction or &lt;code&gt;SELECT ... FOR UPDATE&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rendering the map on every location ping.&lt;/strong&gt; Debounce marker updates.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrap
&lt;/h2&gt;

&lt;p&gt;React Native + Expo + Supabase + Stripe + Google Maps is boring, well-documented, and consolidated for a reason. The wins in this space come from operational density and freshness, not from the tech stack. Ship the MVP, get it in front of ten customers, iterate.&lt;/p&gt;

&lt;p&gt;If you want the whole architecture in one place (data model, screen list, prompt sequence), the &lt;a href="https://www.rapidnative.com/blogs/how-to-build-a-grocery-delivery-app?utm_source=devto&amp;amp;utm_medium=content&amp;amp;utm_campaign=how-to-build-a-grocery-delivery-app" rel="noopener noreferrer"&gt;full walkthrough is on our blog&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Posting instructions for Dev.to:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Best time: Tuesday–Thursday, 8–10 AM ET&lt;/li&gt;
&lt;li&gt;Community: Also cross-post to &lt;code&gt;#reactnative&lt;/code&gt; and &lt;code&gt;#tutorial&lt;/code&gt; Dev.to tags&lt;/li&gt;
&lt;li&gt;Engagement: Reply to every comment in the first 24 hours; Dev.to's algorithm weights early engagement&lt;/li&gt;
&lt;li&gt;Cover image: Use the Unsplash URL in frontmatter or upload a screenshot of the finished app&lt;/li&gt;
&lt;li&gt;After posting: Share in &lt;code&gt;r/reactnative&lt;/code&gt; and &lt;code&gt;r/expo&lt;/code&gt; with the Dev.to link (Reddit tolerates Dev.to links better than direct-to-marketing links)&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>tutorial</category>
      <category>mobile</category>
    </item>
    <item>
      <title>7 Lessons Shipping React Native Templates</title>
      <dc:creator>Famitha M A</dc:creator>
      <pubDate>Fri, 17 Jul 2026 06:51:33 +0000</pubDate>
      <link>https://dev.to/rapidnative-ai/7-lessons-shipping-react-native-templates-1b21</link>
      <guid>https://dev.to/rapidnative-ai/7-lessons-shipping-react-native-templates-1b21</guid>
      <description>&lt;p&gt;Seven React Native templates in one year, all on Expo SDK 54 + Supabase + NativeWind. This is the technical retrospective — seven lessons with code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The templates
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Weather ($49, 5 screens)&lt;/li&gt;
&lt;li&gt;Fitness ($89, 12+ screens)&lt;/li&gt;
&lt;li&gt;LearnHub ($89, 9 screens)&lt;/li&gt;
&lt;li&gt;RideNow ($99, 8 screens)&lt;/li&gt;
&lt;li&gt;Food Delivery ($79, 5 screens)&lt;/li&gt;
&lt;li&gt;AI Voice Notes ($79, 16+ screens) — Whisper&lt;/li&gt;
&lt;li&gt;Chat with PDF ($79, 16+ screens) — RAG&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All share one substrate. That's the whole point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson 1 — Build the substrate before the first template
&lt;/h2&gt;

&lt;p&gt;Weather was our first template. Five screens, no auth, no DB. It didn't need Supabase. We built it against Supabase anyway.&lt;/p&gt;

&lt;p&gt;Rationale: template two would need auth, RLS, edge functions, and a marketing page contract. Building that infra for a template that didn't need it looked wasteful for two weeks. It paid for itself by template three.&lt;/p&gt;

&lt;p&gt;If you want to build a template business, spend the first sprint on infra you won't use. It's the least glamorous advice I have.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson 2 — API keys never live in the RN bundle
&lt;/h2&gt;

&lt;p&gt;Both AI templates route through Supabase Edge Functions. The customer's OpenAI or Anthropic key sits at the edge. Client calls the function; function holds the key; RLS enforces per-user rate limits.&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;// Bad: key in the client&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;openai&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;OpenAI&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;EXPO_PUBLIC_OPENAI_KEY&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// Good: client calls edge function&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;SUPABASE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/functions/v1/ai-chat`&lt;/span&gt;&lt;span class="p"&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;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;access_token&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;body&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;messages&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;EXPO_PUBLIC_*&lt;/code&gt; bakes into the bundle. Anyone can &lt;code&gt;strings&lt;/code&gt; your &lt;code&gt;.ipa&lt;/code&gt; and pull it out. This is not theoretical — it happens weekly to indie apps.&lt;/p&gt;
&lt;h2&gt;
  
  
  Lesson 3 — One data contract, all templates
&lt;/h2&gt;

&lt;p&gt;Every template's marketing page on our site renders from the same TypeScript interface:&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;ProductData&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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="nx"&gt;tagline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;screenshots&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Screenshot&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
  &lt;span class="nx"&gt;pricing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PricingTier&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
  &lt;span class="nx"&gt;features&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Feature&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
  &lt;span class="nx"&gt;testimonials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Testimonial&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
  &lt;span class="nx"&gt;faqs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FAQ&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
  &lt;span class="nx"&gt;techStack&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TechItem&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;Sixty-three lines. Every template's config satisfies it. Every marketing component consumes it. Zero forks.&lt;/p&gt;

&lt;p&gt;Before this contract, we had five copies of the pricing card. A fix on one wasn't a fix on all. After the contract: one component, one bug fix, seven templates updated.&lt;/p&gt;
&lt;h2&gt;
  
  
  Lesson 4 — Ship CLAUDE.md, AGENTS.md, .cursorrules
&lt;/h2&gt;

&lt;p&gt;Every template ships with agent-readiness files. Short, opinionated, points to &lt;code&gt;lib/data/types.ts&lt;/code&gt; and lists invariants.&lt;/p&gt;

&lt;p&gt;Result: support tickets about "the agent broke something in your template" dropped noticeably. Not because agents got smarter — because they had a map.&lt;/p&gt;

&lt;p&gt;Structure that works:&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;# CLAUDE.md&lt;/span&gt;
&lt;span class="gu"&gt;## Invariants&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Do not touch &lt;span class="sb"&gt;`lib/data/types.ts`&lt;/span&gt; — it's the shared contract
&lt;span class="p"&gt;-&lt;/span&gt; Every new screen goes in &lt;span class="sb"&gt;`app/(tabs)/`&lt;/span&gt;, wired via Expo Router
&lt;span class="p"&gt;-&lt;/span&gt; Edge functions in &lt;span class="sb"&gt;`supabase/functions/`&lt;/span&gt;, never in the client

&lt;span class="gu"&gt;## File map&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; app/apps/config/&lt;span class="err"&gt;*&lt;/span&gt;.config.ts — template configs
&lt;span class="p"&gt;-&lt;/span&gt; app/apps/components/ — shared marketing UI
&lt;span class="p"&gt;-&lt;/span&gt; app/apps/lib/data/ — data layer

&lt;span class="gu"&gt;## When adding a feature&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; Update the config type first
&lt;span class="p"&gt;2.&lt;/span&gt; Then update the config value
&lt;span class="p"&gt;3.&lt;/span&gt; Then update the UI that reads it
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Lesson 5 — Keep the entry price low
&lt;/h2&gt;

&lt;p&gt;Templates range $49–$99. Temptation: raise everything to $99. Support-per-dollar is worse at $49.&lt;/p&gt;

&lt;p&gt;Reality: $49 buyers come back for $79–$99 templates. The cheap template is a paid trial. Kill it and the funnel collapses.&lt;/p&gt;
&lt;h2&gt;
  
  
  Lesson 6 — Docs convert, not screenshots
&lt;/h2&gt;

&lt;p&gt;Analytics: converting buyers spend more time in &lt;code&gt;/docs&lt;/code&gt; than on the marketing page. Specifically the "how do I swap the AI provider" sections.&lt;/p&gt;

&lt;p&gt;Writing docs first also forces you to design the escape hatch before the feature. If you can't write the "how do I swap this" section, the feature isn't done.&lt;/p&gt;
&lt;h2&gt;
  
  
  Lesson 7 — Stack review
&lt;/h2&gt;

&lt;p&gt;Would pick again:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Expo SDK 54, RN 0.81 — new arch stable&lt;/li&gt;
&lt;li&gt;NativeWind v4 — kills styling drift across templates&lt;/li&gt;
&lt;li&gt;Supabase — auth, Postgres, edge, storage, one bill&lt;/li&gt;
&lt;li&gt;TanStack Query v5 — replaced Redux everywhere&lt;/li&gt;
&lt;li&gt;Expo Router v6 — file-based routing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Would skip:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Zustand for state that TanStack Query handles. Two templates had Zustand stores that were purely query-cache reinventions. Removing them cost more than not adding them.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  For year two
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Config file exists before any RN code does&lt;/li&gt;
&lt;li&gt;Docs written before the feature&lt;/li&gt;
&lt;li&gt;CLAUDE.md written during design&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Related
&lt;/h2&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://www.applighter.com/blog/streaming-ai-responses-to-react-native-the-complete-guide" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdnimg.co%2F62e8b74c-52a4-4089-b3d3-fd82483028c1%2Fe462e274-b3ea-483b-b3fe-039834185114%2Fstreaming-ai-responses-to-react-native-the-complete-guide-abstract-illustration.jpg" height="449" class="m-0" width="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://www.applighter.com/blog/streaming-ai-responses-to-react-native-the-complete-guide" rel="noopener noreferrer" class="c-link"&gt;
            Streaming AI Responses to React Native: The Complete Guide
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Streaming AI Responses to React Native: The Complete Guide. Build fluid, conversational UIs with SSE, edge functions, &amp;amp; robust client-side rendering.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.applighter.com%2Ficon.svg%3Ficon.f2cf9ab1.svg" width="32" height="32"&gt;
          applighter.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;




&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://www.applighter.com/blog/react-native-supabase-template-auth-rls-storage" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1517694712202-14dd9538aa97%3Fauto%3Dformat%26fit%3Dcrop%26w%3D1600%26q%3D80" height="1067" class="m-0" width="1600"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://www.applighter.com/blog/react-native-supabase-template-auth-rls-storage" rel="noopener noreferrer" class="c-link"&gt;
            React Native Supabase Template: Auth, RLS, Storage
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            How Applighter ships React Native templates with Supabase auth, RLS policies, and Storage already wired — clone, run migrations, build features.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.applighter.com%2Ficon.svg%3Ficon.f2cf9ab1.svg" width="32" height="32"&gt;
          applighter.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>supabase</category>
      <category>applighter</category>
    </item>
    <item>
      <title>Wiring Designers Into Your EAS Preview Loop</title>
      <dc:creator>Famitha M A</dc:creator>
      <pubDate>Wed, 08 Jul 2026 08:15:28 +0000</pubDate>
      <link>https://dev.to/rapidnative-ai/wiring-designers-into-your-eas-preview-loop-1hil</link>
      <guid>https://dev.to/rapidnative-ai/wiring-designers-into-your-eas-preview-loop-1hil</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Figma reviews miss everything that's a function of the real device — touch targets, scroll momentum, keyboard behavior, font substitution, dark-mode transitions.&lt;/li&gt;
&lt;li&gt;Wrap &lt;code&gt;eas build --profile designer-preview&lt;/code&gt; in a Slack slash command so designers trigger their own preview builds without engineering.&lt;/li&gt;
&lt;li&gt;Use an EAS Update &lt;code&gt;designer-preview&lt;/code&gt; channel to push JS-only changes to an already-installed build — no reinstall, no rebuild.&lt;/li&gt;
&lt;li&gt;Remove three friction points first: the install step, the "which build is this?" question, and the "is this update live yet?" question.&lt;/li&gt;
&lt;li&gt;Measure &lt;strong&gt;review-loop time&lt;/strong&gt;, not build time.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Our mobile design reviews used to happen in Figma. Then a build would ship, the designer would see it on their phone a day later, and we'd realize the touch target was 8dp too small or the sheet animation felt wrong on Android. The fix was obvious in hindsight: put designers directly into the EAS preview loop so they see the change on a real device before signing off.&lt;/p&gt;

&lt;p&gt;Here's how we wired it up and what actually moved the needle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why design reviews on Figma miss real device behavior
&lt;/h2&gt;

&lt;p&gt;Figma reviews catch layout, spacing, and color drift. They miss everything that's a function of the device:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Touch target size and reachability on the specific phone the designer holds every day&lt;/li&gt;
&lt;li&gt;Momentum and rubber-banding on real ScrollViews&lt;/li&gt;
&lt;li&gt;Keyboard behavior (especially on iOS Safe Area vs Android IME resize)&lt;/li&gt;
&lt;li&gt;Actual font rendering — system font substitution differs by OS version&lt;/li&gt;
&lt;li&gt;Dark-mode transitions in a real app vs mocked screens&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every one of these has bitten us. And every one of them is invisible until a designer scrolls, taps, and feels the app on their own device.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-Slack-command pattern that unblocks designers
&lt;/h2&gt;

&lt;p&gt;The unlock was making it possible for a designer to trigger their own preview build without asking engineering. We wrapped &lt;code&gt;eas build --profile designer-preview --platform ios&lt;/code&gt; in a Slack slash command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/preview branch feat/new-onboarding
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command dispatches a GitHub Action that runs &lt;code&gt;eas build&lt;/code&gt; against the named branch, then posts the QR code + install link back into the Slack thread when it completes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/designer-preview.yml (excerpt)&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;designer-preview&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;branch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;true&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="pi"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;inputs.branch&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;expo/expo-github-action@v8&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;expo-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;latest&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="pi"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;secrets.EXPO_TOKEN&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;eas build --profile designer-preview --platform ios --non-interactive --no-wait&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The critical detail: &lt;code&gt;--profile designer-preview&lt;/code&gt; in &lt;code&gt;eas.json&lt;/code&gt; should be internal distribution, not TestFlight — TestFlight has an approval delay that breaks the loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  EAS Update channels for designer-only preview builds
&lt;/h2&gt;

&lt;p&gt;Once you have preview builds working, the second unlock is EAS Update for JS-only changes. Configure a &lt;code&gt;designer-preview&lt;/code&gt; channel in &lt;code&gt;eas.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;"build"&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;"designer-preview"&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;"channel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"designer-preview"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"distribution"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"internal"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"env"&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;"EXPO_PUBLIC_ENV"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"designer"&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;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;Any JS/TSX change (which is 80% of what designers care about — spacing, colors, typography, animation curves) can now be pushed to their existing installed build with &lt;code&gt;eas update --channel designer-preview --branch feat/spacing-fix&lt;/code&gt;. No new install, no new build. The designer opens the app, pulls to refresh, sees the change.&lt;/p&gt;

&lt;p&gt;This compressed our design-iteration loop from ~1 build/day to ~4-6 JS pushes/hour when we're in an active review.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three friction points to remove before you invite designers
&lt;/h2&gt;

&lt;p&gt;Before rolling this out, remove these:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The install step.&lt;/strong&gt; Designers should not have to install Expo Dev Client from scratch every time. Ship one Dev Client build a week, keep it stable, and use EAS Update on top of it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The "which build is this?" question.&lt;/strong&gt; Add a corner label to your app in the designer env showing the branch name + short commit hash. A &lt;code&gt;useConstants()&lt;/code&gt; hook + a semi-transparent &lt;code&gt;Text&lt;/code&gt; component is enough:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&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;Constants&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;expo-constants&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;manifest&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Constants&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// In your root layout:&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;watermark&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;manifest&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;branch&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; · &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;manifest&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;commit&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;ol&gt;
&lt;li&gt;
&lt;strong&gt;The "is this update live yet?" question.&lt;/strong&gt; Show the last-updated timestamp somewhere the designer can find (a debug menu, a settings screen). Nothing kills trust in the loop like reviewing an old version.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Conclusion: measure the review-loop time, not build time
&lt;/h2&gt;

&lt;p&gt;After wiring this up, our design-to-shipped-mobile-build time dropped roughly 40%. But the metric that actually mattered wasn't build time — it was &lt;strong&gt;review-loop time&lt;/strong&gt;: how long between "designer flags a change" and "engineer knows if the fix is right."&lt;/p&gt;

&lt;p&gt;Measure that instead of build time and you'll instrument the right optimizations. Faster builds are useful; a designer who can review on-device without waiting for engineering is transformative.&lt;/p&gt;




&lt;p&gt;Are you running designers through your EAS loop, or still bouncing builds over Slack manually? Drop a comment with how your review loop works — especially if you've found a cleaner way to handle the "which build is this?" problem.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>designops</category>
      <category>mobile</category>
    </item>
    <item>
      <title>How to Build a HIPAA-Compliant Healthcare App in React Native (2026 Guide)</title>
      <dc:creator>Famitha M A</dc:creator>
      <pubDate>Tue, 30 Jun 2026 13:12:04 +0000</pubDate>
      <link>https://dev.to/rapidnative-ai/how-to-build-a-hipaa-compliant-healthcare-app-in-react-native-2026-guide-1ppk</link>
      <guid>https://dev.to/rapidnative-ai/how-to-build-a-hipaa-compliant-healthcare-app-in-react-native-2026-guide-1ppk</guid>
      <description>&lt;h1&gt;
  
  
  How to Build a HIPAA-Compliant Healthcare App in React Native (2026 Guide)
&lt;/h1&gt;

&lt;p&gt;A single HIPAA violation in 2026 can cost up to &lt;strong&gt;$2,190,294&lt;/strong&gt;. Per violation. Not per incident.&lt;/p&gt;

&lt;p&gt;If you're a React Native developer building a telemedicine, patient portal, or health-tracking app that handles Protected Health Information (PHI), this is the technical playbook I wish someone had handed me before I started. Code-first, no hand-waving.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 60-Second Mental Model
&lt;/h2&gt;

&lt;p&gt;HIPAA applies if you're a &lt;strong&gt;covered entity&lt;/strong&gt; (provider, payer, clearinghouse) or a &lt;strong&gt;business associate&lt;/strong&gt; (vendor processing PHI on their behalf). It splits into three rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Privacy Rule&lt;/strong&gt; → who can see PHI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security Rule&lt;/strong&gt; → technical, admin, and physical safeguards on ePHI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Breach Notification Rule&lt;/strong&gt; → what you do when something goes wrong&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Security Rule is where 90% of your code lives.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Tag Your Data Surface Before You Write Code
&lt;/h2&gt;

&lt;p&gt;Open a spreadsheet. Write down every field your app stores, sends, or displays. Tag each one:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bucket&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;th&gt;HIPAA controls&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PHI&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;patientName&lt;/code&gt;, &lt;code&gt;dob&lt;/code&gt;, &lt;code&gt;medicalRecordNumber&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Full controls (encrypt, log, RBAC)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;De-identified&lt;/td&gt;
&lt;td&gt;Aggregated stats with no identifiers&lt;/td&gt;
&lt;td&gt;None required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Non-PHI&lt;/td&gt;
&lt;td&gt;UI preferences, app version&lt;/td&gt;
&lt;td&gt;Standard hygiene&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This exercise also tells you what NOT to send to Sentry, PostHog, Mixpanel, or any non-BAA vendor. A stack trace with a patient's name in a URL parameter is a breach.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Lock In a HIPAA-Eligible Stack
&lt;/h2&gt;

&lt;p&gt;You cannot build a compliant app on a non-eligible backend. Decision matrix:&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;Pick&lt;/th&gt;
&lt;th&gt;Watch out for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hosting&lt;/td&gt;
&lt;td&gt;AWS / GCP / Azure (HIPAA-eligible services only)&lt;/td&gt;
&lt;td&gt;Default Vercel &amp;amp; Netlify don't sign BAAs on free tiers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth&lt;/td&gt;
&lt;td&gt;AWS Cognito, Auth0 Enterprise&lt;/td&gt;
&lt;td&gt;Firebase Auth (free tier) won't sign&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DB&lt;/td&gt;
&lt;td&gt;RDS, Aurora, Firestore Enterprise&lt;/td&gt;
&lt;td&gt;Enable encryption-at-rest&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage&lt;/td&gt;
&lt;td&gt;S3 with SSE-KMS&lt;/td&gt;
&lt;td&gt;Never use public buckets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Email/SMS&lt;/td&gt;
&lt;td&gt;AWS SES, Twilio HIPAA tier&lt;/td&gt;
&lt;td&gt;Default Twilio is not covered&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LLM&lt;/td&gt;
&lt;td&gt;Anthropic, OpenAI API, AWS Bedrock&lt;/td&gt;
&lt;td&gt;Consumer ChatGPT is NOT covered&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Push&lt;/td&gt;
&lt;td&gt;OneSignal HIPAA tier, AWS SNS&lt;/td&gt;
&lt;td&gt;Never put PHI in payloads&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Rule of thumb: free tiers are almost never HIPAA-eligible. Budget the paid tier from day one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: The React Native Patterns That Matter
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Encryption at rest
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ✅ Tokens, secrets&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;SecureStore&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;expo-secure-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;await&lt;/span&gt; &lt;span class="nx"&gt;SecureStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItemAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auth_token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ Large cached data&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;open&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-mmkv&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;storage&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;MMKV&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;phi-cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;encryptionKey&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;getKeyFromKeychain&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// ❌ Never&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;AsyncStorage&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-async-storage/async-storage&lt;/span&gt;&lt;span class="dl"&gt;'&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;AsyncStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;patient&lt;/span&gt;&lt;span class="dl"&gt;'&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;patient&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// plaintext on disk&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  TLS + cert pinning
&lt;/h3&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;fetch&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-ssl-pinning&lt;/span&gt;&lt;span class="dl"&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;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://api.yourapp.com/patients&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;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;GET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;sslPinning&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;certs&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;cert-fingerprint&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;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;token&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Background screen lock
&lt;/h3&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;useEffect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useState&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;AppState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Modal&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;BlurView&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;expo-blur&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;usePrivacyScreen&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;isInactive&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setIsInactive&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&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="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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sub&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;AppState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;change&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;state&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;setIsInactive&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;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;active&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;return &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;sub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&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;return&lt;/span&gt; &lt;span class="nx"&gt;isInactive&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;lt;&lt;/span&gt;&lt;span class="nx"&gt;Modal&lt;/span&gt; &lt;span class="nx"&gt;visible&lt;/span&gt; &lt;span class="nx"&gt;transparent&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;BlurView&lt;/span&gt; &lt;span class="nx"&gt;intensity&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;}&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;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="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Modal&lt;/span&gt;&lt;span class="err"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PHI doesn't show up in the iOS app switcher.&lt;/p&gt;

&lt;h3&gt;
  
  
  Session timeout + biometric re-auth
&lt;/h3&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="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;LocalAuthentication&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;expo-local-authentication&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;INACTIVITY_LIMIT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// clinician = 15min&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;ensureBiometric&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;LocalAuthentication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;authenticateAsync&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;promptMessage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Confirm to access patient data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;fallbackLabel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Use passcode&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;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;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;success&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Auth failed&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;h3&gt;
  
  
  Push notifications: no PHI in payloads
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Breach&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Dr. Patel: your diabetes follow-up is at 2pm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ Generic notification, fetch detail in-app&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;You have a new appointment&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;h3&gt;
  
  
  Audit logs
&lt;/h3&gt;

&lt;p&gt;Every read, write, login, export. Server-side, immutable, 6-year retention. Minimum schema:&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;type&lt;/span&gt; &lt;span class="nx"&gt;AuditEntry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;timestamp&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;actorId&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;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;view&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;create&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;update&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;delete&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;export&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;resourceType&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;resourceId&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;sourceIp&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;deviceId&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;outcome&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;success&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;failure&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;Ship to S3 with Object Lock or CloudWatch with retention policies. The app should have &lt;em&gt;no permission&lt;/em&gt; to delete from this log.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Where AI App Builders Fit
&lt;/h2&gt;

&lt;p&gt;I work on &lt;a href="https://www.rapidnative.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=how-to-build-hipaa-compliant-healthcare-app" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt;, an AI builder that generates React Native + Expo apps from a prompt. Honest take on what it can and can't do for HIPAA:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Speeds up massively:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Patient flows, intake forms, dashboards, scheduling, chat scaffolds&lt;/li&gt;
&lt;li&gt;State management, navigation, RBAC scaffolds&lt;/li&gt;
&lt;li&gt;Real-time iteration with a clinical advisor in the loop&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Doesn't do (and no AI builder does):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sign your BAA&lt;/li&gt;
&lt;li&gt;Provision your HIPAA-eligible AWS environment&lt;/li&gt;
&lt;li&gt;Write your privacy officer's policy&lt;/li&gt;
&lt;li&gt;Replace a security audit&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It compresses ~8-12 weeks of UI work into days. Your team spends the recovered time on compliance work that actually needs human judgment. Code exports cleanly so you can audit and pen-test it like any hand-written code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Pitfalls That Will Bite You
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Storing PHI in AsyncStorage.&lt;/strong&gt; It's plaintext. Use SecureStore or encrypted MMKV.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Default Sentry without scrubbing PHI.&lt;/strong&gt; Your stack traces are walking breaches.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free-tier auth providers.&lt;/strong&gt; Most don't sign BAAs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Push notification bodies.&lt;/strong&gt; They sit unencrypted on Apple/Google servers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Background screen exposure.&lt;/strong&gt; Patient names in the app switcher.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logging request/response bodies on backend.&lt;/strong&gt; Logs are PHI too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Using consumer ChatGPT for AI features.&lt;/strong&gt; Use API tier + BAA, or Anthropic, or Bedrock.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Checklist
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;✅&lt;/th&gt;
&lt;th&gt;Control&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;☐&lt;/td&gt;
&lt;td&gt;Data tagged: PHI / de-identified / non-PHI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;☐&lt;/td&gt;
&lt;td&gt;BAAs signed for every PHI-touching vendor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;☐&lt;/td&gt;
&lt;td&gt;Encryption at rest via SecureStore + encrypted MMKV/SQLite&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;☐&lt;/td&gt;
&lt;td&gt;TLS 1.3 + certificate pinning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;☐&lt;/td&gt;
&lt;td&gt;MFA + biometric re-auth&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;☐&lt;/td&gt;
&lt;td&gt;15/30-minute session timeouts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;☐&lt;/td&gt;
&lt;td&gt;Background screen blur&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;☐&lt;/td&gt;
&lt;td&gt;Server-enforced RBAC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;☐&lt;/td&gt;
&lt;td&gt;Immutable audit log, 6-year retention&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;☐&lt;/td&gt;
&lt;td&gt;PHI-free push notification payloads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;☐&lt;/td&gt;
&lt;td&gt;Annual risk assessment + penetration test&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;☐&lt;/td&gt;
&lt;td&gt;Documented breach response plan&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;If you want to scaffold a healthcare flow in minutes (and then wire it to a compliant backend on your own timeline), give RapidNative a try. Free, no credit card.&lt;/p&gt;

&lt;p&gt;Originally published on &lt;a href="https://www.rapidnative.com/blogs/how-to-build-hipaa-compliant-healthcare-app" rel="noopener noreferrer"&gt;rapidnative.com/blogs&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>healthcare</category>
      <category>mobile</category>
      <category>security</category>
    </item>
    <item>
      <title>How to Build a HIPAA-Compliant Healthcare App in React Native (2026)</title>
      <dc:creator>Famitha M A</dc:creator>
      <pubDate>Wed, 27 May 2026 06:21:14 +0000</pubDate>
      <link>https://dev.to/rapidnative-ai/how-to-build-a-hipaa-compliant-healthcare-app-in-react-native-2026-27ne</link>
      <guid>https://dev.to/rapidnative-ai/how-to-build-a-hipaa-compliant-healthcare-app-in-react-native-2026-27ne</guid>
      <description>&lt;h1&gt;
  
  
  How to Build a HIPAA-Compliant Healthcare App in React Native (2026)
&lt;/h1&gt;

&lt;p&gt;I've spent the last few years watching healthcare startups ship apps that wouldn't survive a five-minute OCR audit: plaintext PHI in CloudWatch logs, Firebase pulling double duty as analytics &lt;em&gt;and&lt;/em&gt; PHI database, and "we'll add a BAA later" as a roadmap item. So here's the actual developer checklist for shipping a HIPAA-compliant React Native + Expo app in 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  First, are you actually on the hook?
&lt;/h2&gt;

&lt;p&gt;HIPAA applies if you are a Covered Entity (provider, plan, clearinghouse) or a Business Associate (vendor processing PHI on behalf of a CE). A consumer wellness app where users self-report data and you have no provider contracts is usually out of scope. The second a clinic signs up, you're in. Get this in writing from a lawyer before you build anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 9 technical safeguards, in code terms
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Encryption everywhere&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;TLS 1.3 minimum on the wire. Pin certs with &lt;code&gt;react-native-ssl-pinning&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;AES-256 at rest in the DB.&lt;/li&gt;
&lt;li&gt;On device: &lt;code&gt;react-native-keychain&lt;/code&gt; for credentials, &lt;code&gt;expo-secure-store&lt;/code&gt; for tokens. Never AsyncStorage for PHI.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. Unique auth + MFA&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One identity per human. No shared logins.&lt;/li&gt;
&lt;li&gt;MFA via TOTP or push (not SMS) for any account touching PHI.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;3. RBAC, not "is_admin"&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bake roles into your data model from migration #1. Retrofitting RBAC into a healthcare app is the worst kind of refactor.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;4. Immutable audit logs&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every PHI read, write, export, print. Append-only table or log stream. Six-year retention.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;5. Auto logoff&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;15-minute inactivity timeout for providers, configurable per role.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;6. Integrity controls&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Row-level audit trails or CDC. Be able to prove a chart wasn't tampered with.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;7. Transmission security&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Push notifications: never put PHI in the body. The notification can say "New message," not "Lab result: positive."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;8. Device controls&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MDM integration for shared/clinic devices. Remote wipe.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;9. Risk analysis&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A document. Annual. OCR audits start here. NIST SP 800-66 is the template.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The stack that actually has BAAs
&lt;/h2&gt;

&lt;p&gt;The single biggest compliance lever is vendor selection. Every component touching PHI needs a BAA.&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 works&lt;/th&gt;
&lt;th&gt;What to avoid&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cloud&lt;/td&gt;
&lt;td&gt;AWS, GCP, Azure&lt;/td&gt;
&lt;td&gt;Anything that won't sign a BAA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DB&lt;/td&gt;
&lt;td&gt;RDS, Supabase Team+, Aiven&lt;/td&gt;
&lt;td&gt;Firebase Firestore (no BAA)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth&lt;/td&gt;
&lt;td&gt;Cognito, Auth0 Enterprise, Stytch&lt;/td&gt;
&lt;td&gt;Free tiers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Analytics&lt;/td&gt;
&lt;td&gt;Heap, Amplitude Enterprise, self-hosted PostHog&lt;/td&gt;
&lt;td&gt;Google Analytics, Firebase Analytics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Error monitoring&lt;/td&gt;
&lt;td&gt;Sentry Business+&lt;/td&gt;
&lt;td&gt;Free Sentry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI&lt;/td&gt;
&lt;td&gt;Anthropic (BAA), Bedrock, Azure OpenAI&lt;/td&gt;
&lt;td&gt;OpenAI free/standard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Email&lt;/td&gt;
&lt;td&gt;Paubox, AWS SES + BAA&lt;/td&gt;
&lt;td&gt;Mailchimp, SendGrid free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Video&lt;/td&gt;
&lt;td&gt;Daily.co, Twilio Video, Zoom Healthcare&lt;/td&gt;
&lt;td&gt;Vanilla Zoom&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If a vendor won't sign a BAA, they don't see PHI. Maintain two analytics streams: a PHI-free one for general behavior and a fully BAA-covered one for anything PHI-adjacent.&lt;/p&gt;

&lt;h2&gt;
  
  
  React Native specifics
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Use EAS Update for OTA security patches — critical for incident response when the App Store review queue is 3 days deep.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;react-native-encrypted-storage&lt;/code&gt; for any local PHI cache.&lt;/li&gt;
&lt;li&gt;Scrub PHI from Sentry breadcrumbs with &lt;code&gt;beforeSend&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Disable screenshots on PHI screens via &lt;code&gt;react-native-prevent-screenshot&lt;/code&gt; or &lt;code&gt;FLAG_SECURE&lt;/code&gt; on Android.&lt;/li&gt;
&lt;li&gt;Biometric unlock for patient apps is fine; providers should re-auth more aggressively.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where AI app builders fit (and don't)
&lt;/h2&gt;

&lt;p&gt;The patient-facing UI — onboarding, intake forms, appointment screens, messaging — has zero compliance value on its own. It's pixels. The compliance value lives in the backend (where PHI is stored, accessed, audited).&lt;/p&gt;

&lt;p&gt;So a sane workflow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Generate the React Native UI fast with a tool like &lt;a href="https://www.rapidnative.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=hipaa-compliant-healthcare-app" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt; (exportable Expo code).&lt;/li&gt;
&lt;li&gt;Wire it to your own BAA-covered backend (RDS + Cognito + KMS + audit logging).&lt;/li&gt;
&lt;li&gt;The builder never sees PHI; you own the deployed code and the data layer.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is the same logical split as using Figma for design or Storybook for components.&lt;/p&gt;

&lt;h2&gt;
  
  
  A realistic timeline
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Weeks 0–2: legal scoping, BAAs, initial risk analysis&lt;/li&gt;
&lt;li&gt;Weeks 2–4: backend architecture with RBAC + audit logging from day one&lt;/li&gt;
&lt;li&gt;Weeks 4–12: feature build (this is where AI builders compress UI work the most)&lt;/li&gt;
&lt;li&gt;Weeks 12–16: penetration test, training, BAA paperwork with launch customers&lt;/li&gt;
&lt;li&gt;Ongoing: quarterly access reviews, annual risk analysis, incident response drills&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Budget for an MVP: $70k–$250k all-in, with 15–25% compliance overhead vs. a comparable non-HIPAA app.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Confirm you're actually in scope.&lt;/li&gt;
&lt;li&gt;BAAs before code.&lt;/li&gt;
&lt;li&gt;RBAC and audit logging from migration #1.&lt;/li&gt;
&lt;li&gt;Encrypt in transit (TLS 1.3) and at rest (AES-256). No PHI in logs.&lt;/li&gt;
&lt;li&gt;Two analytics streams: PHI-free and BAA-covered.&lt;/li&gt;
&lt;li&gt;Use AI builders for UI scaffolding (no compliance value), hand-build the backend (all the compliance value).&lt;/li&gt;
&lt;li&gt;Annual risk analysis. Document everything.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What's your stack looking like? Drop a comment with what you're building — especially curious how others are handling the analytics split and PHI scrubbing in Sentry.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>healthcare</category>
      <category>mobile</category>
      <category>security</category>
    </item>
    <item>
      <title>Zustand vs Redux vs Jotai: React Native State Management in 2026</title>
      <dc:creator>Famitha M A</dc:creator>
      <pubDate>Tue, 19 May 2026 14:02:18 +0000</pubDate>
      <link>https://dev.to/famitha_ma_b9c13ab1d324e/zustand-vs-redux-vs-jotai-react-native-state-management-in-2026-2703</link>
      <guid>https://dev.to/famitha_ma_b9c13ab1d324e/zustand-vs-redux-vs-jotai-react-native-state-management-in-2026-2703</guid>
      <description>&lt;h1&gt;
  
  
  Zustand vs Redux vs Jotai: React Native State Management in 2026
&lt;/h1&gt;

&lt;p&gt;Pick the wrong state library for React Native in 2026 and you'll pay in cold-start time on a $150 Android, re-renders that drop your list from 60fps, and a long tail of &lt;code&gt;useEffect&lt;/code&gt; bugs. Three libraries cover ~95% of production apps today: Zustand, Redux Toolkit, and Jotai. Here's how they actually compare on mobile.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use Zustand&lt;/strong&gt; for most new React Native apps (1.2KB, no boilerplate, perfect Hermes compat)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use Redux Toolkit&lt;/strong&gt; for 10+ engineer teams or compliance-heavy apps&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use Jotai&lt;/strong&gt; when you have hundreds of independent UI pieces (forms, canvases)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The modern default is &lt;strong&gt;Zustand + TanStack Query&lt;/strong&gt; — Zustand for UI state, TanStack Query for server state.&lt;/p&gt;

&lt;h2&gt;
  
  
  2026 Benchmarks
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Library&lt;/th&gt;
&lt;th&gt;Bundle gzipped&lt;/th&gt;
&lt;th&gt;Weekly downloads&lt;/th&gt;
&lt;th&gt;Memory (1000 components)&lt;/th&gt;
&lt;th&gt;Parse time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Zustand&lt;/td&gt;
&lt;td&gt;1.2 KB&lt;/td&gt;
&lt;td&gt;14.2M&lt;/td&gt;
&lt;td&gt;2.1 MB&lt;/td&gt;
&lt;td&gt;8 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jotai&lt;/td&gt;
&lt;td&gt;2.1 KB&lt;/td&gt;
&lt;td&gt;~5.4M&lt;/td&gt;
&lt;td&gt;1.8 MB&lt;/td&gt;
&lt;td&gt;9 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTK + react-redux&lt;/td&gt;
&lt;td&gt;13.8 KB&lt;/td&gt;
&lt;td&gt;9.8M&lt;/td&gt;
&lt;td&gt;3.2 MB&lt;/td&gt;
&lt;td&gt;34 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Zustand crossed Redux Toolkit in weekly downloads in 2025. On a Samsung Galaxy A14, RTK's parse cost shows up as visible TTI regression.&lt;/p&gt;

&lt;h2&gt;
  
  
  Zustand: The Pragmatic Default
&lt;/h2&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;create&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;zustand&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;useCart&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;CartStore&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
  &lt;span class="na"&gt;addItem&lt;/span&gt;&lt;span class="p"&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="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="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;items&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;items&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="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 provider, no reducer, no &lt;code&gt;dispatch&lt;/code&gt;. Selectors are what make it fast — components only re-render when their selected slice changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For React Native specifically:&lt;/strong&gt; persistence is one line with AsyncStorage or MMKV:&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;persist&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;createJSONStorage&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;zustand/middleware&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;AsyncStorage&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-async-storage/async-storage&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;useCart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;persist&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;CartStore&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="kd"&gt;set&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="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cart&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;createJSONStorage&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;AsyncStorage&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;Swap to &lt;a href="https://github.com/mrousavy/react-native-mmkv" rel="noopener noreferrer"&gt;react-native-mmkv&lt;/a&gt; (5-10× faster) by changing one line.&lt;/p&gt;

&lt;h2&gt;
  
  
  Redux Toolkit: Still Right for Big Teams
&lt;/h2&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="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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cartSlice&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;cart&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;items&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;Item&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="na"&gt;addItem&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;action&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;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&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;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="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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cartSlice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reducer&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 to use it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;10+ engineers — predictable structure stops merge chaos&lt;/li&gt;
&lt;li&gt;Time-travel debugging matters (fintech, complex forms)&lt;/li&gt;
&lt;li&gt;You need RTK Query for cache normalization&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When not to: solo dev, MVP, or anywhere bundle size matters. The 13.8KB cost is real on RN.&lt;/p&gt;

&lt;h2&gt;
  
  
  Jotai: Atomic State
&lt;/h2&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;atom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useAtom&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;jotai&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;itemsAtom&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;atom&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;totalAtom&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;atom&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="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;itemsAtom&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;reduce&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;i&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="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;price&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;Total&lt;/code&gt; component only re-renders when &lt;code&gt;totalAtom&lt;/code&gt;'s value changes. The dependency graph is implicit in &lt;code&gt;get&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Use Jotai for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Form-heavy screens (each input = one atom = surgical re-renders)&lt;/li&gt;
&lt;li&gt;Visual editors / canvases&lt;/li&gt;
&lt;li&gt;Anywhere "many small reactive pieces" describes your UI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Skip it for global app settings or simple stores — Zustand's mental model is easier for those.&lt;/p&gt;

&lt;h2&gt;
  
  
  The React Native–Specific Bits
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Hermes compatibility:&lt;/strong&gt; All three work. Redux Toolkit's Immer relies on Proxy, which was historically slower on Hermes; that gap has closed but heavy nested updates are still measurably slower.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;New Architecture (Fabric/JSI):&lt;/strong&gt; No library breaks. Lower-overhead libraries (Zustand, Jotai) see proportionally bigger wins from Fabric's synchronous layout.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Persistence:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Zustand: &lt;code&gt;persist&lt;/code&gt; middleware, one line, async non-blocking&lt;/li&gt;
&lt;li&gt;Redux: &lt;code&gt;redux-persist&lt;/code&gt; (3rd party, maintenance backlog) or manual&lt;/li&gt;
&lt;li&gt;Jotai: &lt;code&gt;atomWithStorage&lt;/code&gt; per atom&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cold start on low-end Android:&lt;/strong&gt; Saving 25ms parse time by picking Zustand over RTK isn't dramatic, but on a screen that already feels slow it shows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision Framework
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Solo / MVP / &amp;lt;10 engineers → &lt;strong&gt;Zustand&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Form-heavy or canvas-heavy → &lt;strong&gt;Jotai&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;10+ engineers or compliance → &lt;strong&gt;Redux Toolkit&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Mostly fetching server data → &lt;strong&gt;Zustand + TanStack Query&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Unsure → &lt;strong&gt;Zustand&lt;/strong&gt; (easy to migrate later)&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Patterns That Beat the Default
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zustand + TanStack Query&lt;/strong&gt; — UI state in Zustand, server state in TanStack. ~90% of modern RN apps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Jotai atoms + Zustand for settings&lt;/strong&gt; — atoms for forms/canvases, Zustand slice for theme/auth/locale.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redux Toolkit + RTK Query for everything&lt;/strong&gt; — if you're going Redux, go all in. Half-and-half duplicates logic.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is Zustand better than Redux for React Native?&lt;/strong&gt; For most apps in 2026, yes. 12× smaller, no provider, perfect Hermes compat. Redux Toolkit still wins for large teams or strict auditing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does Jotai work with React Native?&lt;/strong&gt; Yes. Zero React Native–specific dependencies. Runs on Hermes, JSC, and Expo. &lt;code&gt;atomWithStorage&lt;/code&gt; handles AsyncStorage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should I use Redux for a new RN app?&lt;/strong&gt; Only if you already know it deeply, your team is large, or you need RTK Query's cache. Otherwise Zustand is the default.&lt;/p&gt;




&lt;p&gt;If you're scaffolding a new React Native app and want the state layer wired up automatically, &lt;a href="https://www.rapidnative.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=zustand-vs-redux-vs-jotai-react-native-2026" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt; ships Zustand + AsyncStorage + TanStack Query out of the box. Production-ready Expo code, exportable, no boilerplate.&lt;/p&gt;

&lt;p&gt;What state library do you ship in production? Drop it in the comments — I'm curious how the split is looking on bigger teams in 2026.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Build a Property Listing App Like Zillow with React Native</title>
      <dc:creator>Famitha M A</dc:creator>
      <pubDate>Mon, 18 May 2026 07:36:09 +0000</pubDate>
      <link>https://dev.to/famitha_ma_b9c13ab1d324e/how-to-build-a-property-listing-app-like-zillow-with-react-native-4j30</link>
      <guid>https://dev.to/famitha_ma_b9c13ab1d324e/how-to-build-a-property-listing-app-like-zillow-with-react-native-4j30</guid>
      <description>&lt;h1&gt;
  
  
  How to Build a Property Listing App Like Zillow with React Native
&lt;/h1&gt;

&lt;p&gt;Zillow's mobile apps serve ~220M monthly unique users and show 157 homes per second. Behind that scale is a surprisingly small core: four screens, one map, one search index. Here's how to build the same thing in React Native + Expo — once the manual way, and once with AI doing the boilerplate.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four screens you actually need
&lt;/h2&gt;

&lt;p&gt;Strip Zillow down to its load-bearing parts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Listings feed&lt;/strong&gt; — paginated &lt;code&gt;FlatList&lt;/code&gt; of property cards&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Map view&lt;/strong&gt; — &lt;code&gt;react-native-maps&lt;/code&gt; with price-bubble markers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Property detail&lt;/strong&gt; — photo carousel + agent contact CTA&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Saved + profile&lt;/strong&gt; — favorites synced to auth&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Everything else (mortgage origination, 3D tours, AR staging) is post-PMF.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx create-expo-app@latest zillow-clone
npx expo &lt;span class="nb"&gt;install &lt;/span&gt;react-native-maps expo-location expo-router expo-image
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pair with &lt;strong&gt;Supabase&lt;/strong&gt; for auth/data and &lt;strong&gt;Algolia&lt;/strong&gt; for search. That's the modern default for React Native real estate apps in 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  The map screen is where MVPs die
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;react-native-maps&lt;/code&gt; is easy. Making it survive 1,000+ markers on a mid-range Android device is the actual engineering work. Three patterns that hold up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cluster at low zoom&lt;/strong&gt; (&lt;code&gt;react-native-maps-super-cluster&lt;/code&gt; or H3 grids)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Price bubbles, not pins&lt;/strong&gt; — users want prices, not red drops&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debounce &lt;code&gt;onRegionChangeComplete&lt;/code&gt;&lt;/strong&gt; by 300-400ms before refetching
&lt;/li&gt;
&lt;/ul&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;MapView&lt;/span&gt; &lt;span class="na"&gt;onRegionChangeComplete&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;debouncedFetch&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;listings&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;l&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;Marker&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;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;coordinate&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;latitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;longitude&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;PriceBubble&lt;/span&gt; &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;price&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;Marker&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;MapView&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;Without debouncing, every pan fires a query storm and your Supabase logs cry.&lt;/p&gt;

&lt;h2&gt;
  
  
  The listings feed
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;FlatList&lt;/code&gt;, not &lt;code&gt;ScrollView&lt;/code&gt;. Set &lt;code&gt;initialNumToRender={6}&lt;/code&gt; and &lt;code&gt;windowSize={5}&lt;/code&gt;. Use &lt;code&gt;expo-image&lt;/code&gt; instead of the default &lt;code&gt;Image&lt;/code&gt; — it gives you caching, blur placeholders, and WebP for free. Property listings average 25-40 photos; the default &lt;code&gt;Image&lt;/code&gt; component will hate you for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Property detail screen
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;app/listing/[id].tsx&lt;/code&gt; with Expo Router. Dynamic routes give you shareable URLs which matter later for deep links from push notifications ("Price drop on 123 Main St!").&lt;/p&gt;

&lt;p&gt;Structure: photo carousel → price + facts → sticky "Contact agent" → description → amenities → mortgage estimate → similar listings. Lazy-load everything below the photo gallery.&lt;/p&gt;

&lt;h2&gt;
  
  
  Search: don't roll your own
&lt;/h2&gt;

&lt;p&gt;The temptation is to write &lt;code&gt;ILIKE&lt;/code&gt; queries on Postgres for filters. It works until you have ~5,000 listings, then latency explodes. Wire up &lt;strong&gt;Algolia&lt;/strong&gt; or &lt;strong&gt;Typesense&lt;/strong&gt; before you cross 1,000 records.&lt;/p&gt;

&lt;h2&gt;
  
  
  The AI shortcut
&lt;/h2&gt;

&lt;p&gt;If 3-6 weeks of scaffolding sounds painful, &lt;a href="https://www.rapidnative.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=how-to-build-a-property-listing-app-like-zillow-with-react-native" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt; scaffolds all four screens, the map, and the navigation tree from a prompt:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Build a property listing app like Zillow. Listings feed with photo, price, beds/baths cards. Map tab with price-bubble markers. Detail screen with photo carousel and contact agent button. Saved homes and profile tabs.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You watch it render in live preview, point-and-edit anything you don't like, scan a QR code to test on your real phone, then export the full React Native + Expo project. It's not a runtime — it's actual code you own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pitfalls
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Don't scrape MLS data.&lt;/strong&gt; Use IDX/RETS agreements or licensed aggregators (Bridge, Realtyna, Estated).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't ship without &lt;code&gt;expo-image&lt;/code&gt;.&lt;/strong&gt; Default &lt;code&gt;Image&lt;/code&gt; perf on listings with 30+ photos is brutal on Android.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't write search yourself.&lt;/strong&gt; Algolia free tier handles 10K records before you pay a dollar.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;The Zillow surface area is intimidating; the Zillow core is four screens. Start with the scaffolding — manually or with AI — and earn the right to add the fancy stuff once users actually retain.&lt;/p&gt;

&lt;p&gt;Full template + code: &lt;a href="https://www.rapidnative.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=how-to-build-a-property-listing-app-like-zillow-with-react-native" rel="noopener noreferrer"&gt;RapidNative Real Estate starter&lt;/a&gt;&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>ai</category>
      <category>mobile</category>
    </item>
  </channel>
</rss>
