<?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>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/famitha_ma_b9c13ab1d324e/how-to-build-a-grocery-delivery-app-with-react-native-expo-2026-f2d</link>
      <guid>https://dev.to/famitha_ma_b9c13ab1d324e/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/famitha_ma_b9c13ab1d324e/7-lessons-shipping-react-native-templates-1b21</link>
      <guid>https://dev.to/famitha_ma_b9c13ab1d324e/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/famitha_ma_b9c13ab1d324e/wiring-designers-into-your-eas-preview-loop-1hil</link>
      <guid>https://dev.to/famitha_ma_b9c13ab1d324e/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/famitha_ma_b9c13ab1d324e/how-to-build-a-hipaa-compliant-healthcare-app-in-react-native-2026-guide-1ppk</link>
      <guid>https://dev.to/famitha_ma_b9c13ab1d324e/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/famitha_ma_b9c13ab1d324e/how-to-build-a-hipaa-compliant-healthcare-app-in-react-native-2026-27ne</link>
      <guid>https://dev.to/famitha_ma_b9c13ab1d324e/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>
    <item>
      <title>Mobile App Launch Checklist — 25 Things to Verify Before You Hit Publish</title>
      <dc:creator>Famitha M A</dc:creator>
      <pubDate>Fri, 15 May 2026 13:13:32 +0000</pubDate>
      <link>https://dev.to/famitha_ma_b9c13ab1d324e/mobile-app-launch-checklist-25-things-to-verify-before-you-hit-publish-194p</link>
      <guid>https://dev.to/famitha_ma_b9c13ab1d324e/mobile-app-launch-checklist-25-things-to-verify-before-you-hit-publish-194p</guid>
      <description>&lt;h1&gt;
  
  
  Mobile App Launch Checklist: 25 Things to Verify Before You Hit Publish
&lt;/h1&gt;

&lt;p&gt;You're staring at the "Submit for Review" button. The icon looks right. Screens feel good on your phone. But something is nagging at you.&lt;/p&gt;

&lt;p&gt;That feeling is usually accurate. Most apps don't get rejected for bad code — they get rejected for a missing privacy disclosure, a wrong-resolution screenshot, or a forgotten test build that just trapped your brand-new Google Play account in a 14-day waiting period.&lt;/p&gt;

&lt;p&gt;Here are 25 items I run before every submission, grouped into five categories. Save the post, print the list, then hit publish.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reality check before you start
&lt;/h2&gt;

&lt;p&gt;Apple averages 24–48 hours review in 2026 for repeat developers. Google Play, 1–3 hours. First-time submission from a brand-new dev account? 7–14 days. Always add a &lt;strong&gt;2-week buffer&lt;/strong&gt; between target launch and first submission to handle one rejection cycle.&lt;/p&gt;

&lt;h2&gt;
  
  
  A. Build &amp;amp; Bundle (1–5)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Sign a production build&lt;/strong&gt; — iOS &lt;code&gt;.ipa&lt;/code&gt; from your distribution profile; Android &lt;code&gt;.aab&lt;/code&gt; (the old &lt;code&gt;.apk&lt;/code&gt; is no longer accepted for new apps). On Expo, use EAS Build. On bare RN, generate a keystore and back it up to &lt;strong&gt;two locations&lt;/strong&gt; — losing it kills your ability to update.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Increment version + build numbers&lt;/strong&gt; — Each upload must bump &lt;code&gt;CFBundleVersion&lt;/code&gt; / &lt;code&gt;versionCode&lt;/code&gt;. A lower number is the #1 silent rejection.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Test on at least 5 device classes&lt;/strong&gt; — Old iPhone, current iPhone, budget Android, mid-range Android, tablet. Simulators lie about gestures, network, and performance.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Strip dev code&lt;/strong&gt; — &lt;code&gt;console.log&lt;/code&gt;, hidden gesture menus, hardcoded test users, &lt;code&gt;__DEV__&lt;/code&gt; branches. App Store reviewers actively search for "test" or "demo" strings.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Wire up crash reporting&lt;/strong&gt; — Sentry, Crashlytics, Bugsnag. Force a crash in a production build and verify it lands in your dashboard before you submit.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  B. App Store Metadata (6–10)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Lock app name + subtitle&lt;/strong&gt; — iOS: 30 + 30 chars. Google Play: 50 + 80 chars. Apple weighs the name field higher than anything else.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Write a real long description&lt;/strong&gt; — Lead with the value prop. Google Play shows first 80 chars on the card; iOS shows ~170 chars before the "more" tap. Don't waste those characters.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Pick primary + secondary category&lt;/strong&gt; — Most specific honest fit, not most popular.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Honest age/content rating&lt;/strong&gt; — Self-assessment for both stores. Lying about UGC or unfiltered web views is the most common second-round rejection.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;ASO keyword strategy&lt;/strong&gt; — iOS keyword field is 100 chars, comma-separated, never duplicate words from your name. Score keywords by &lt;code&gt;relevance × volume ÷ difficulty&lt;/code&gt;. Place top 40 only.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  C. Visual Assets (11–15)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;App icon 1024×1024 PNG, no alpha&lt;/strong&gt; — Test it at 60×60. If it's unreadable, the icon needs work.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Required screenshot sizes&lt;/strong&gt; —&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;iOS: 6.7-inch (1290×2796), 6.5-inch (1242×2688), iPad 12.9-inch if supported.&lt;/li&gt;
&lt;li&gt;Android: phone (1080×1920 or 1080×2340), 7" and 10" tablets.&lt;/li&gt;
&lt;li&gt;Frame each with a one-line value prop. Bare UI shots convert 20–40% worse.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;App preview video (15–30s)&lt;/strong&gt; — Silent by default on iOS. Show the outcome in the first 3 seconds, not the onboarding flow.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Splash screen = first screen&lt;/strong&gt; — Apple's guidance. Never gate it on a network call.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Google Play feature graphic (1024×500 PNG)&lt;/strong&gt; — No iOS equivalent. A blank one screams "amateur."&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  D. Privacy, Legal &amp;amp; Account (16–20)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Public privacy + support URLs&lt;/strong&gt; — Must load from anywhere, no auth wall. Use &lt;code&gt;/privacy&lt;/code&gt; and &lt;code&gt;/support&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;iOS Privacy Nutrition Label&lt;/strong&gt; — Declare every data type, including what your &lt;strong&gt;third-party SDKs&lt;/strong&gt; collect. Apple maintains an &lt;a href="https://developer.apple.com/support/third-party-SDK-requirements/" rel="noopener noreferrer"&gt;SDK list&lt;/a&gt; of trackers needing disclosure.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Google Play Data Safety form&lt;/strong&gt; — Mismatches with your real SDK behavior are caught silently weeks after launch. App gets unlisted.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;ATT prompt (if you use IDFA)&lt;/strong&gt; — Apple reviews your pre-permission "context primer." Generic copy like "Tap Allow" gets rejected. Explain &lt;em&gt;why&lt;/em&gt; the user benefits.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Closed test before production&lt;/strong&gt; — iOS: 50–200 TestFlight testers, 5–7 days. Android &lt;strong&gt;personal accounts&lt;/strong&gt;: 12 testers, 14 continuous days. This rule traps every solo founder. Plan 3 weeks, not 3 days. (&lt;a href="https://support.google.com/googleplay/android-developer/answer/14151465" rel="noopener noreferrer"&gt;Docs&lt;/a&gt;.)&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  E. Launch Day &amp;amp; Post-Launch (21–25)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Release strategy&lt;/strong&gt; — Manual (iOS) vs. automatic, plus phased rollout (iOS) or staged rollout (Android). Always phased for first release: 1% → 5% → 20% → 100% over a week.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Analytics + remote-config kill switch&lt;/strong&gt; — Mixpanel/Amplitude/PostHog/Firebase. Ship a remote flag that disables a broken feature without resubmitting. First time you use it you'll wonder why you didn't always.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Real release notes&lt;/strong&gt; — "Bug fixes and performance improvements" is malpractice. Three honest bullets. Users actually read these.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Day-one comms&lt;/strong&gt; — Product Hunt (Tue/Wed), LinkedIn post, X thread, demo video, email to pre-signups, Show HN if technical. Plan it before you submit, because approval arrives at 3 a.m.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Review-reply + crash-triage rotation&lt;/strong&gt; — Reply to every 1-star within 24h (other users read your reply). Daily 5-min crash triage in Sentry/Crashlytics for two weeks. Keep crash-free rate above 99.5%.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Where an AI app builder compresses the list
&lt;/h2&gt;

&lt;p&gt;Items 1, 4, 11, and 14 — production-clean code, signed bundles, framed visuals, splash screens — are the unglamorous infrastructure that consumes the last two weeks of a launch. If you generated your React Native + Expo app with &lt;a href="https://www.rapidnative.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=mobile-app-launch-checklist-before-publishing" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt;, the codebase ships clean by default (no leftover logs, no debug UI), and EAS Build picks it up first try.&lt;/p&gt;

&lt;p&gt;Items 16–20 (privacy + legal) and 21–25 (launch plan) are still on you. Always will be.&lt;/p&gt;

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

&lt;p&gt;Print the 25 items. Run them top-to-bottom. Two-week buffer between target launch and submission. Phased rollout for v1. Reply to every 1-star review. Ship it.&lt;/p&gt;

&lt;p&gt;What's the one item that bit you on a previous launch? Drop it in the comments — I'd bet it's #2 or #17.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>mobile</category>
      <category>expo</category>
      <category>ios</category>
    </item>
    <item>
      <title>Build a Fitness Wearable Companion App with React Native (HealthKit + Health Connect)</title>
      <dc:creator>Famitha M A</dc:creator>
      <pubDate>Thu, 14 May 2026 07:42:19 +0000</pubDate>
      <link>https://dev.to/famitha_ma_b9c13ab1d324e/build-a-fitness-wearable-companion-app-with-react-native-healthkit-health-connect-57en</link>
      <guid>https://dev.to/famitha_ma_b9c13ab1d324e/build-a-fitness-wearable-companion-app-with-react-native-healthkit-health-connect-57en</guid>
      <description>&lt;p&gt;You bought a smartwatch. Within a week, you stopped checking the watch face. The dashboard, the streaks, the weekly chart — all of that lives on the phone.&lt;/p&gt;

&lt;p&gt;That phone app is called a &lt;strong&gt;companion app&lt;/strong&gt;, and it's the most underrated piece of any fitness wearable product. The watch captures. The phone interprets, stores, and visualizes.&lt;/p&gt;

&lt;p&gt;This post is a code-first walkthrough of how to build one in React Native. We'll cover the architecture, HealthKit (iOS), Health Connect (Android), an optional Apple Watch bridge, and the sync model that doesn't fall over in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR for the impatient:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Read from &lt;code&gt;HealthKit&lt;/code&gt; on iOS and &lt;code&gt;Health Connect&lt;/code&gt; on Android — you cover 90% of wearables without writing firmware.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;react-native-health&lt;/code&gt; and &lt;code&gt;react-native-health-connect&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Make your backend &lt;code&gt;POST /samples&lt;/code&gt; idempotent on the HealthKit UUID. This single rule prevents most sync bugs.&lt;/li&gt;
&lt;li&gt;The dashboard UI is the time sink, not the integration. Plan accordingly.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Architecture (Internalize This Before Writing Code)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Wearable] → [OS Health Store] → [React Native bridge]
   → [Local DB (SQLite / WatermelonDB)]
   → [Sync queue] → [Backend]
   → [Other devices / Web dashboard]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things to internalize:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The OS health store is the source of truth for samples&lt;/strong&gt;, not your app. If the user reinstalls, HealthKit still has the data. Your local DB is a &lt;em&gt;cache&lt;/em&gt; for fast UI plus a write buffer for user-generated samples.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reads are one-directional, writes are bidirectional.&lt;/strong&gt; Reads pull from HealthKit/Health Connect. Writes hit &lt;em&gt;both&lt;/em&gt; the OS store and your backend.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Background delivery is a constraint, not a feature.&lt;/strong&gt; iOS will throttle aggressively. Design as if your app wakes every 15–30 minutes, not every second.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Pick Your Wearable Target First
&lt;/h2&gt;

&lt;p&gt;The biggest mistake I see: shipping for "wearables" generically. Each platform has a different data model and review hurdle.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Wearable&lt;/th&gt;
&lt;th&gt;Data path&lt;/th&gt;
&lt;th&gt;Library&lt;/th&gt;
&lt;th&gt;Watch-side code?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Apple Watch&lt;/td&gt;
&lt;td&gt;HealthKit&lt;/td&gt;
&lt;td&gt;&lt;code&gt;react-native-health&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Only for watchOS UI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pixel / Galaxy Watch&lt;/td&gt;
&lt;td&gt;Health Connect&lt;/td&gt;
&lt;td&gt;&lt;code&gt;react-native-health-connect&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Only for Wear OS tile&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fitbit&lt;/td&gt;
&lt;td&gt;Health Connect / Web API&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;react-native-health-connect&lt;/code&gt; + OAuth&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Garmin&lt;/td&gt;
&lt;td&gt;Garmin Health API&lt;/td&gt;
&lt;td&gt;OAuth + REST&lt;/td&gt;
&lt;td&gt;Optional (Connect IQ)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Whoop / Oura / Polar&lt;/td&gt;
&lt;td&gt;Vendor REST APIs&lt;/td&gt;
&lt;td&gt;OAuth + REST&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom BLE hardware&lt;/td&gt;
&lt;td&gt;Direct BLE&lt;/td&gt;
&lt;td&gt;&lt;code&gt;react-native-ble-plx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For most consumer apps, start with HealthKit + Health Connect. They cover every major retail wearable, the user already trusts the permission dialog, and you skip juggling six OAuth flows. Vendor SDKs are a v2 problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1 — Scaffold the Expo Project
&lt;/h2&gt;

&lt;p&gt;You need a custom dev client since both health libraries are native modules.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx create-expo-app fitness-companion &lt;span class="nt"&gt;--template&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;fitness-companion
npx expo &lt;span class="nb"&gt;install &lt;/span&gt;expo-dev-client
npm &lt;span class="nb"&gt;install &lt;/span&gt;react-native-health react-native-health-connect
npx expo prebuild
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In &lt;code&gt;app.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;"expo"&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;"ios"&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;"infoPlist"&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;"NSHealthShareUsageDescription"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Read your activity to power your dashboard."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"NSHealthUpdateUsageDescription"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Save workouts you log in the app."&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;"entitlements"&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;"com.apple.developer.healthkit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&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="nl"&gt;"android"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"permissions"&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="s2"&gt;"android.permission.health.READ_STEPS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"android.permission.health.READ_HEART_RATE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"android.permission.health.READ_SLEEP"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"android.permission.health.READ_EXERCISE"&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;h2&gt;
  
  
  Step 2 — Read Health Data on iOS (HealthKit)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;AppleHealthKit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;HealthKitPermissions&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-health&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;permissions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HealthKitPermissions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;read&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="nx"&gt;AppleHealthKit&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="nx"&gt;Permissions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Steps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;AppleHealthKit&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="nx"&gt;Permissions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;HeartRate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;AppleHealthKit&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="nx"&gt;Permissions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SleepAnalysis&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;AppleHealthKit&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="nx"&gt;Permissions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Workout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;AppleHealthKit&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="nx"&gt;Permissions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ActiveEnergyBurned&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;write&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;AppleHealthKit&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="nx"&gt;Permissions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Workout&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;AppleHealthKit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;initHealthKit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;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;HealthKit init failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fetch today's step count:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;startOfDay&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;Date&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;startOfDay&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setHours&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="mi"&gt;0&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;AppleHealthKit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getStepCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;startOfDay&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toISOString&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;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;setSteps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&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;h3&gt;
  
  
  Three production gotchas
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Aggregated queries can double-count.&lt;/strong&gt; If the user has both an Apple Watch and a third-party app writing steps, the default aggregate may count both. Use &lt;code&gt;getSamples&lt;/code&gt; with explicit source filtering when accuracy matters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;iOS won't tell you if read permission was denied.&lt;/strong&gt; &lt;code&gt;getStepCount&lt;/code&gt; just silently returns zero. Render an empty state, not an error.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;enableBackgroundDelivery&lt;/code&gt; needs &lt;code&gt;HKObserverQuery&lt;/code&gt;.&lt;/strong&gt; Budget half a day to wire it up properly the first time.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 3 — Read Health Data on Android (Health Connect)
&lt;/h2&gt;

&lt;p&gt;Health Connect replaced the Google Fit dev API in 2025. Pixel Watch and Galaxy Watch already write to it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;requestPermission&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;readRecords&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-native-health-connect&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;initialize&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;requestPermission&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;accessType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;read&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;recordType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Steps&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;accessType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;read&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;recordType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;HeartRate&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;accessType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;read&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;recordType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SleepSession&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;accessType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;read&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;recordType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ExerciseSession&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="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="nf"&gt;readRecords&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Steps&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;timeRangeFilter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;operator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;between&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;startTime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;startOfDay&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;endTime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;totalSteps&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;records&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;sum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;r&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;sum&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;count&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;Health Connect's permission UX is cleaner than HealthKit's (one consolidated screen), but you can only read the past 30 days unless the user explicitly grants extended history access.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4 — Apple Watch Bridge (Optional)
&lt;/h2&gt;

&lt;p&gt;If you want a watchOS app — say, a quick-start workout button — you need a watchOS target in Xcode bridged to RN via &lt;code&gt;WCSession&lt;/code&gt;. The watch app itself is Swift (RN doesn't run on watchOS).&lt;/p&gt;

&lt;p&gt;Minimal Swift bridge:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kt"&gt;WatchBridge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;NSObject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;WCSessionDelegate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;RCTBridgeModule&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;moduleName&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"WatchBridge"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;WCSession&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="n"&gt;didReceiveMessage&lt;/span&gt; &lt;span class="nv"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Any&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="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"event"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as?&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"workoutSample"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kt"&gt;RCTBridge&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;current&lt;/span&gt;&lt;span class="p"&gt;()?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eventDispatcher&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendAppEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nv"&gt;withName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"WatchWorkoutSample"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the RN side, subscribe with &lt;code&gt;NativeEventEmitter&lt;/code&gt; and update your store. A reasonable v1 scope: start/end workout from the watch face, with samples flowing back live. Anything more (full watch UI, complications) is multi-week work.&lt;/p&gt;

&lt;p&gt;For Wear OS, the equivalent is the Data Layer API with &lt;code&gt;MessageClient&lt;/code&gt;. Same pattern: native module, JS bridge, event emitter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5 — The Dashboard UI (Where Time Actually Goes)
&lt;/h2&gt;

&lt;p&gt;Data flows. Now you need the actual app. Standard companion-app screens:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Today&lt;/strong&gt; — steps, heart-rate ring, active calories, latest workout&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trends&lt;/strong&gt; — weekly/monthly bar charts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workouts&lt;/strong&gt; — list of &lt;code&gt;ExerciseSession&lt;/code&gt; / &lt;code&gt;Workout&lt;/code&gt; records with detail view&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Goals&lt;/strong&gt; — editable daily targets, local + synced&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Settings&lt;/strong&gt; — permissions, units, connected devices&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stack I'd recommend:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Charts:&lt;/strong&gt; &lt;code&gt;victory-native&lt;/code&gt; (maintained, gesture support, works on both platforms). Skip &lt;code&gt;react-native-svg-charts&lt;/code&gt; — unmaintained.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;List:&lt;/strong&gt; virtualize aggressively. Users routinely have thousands of historical workouts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State:&lt;/strong&gt; Zustand or Redux Toolkit. Avoid Context for high-frequency updates from health observers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Honest aside: this is the part of the build that's the least interesting and the most time-consuming. The integration code lands in week one. The dashboard eats the next two-to-three weeks if you write every screen by hand.&lt;/p&gt;

&lt;p&gt;The third time I built one of these, I described the spec in plain English to an AI mobile app builder and got a full Expo project — components, navigation, theming — in under an hour, then plugged my HealthKit hooks into it. The tool I used was &lt;a href="https://www.rapidnative.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=build-fitness-wearable-companion-app-react-native" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt;, which generates real React Native + Expo code (not a proprietary runtime — you own the output). Worth a look if the dashboard is the part you'd rather not write by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6 — Sync, Offline, Conflicts
&lt;/h2&gt;

&lt;p&gt;The pattern that survives production:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Local-first writes.&lt;/strong&gt; WatermelonDB or &lt;code&gt;expo-sqlite&lt;/code&gt;. Every user-logged workout → local DB → HealthKit/Health Connect → upload queue.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pull-based reads from the health store.&lt;/strong&gt; On app open and on background fetch (~30 min cadence on iOS), pull deltas since &lt;code&gt;last_sync_timestamp&lt;/code&gt;, dedupe by UUID, upsert.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Idempotent uploads.&lt;/strong&gt; Every HealthKit sample has a stable UUID. Make &lt;code&gt;POST /samples&lt;/code&gt; a no-op on duplicate UUIDs:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Backend pseudocode&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;ingestSample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sample&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;samples&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upsert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;healthkit_uuid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uuid&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;update&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;modified_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;endDate&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;create&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;sample&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;healthkit_uuid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uuid&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;This single rule prevents 90% of the sync bugs that haunt fitness apps.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;No cellular uploads by default.&lt;/strong&gt; Gate behind a setting. Battery-sensitive users uninstall fast.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Edge case that bites everyone:&lt;/strong&gt; a user edits a workout in the Apple Health app. HealthKit emits an "updated" sample with the &lt;em&gt;same UUID&lt;/em&gt; but a new modification date. Your sync code must handle this — most don't, which is why so many fitness apps quietly drift out of sync.&lt;/p&gt;

&lt;h2&gt;
  
  
  Realistic Timeline
&lt;/h2&gt;

&lt;p&gt;For a single full-stack dev who's shipped React Native before:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Phase&lt;/th&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Setup, permissions, HealthKit + Health Connect reads&lt;/td&gt;
&lt;td&gt;3–5 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dashboard UI (5 screens, charts, theming)&lt;/td&gt;
&lt;td&gt;5–8 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Local DB + backend sync&lt;/td&gt;
&lt;td&gt;4–6 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Apple Watch companion (basic workout start/stop)&lt;/td&gt;
&lt;td&gt;5–7 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Polish + App Store review prep&lt;/td&gt;
&lt;td&gt;5–10 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;v1 ship&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3–5 weeks&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The dashboard phase is the easiest to compress because it has nothing to do with wearables. Scaffolding it with an AI tool can take that week down to a day.&lt;/p&gt;

&lt;h2&gt;
  
  
  App Store Gotchas That Kill Submissions
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;PrivacyInfo.xcprivacy&lt;/code&gt; is mandatory&lt;/strong&gt; for any iOS app touching HealthKit (since 2024). Missing it = auto-reject.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HealthKit data cannot be used for advertising or sold.&lt;/strong&gt; Period.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Health Connect requires a published privacy policy URL&lt;/strong&gt; in your Play Store listing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You cannot transmit another user's HealthKit data.&lt;/strong&gt; "Share with a friend" features need each recipient's own HealthKit permission grants.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;strong&gt;Can I use Expo for this?&lt;/strong&gt;&lt;br&gt;
Yes, with a custom dev client. &lt;code&gt;npx expo prebuild&lt;/code&gt; then &lt;code&gt;eas build --profile development&lt;/code&gt;. Expo Go won't work because of the native modules.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I have to ship a watchOS app to support Apple Watch?&lt;/strong&gt;&lt;br&gt;
No. The watch already writes to HealthKit. Read from HealthKit and you support every Apple Watch user without writing Swift. A watchOS app is only for on-wrist UI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Google Fit vs Health Connect?&lt;/strong&gt;&lt;br&gt;
Health Connect replaced Google Fit's dev API in 2025. New apps should use &lt;code&gt;react-native-health-connect&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can React Native do live heart-rate streaming?&lt;/strong&gt;&lt;br&gt;
Not via HealthKit (samples arrive on a delay). For real-time streams, either a watchOS app over &lt;code&gt;WCSession&lt;/code&gt; or a direct BLE connection via &lt;code&gt;react-native-ble-plx&lt;/code&gt;.&lt;/p&gt;




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

&lt;p&gt;The integration work — HealthKit, Health Connect, WCSession — is irreducible. You write it yourself. The UI on top is boilerplate you've seen in fifty other apps, and it's where most of the calendar disappears.&lt;/p&gt;

&lt;p&gt;Start with the system health stores. Make your sync endpoint idempotent. Don't ship a watchOS app until you've validated the phone experience.&lt;/p&gt;

&lt;p&gt;If this was useful, drop a comment with what you're building — happy to dig into specific edge cases (sleep data is a rabbit hole, ask me how I know).&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Cover photo by Onur Binay on Unsplash.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>healthkit</category>
      <category>mobile</category>
    </item>
    <item>
      <title>The Complete Guide to React Native Debugging in 2026</title>
      <dc:creator>Famitha M A</dc:creator>
      <pubDate>Tue, 12 May 2026 06:56:39 +0000</pubDate>
      <link>https://dev.to/famitha_ma_b9c13ab1d324e/the-complete-guide-to-react-native-debugging-in-2026-49oi</link>
      <guid>https://dev.to/famitha_ma_b9c13ab1d324e/the-complete-guide-to-react-native-debugging-in-2026-49oi</guid>
      <description>&lt;h1&gt;
  
  
  The Complete Guide to React Native Debugging in 2026
&lt;/h1&gt;

&lt;p&gt;If you learned React Native debugging before 2025, almost everything you knew is now obsolete. Flipper is dead. The remote JS debugger is gone. And as of React Conf 2025, the New Architecture is no longer optional — it's the only architecture.&lt;/p&gt;

&lt;p&gt;This post is what I actually use to debug React Native in 2026, after migrating a handful of older codebases and starting new ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR — the 2026 stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;JS debugging&lt;/strong&gt;: React Native DevTools (press &lt;code&gt;j&lt;/code&gt; in Metro). It's built in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State / storage&lt;/strong&gt;: Reactotron (Redux, Zustand, MMKV).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Native&lt;/strong&gt;: Xcode or Android Studio directly. No middleware.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Production&lt;/strong&gt;: Sentry / Bugsnag / Crashlytics with source maps.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Four tools. Clear jobs. Compare to the 2022 chaos of Flipper plugins + standalone React Native Debugger + Chrome DevTools + Reactotron and you'll appreciate the simplification.&lt;/p&gt;

&lt;h2&gt;
  
  
  React Native DevTools, the new default
&lt;/h2&gt;

&lt;p&gt;Ships built-in with RN 0.76+. In the Metro terminal, press &lt;code&gt;j&lt;/code&gt;. A Chrome window attaches to your Hermes runtime through the Chrome DevTools Protocol. No Bridge, no proxy.&lt;/p&gt;

&lt;p&gt;Five panels matter:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Console&lt;/strong&gt; — logs, warnings, React component stack traces.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sources&lt;/strong&gt; — breakpoints, step-through, source map support.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network&lt;/strong&gt; — first-class HTTP inspection (added in RN 0.83).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance&lt;/strong&gt; — flame graphs, JS thread timings, commit phases (RN 0.83).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;React Components / Profiler&lt;/strong&gt; — same panels you know from web.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What it doesn't do: native module inspection (use Xcode/Android Studio) and live UI editing of native views.&lt;/p&gt;

&lt;h2&gt;
  
  
  Source maps in production
&lt;/h2&gt;

&lt;p&gt;The single most underrated debugging setup. Without them, every production crash looks like &lt;code&gt;0xb73f4: anonymous (index.android.bundle:1:284731)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For Sentry, one command does the wiring:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @sentry/wizard@latest &lt;span class="nt"&gt;-i&lt;/span&gt; reactNative &lt;span class="nt"&gt;-p&lt;/span&gt; ios android
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It adds a build phase to Xcode and a Gradle task to Android that uploads source maps on every release build. Skip this and debugging production-only crashes becomes punishment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reactotron for state
&lt;/h2&gt;

&lt;p&gt;React Native DevTools doesn't know what Redux is. Reactotron does.&lt;/p&gt;

&lt;p&gt;Install the desktop app, add &lt;code&gt;reactotron-react-native&lt;/code&gt; + your state plugin, wire it up in a dev-only config file, and you get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Live timeline of every Redux/Zustand action with diffs&lt;/li&gt;
&lt;li&gt;MMKV and AsyncStorage inspection&lt;/li&gt;
&lt;li&gt;Custom commands fireable from the Reactotron UI ("log in as test user")&lt;/li&gt;
&lt;li&gt;Image overlay mode for designer-developer handoff&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Infinite Red kept this alive after Flipper died. Still actively maintained in 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  Native debugging without middleware
&lt;/h2&gt;

&lt;p&gt;When the bug isn't in JS, you need native tools.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;iOS&lt;/strong&gt;: open &lt;code&gt;ios/YourApp.xcworkspace&lt;/code&gt; in Xcode. Run from Xcode (not Metro). Native breakpoints, &lt;code&gt;NSLog&lt;/code&gt; output, Instruments, View Hierarchy Debugger.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Android&lt;/strong&gt;: open &lt;code&gt;android/&lt;/code&gt; in Android Studio. Logcat filtered by your package name + the Layout Inspector. The Layout Inspector is critical for Fabric layout bugs that look fine in React DevTools but render wrong on device.&lt;/p&gt;

&lt;p&gt;A 2026 rule of thumb: if a bug reproduces in &lt;code&gt;__DEV__&lt;/code&gt; but disappears in release builds, it's almost always a native module config issue. Skip React Native DevTools, go straight to Xcode/Android Studio.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance profiling in the New Architecture
&lt;/h2&gt;

&lt;p&gt;The Bridge is gone. JSI exposes native objects as JS references. Fabric renders synchronously where it can. Turbo Modules load lazily.&lt;/p&gt;

&lt;p&gt;What changes for debugging:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Too many bridge calls" is no longer a thing.&lt;/li&gt;
&lt;li&gt;Frame drops have clearer causes: slow JS commit (React Profiler) or native module blocking UI thread (Performance flame graphs).&lt;/li&gt;
&lt;li&gt;Bridgeless mode is the default in every new project.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Profile flow: DevTools → Performance → Record → interact → stop. Hunt for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JS tasks over 50ms (hot paths — &lt;code&gt;useMemo&lt;/code&gt;, &lt;code&gt;useCallback&lt;/code&gt;, &lt;code&gt;React.memo&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Frequent re-renders (use Profiler's "Highlight updates")&lt;/li&gt;
&lt;li&gt;Heavy commits on navigation (usually &lt;code&gt;useEffect&lt;/code&gt;-on-mount doing too much)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Common bugs → right tool
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;First tool&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Red screen JS error&lt;/td&gt;
&lt;td&gt;DevTools → Console&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1-3 second freeze&lt;/td&gt;
&lt;td&gt;DevTools → Performance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;200 response but no UI update&lt;/td&gt;
&lt;td&gt;Network + Reactotron&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Production-only crash&lt;/td&gt;
&lt;td&gt;Sentry → Issues&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Android layout broken, iOS fine&lt;/td&gt;
&lt;td&gt;Android Studio Layout Inspector&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Image not loading&lt;/td&gt;
&lt;td&gt;Xcode/Android Studio console&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Unable to resolve module"&lt;/td&gt;
&lt;td&gt;&lt;code&gt;npx react-native start --reset-cache&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  "Works on simulator, broken on device"
&lt;/h3&gt;

&lt;p&gt;Three buckets:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Permissions&lt;/strong&gt; — simulator auto-grants, device doesn't. Check &lt;code&gt;Info.plist&lt;/code&gt; / &lt;code&gt;AndroidManifest.xml&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;localhost&lt;/strong&gt; — on device, &lt;code&gt;localhost&lt;/code&gt; is the device. Use LAN IP or ngrok.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Signing / bundle ID&lt;/strong&gt; — release builds hit signing rules dev builds don't.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Final notes
&lt;/h2&gt;

&lt;p&gt;The 2026 React Native debugging stack is the leanest it has been in years. One built-in debugger, one state inspector, native tools when you need them, a crash reporter for production.&lt;/p&gt;

&lt;p&gt;If you start a new project, you only need to install Reactotron and your crash reporter SDK — everything else ships by default. And if you're using an AI builder that outputs Expo apps (full disclosure: I work on &lt;a href="https://www.rapidnative.com/?utm_source=devto&amp;amp;utm_medium=content&amp;amp;utm_campaign=react-native-debugging-complete-guide-2026" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt;, an AI mobile app builder), every tool above works on the exported code without modification — there's no proprietary runtime to learn.&lt;/p&gt;

&lt;p&gt;What's in your 2026 React Native debugging stack? Curious what people are using for production observability.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>javascript</category>
      <category>mobile</category>
      <category>webdev</category>
    </item>
    <item>
      <title>React Native i18n: A Practical Guide to Multi-Language Mobile Apps</title>
      <dc:creator>Famitha M A</dc:creator>
      <pubDate>Fri, 08 May 2026 10:12:05 +0000</pubDate>
      <link>https://dev.to/famitha_ma_b9c13ab1d324e/react-native-i18n-a-practical-guide-to-multi-language-mobile-apps-1jl9</link>
      <guid>https://dev.to/famitha_ma_b9c13ab1d324e/react-native-i18n-a-practical-guide-to-multi-language-mobile-apps-1jl9</guid>
      <description>&lt;h1&gt;
  
  
  React Native i18n: A Practical Guide to Multi-Language Mobile Apps
&lt;/h1&gt;

&lt;p&gt;Most i18n bugs in React Native apps trace back to two decisions made early: hardcoding strings, and using string concatenation to build sentences. Both feel fine when you're shipping in English. Both are catastrophic the day you add a second locale.&lt;/p&gt;

&lt;p&gt;This is the practical playbook I wish I had when I first added Spanish, Japanese, and Arabic to a React Native app: which library to pick, the patterns that scale, and the traps that only show up in production.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;i18next&lt;/code&gt; + &lt;code&gt;react-i18next&lt;/code&gt; + &lt;code&gt;expo-localization&lt;/code&gt; (or &lt;code&gt;react-native-localize&lt;/code&gt; on bare RN).&lt;/li&gt;
&lt;li&gt;Never hardcode strings, never concatenate to build sentences.&lt;/li&gt;
&lt;li&gt;Use ICU pluralization — &lt;code&gt;count === 1 ? 'a' : 'b'&lt;/code&gt; is wrong in most languages.&lt;/li&gt;
&lt;li&gt;Plan for RTL from day one with &lt;code&gt;marginStart&lt;/code&gt;/&lt;code&gt;marginEnd&lt;/code&gt; instead of left/right.&lt;/li&gt;
&lt;li&gt;Use namespaces (&lt;code&gt;auth.json&lt;/code&gt;, &lt;code&gt;home.json&lt;/code&gt;) once you cross ~500 keys.&lt;/li&gt;
&lt;li&gt;Format dates/numbers/currencies with the &lt;code&gt;Intl&lt;/code&gt; API.&lt;/li&gt;
&lt;li&gt;Hook up a translation management service (Crowdin, Lokalise, Locize) before you ship to a third locale.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why i18n Is an Architectural Decision
&lt;/h2&gt;

&lt;p&gt;Retrofitting translations into a codebase that hardcodes strings everywhere is one of the most painful refactors in mobile development. For a 50-screen app, expect 1-3 weeks of dedicated work to extract strings, set up a translation library, and validate every screen.&lt;/p&gt;

&lt;p&gt;The discipline of always using &lt;code&gt;t('home.welcome')&lt;/code&gt; instead of &lt;code&gt;"Welcome"&lt;/code&gt; from day one means you never have a "what strings did we miss?" problem when you add a second language. It also doubles as a code review smell test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Picking a Library
&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;Best For&lt;/th&gt;
&lt;th&gt;Bundle Size&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;i18next&lt;/code&gt; + &lt;code&gt;react-i18next&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Most apps&lt;/td&gt;
&lt;td&gt;~50KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;react-intl&lt;/code&gt; (FormatJS)&lt;/td&gt;
&lt;td&gt;ICU MessageFormat purists&lt;/td&gt;
&lt;td&gt;~80KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LinguiJS&lt;/td&gt;
&lt;td&gt;Type-safe, compile-time extraction&lt;/td&gt;
&lt;td&gt;~20KB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For 95% of React Native projects, default to &lt;code&gt;i18next&lt;/code&gt; unless you have a specific reason not to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Minimal Setup (Expo)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// i18n.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;i18n&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;i18next&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;initReactI18next&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-i18next&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Localization&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-localization&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;en&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;./locales/en/common.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;es&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;./locales/es/common.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;i18n&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;initReactI18next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;en&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;common&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;en&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;es&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;common&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;es&lt;/span&gt; &lt;span class="p"&gt;},&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;Localization&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;fallbackLng&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;defaultNS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;common&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;interpolation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;escapeValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;compatibilityJSON&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;v3&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;i18n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// App.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./i18n&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;useTranslation&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-i18next&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;App&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useTranslation&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Text&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;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;greeting&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Maria&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;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Text&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  7 Patterns That Scale
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Use interpolation, not concatenation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Bad&lt;/span&gt;
&lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hello&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&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="c1"&gt;// Good&lt;/span&gt;
&lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;greeting&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="c1"&gt;// en.json: { "greeting": "Hello, {{name}}!" }&lt;/span&gt;
&lt;span class="c1"&gt;// ja.json: { "greeting": "{{name}}さん、こんにちは!" }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Use ICU pluralization
&lt;/h3&gt;

&lt;p&gt;English has 2 plural forms, Russian has 3, Arabic has 6. Don't roll your own.&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;"items_one"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{{count}} item"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"items_other"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{{count}} items"&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;h3&gt;
  
  
  3. Plan for RTL from day one
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;marginStart&lt;/code&gt;/&lt;code&gt;marginEnd&lt;/code&gt; instead of left/right.&lt;/li&gt;
&lt;li&gt;Mirror directional icons: &lt;code&gt;transform: [{ scaleX: I18nManager.isRTL ? -1 : 1 }]&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Switching between LTR/RTL requires an app restart — design your language switcher UX accordingly.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Namespace your translations
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;locales/en/
├── common.json
├── auth.json
├── home.json
└── errors.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lazy-load namespaces per screen to reduce startup load on lower-end Android.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Format with the Intl API
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Intl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DateTimeFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i18n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;language&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;dateStyle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;long&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Intl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;NumberFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i18n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;language&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;currency&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;USD&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hermes supports &lt;code&gt;Intl&lt;/code&gt; natively as of RN 0.71+. If you're older, use the &lt;code&gt;formatjs&lt;/code&gt; polyfill — but upgrade Hermes if you can.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Set up a translation pipeline
&lt;/h3&gt;

&lt;p&gt;Hook your &lt;code&gt;locales/&lt;/code&gt; folder to Crowdin, Lokalise, or Locize before you ship to a third language. CI should fail the build on missing keys in production locales.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Test with pseudo-localization
&lt;/h3&gt;

&lt;p&gt;Use a "fake locale" that wraps every string in brackets and adds 30% length. Catches truncation, hardcoded strings, and layout breakage before real translators see anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  How RapidNative Handles This
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.rapidnative.com/?utm_source=blog&amp;amp;utm_medium=content&amp;amp;utm_campaign=react-native-i18n-best-practices" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt; is an AI app builder that generates React Native + Expo code from natural-language prompts. The generated components use translation keys (&lt;code&gt;t('auth.sign_in_button')&lt;/code&gt;) instead of hardcoded strings, and the translation JSON is scaffolded alongside the code — so adding a new locale is dropping in a translated JSON file, not refactoring imports.&lt;/p&gt;

&lt;p&gt;If you're starting a new project and don't want to build the i18n scaffolding by hand, that's a faster path to "i18n-ready by default."&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;Multi-language support gets dramatically more expensive over time. Build it in from day one — translation keys, ICU plurals, RTL-aware layouts, real translation pipeline — and your app can ship to any market without a future refactor.&lt;/p&gt;

&lt;p&gt;What pattern do you wish you'd known earlier? Drop it in the comments — always curious how other teams handle the RTL transition specifically.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>javascript</category>
      <category>mobile</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Add Payment Processing to Your React Native App in 2026</title>
      <dc:creator>Famitha M A</dc:creator>
      <pubDate>Thu, 07 May 2026 11:15:16 +0000</pubDate>
      <link>https://dev.to/famitha_ma_b9c13ab1d324e/how-to-add-payment-processing-to-your-react-native-app-in-2026-1o83</link>
      <guid>https://dev.to/famitha_ma_b9c13ab1d324e/how-to-add-payment-processing-to-your-react-native-app-in-2026-1o83</guid>
      <description>&lt;h1&gt;
  
  
  How to Add Payment Processing to Your React Native App in 2026
&lt;/h1&gt;

&lt;p&gt;Adding payments to a React Native app sounds simple until you realize there's no single "right way." If you sell physical goods, Stripe is the answer. If you sell digital content consumed inside your app, &lt;strong&gt;Apple and Google require their own billing APIs&lt;/strong&gt; and they take up to 30%. Mix the two and you'll either pay double fees or get rejected during App Store review.&lt;/p&gt;

&lt;p&gt;This is the practical guide I wish I'd had the first time. We'll cover the decision tree, set up &lt;code&gt;@stripe/stripe-react-native&lt;/code&gt;, add Apple Pay and Google Pay, handle subscriptions with &lt;code&gt;react-native-iap&lt;/code&gt; or RevenueCat, build a minimal backend, and test it all without spending a dollar.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Decision Tree
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;You sell...&lt;/th&gt;
&lt;th&gt;Use this&lt;/th&gt;
&lt;th&gt;Fee&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Physical goods&lt;/td&gt;
&lt;td&gt;Stripe / Braintree&lt;/td&gt;
&lt;td&gt;~2.9% + $0.30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Real-world services (rides, etc.)&lt;/td&gt;
&lt;td&gt;Stripe / Braintree&lt;/td&gt;
&lt;td&gt;~2.9% + $0.30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Digital subscriptions&lt;/td&gt;
&lt;td&gt;Apple IAP / Google Play Billing&lt;/td&gt;
&lt;td&gt;15–30%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One-time digital unlocks&lt;/td&gt;
&lt;td&gt;Apple IAP / Google Play Billing&lt;/td&gt;
&lt;td&gt;15–30%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Apple's &lt;a href="https://developer.apple.com/app-store/review/guidelines/#payments" rel="noopener noreferrer"&gt;Guideline 3.1.1&lt;/a&gt; is the source of truth here. Don't skip it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Install the Stripe SDK
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @stripe/stripe-react-native
&lt;span class="nb"&gt;cd &lt;/span&gt;ios &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; pod &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expo SDK 50+ works via the &lt;a href="https://docs.expo.dev/versions/latest/sdk/stripe/" rel="noopener noreferrer"&gt;config plugin&lt;/a&gt;. You need a development build — Expo Go can't run native payment modules.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Wrap Your App in StripeProvider
&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;StripeProvider&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;@stripe/stripe-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="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;App&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;StripeProvider&lt;/span&gt;
      &lt;span class="na"&gt;publishableKey&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;EXPO_PUBLIC_STRIPE_PUBLISHABLE_KEY&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;merchantIdentifier&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"merchant.com.yourapp"&lt;/span&gt;
      &lt;span class="na"&gt;urlScheme&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"yourapp"&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;RootNavigator&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;StripeProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only the publishable key (&lt;code&gt;pk_...&lt;/code&gt;) goes in the client. Your secret key never touches the bundle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Create a PaymentIntent on Your Backend
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/payments/create-intent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="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;intent&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;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;paymentIntents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&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="na"&gt;automatic_payment_methods&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;enabled&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="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;clientSecret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;client_secret&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;You can't create PaymentIntents from the client. Stripe blocks it because it would let an attacker manipulate the amount.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Present PaymentSheet
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;initPaymentSheet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;presentPaymentSheet&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useStripe&lt;/span&gt;&lt;span class="p"&gt;();&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;checkout&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;clientSecret&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createIntent&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="mi"&gt;1999&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;initPaymentSheet&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;merchantDisplayName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Your Store&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;paymentIntentClientSecret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;clientSecret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;applePay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;merchantCountryCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;US&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;googlePay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;merchantCountryCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;US&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;testEnv&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;presentPaymentSheet&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;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;showReceipt&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;PaymentSheet picks the right local methods automatically — cards in the US, iDEAL in the Netherlands, BLIK in Poland. You don't build any of that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Apple Pay &amp;amp; Google Pay
&lt;/h2&gt;

&lt;p&gt;Both flow &lt;strong&gt;through&lt;/strong&gt; Stripe, they're not separate gateways. To enable Apple Pay:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a Merchant ID in the Apple Developer portal.&lt;/li&gt;
&lt;li&gt;Generate the processing certificate from Stripe's dashboard.&lt;/li&gt;
&lt;li&gt;Add the &lt;code&gt;Apple Pay&lt;/code&gt; capability in Xcode.&lt;/li&gt;
&lt;li&gt;Pass &lt;code&gt;merchantIdentifier&lt;/code&gt; to &lt;code&gt;StripeProvider&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Google Pay is one config flag plus an &lt;code&gt;AndroidManifest.xml&lt;/code&gt; meta-data tag. Conversion lifts 20–30% on iOS when Apple Pay is offered.&lt;/p&gt;

&lt;h2&gt;
  
  
  Subscriptions: react-native-iap or RevenueCat?
&lt;/h2&gt;

&lt;p&gt;For digital content, you have to use Apple/Google IAP. Two practical paths:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;react-native-iap&lt;/code&gt;&lt;/strong&gt; (free, DIY backend):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;initConnection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;requestPurchase&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;finishTransaction&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-iap&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;initConnection&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;purchase&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;requestPurchase&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pro_monthly&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;// validate receipt on your backend, then:&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;finishTransaction&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;purchase&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;isConsumable&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.revenuecat.com/docs/getting-started/installation/reactnative" rel="noopener noreferrer"&gt;RevenueCat&lt;/a&gt;&lt;/strong&gt; (free under ~$2.5K MTR, then 1% — handles all the receipt validation, entitlement, and analytics):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Purchases&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-purchases&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;Purchases&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configure&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;your_key&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;offerings&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;Purchases&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getOfferings&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;customerInfo&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Purchases&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;purchasePackage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;offerings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;monthly&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;isPro&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;customerInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;entitlements&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;active&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pro&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Rule of thumb:&lt;/strong&gt; if you ship subscriptions and don't have a payments engineer, use RevenueCat. The hours you save on receipt validation, server-to-server notifications, and cross-platform entitlement state will pay for it in week one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing Without Real Money
&lt;/h2&gt;

&lt;p&gt;Stripe test cards I use most:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;4242 4242 4242 4242&lt;/code&gt; — success&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;4000 0027 6000 3184&lt;/code&gt; — 3D Secure challenge&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;4000 0000 0000 9995&lt;/code&gt; — declined (insufficient funds)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;4000 0000 0000 0341&lt;/code&gt; — fails on charge&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Apple sandbox subscriptions renew at accelerated cadence (1 month = 5 minutes, up to 6 cycles). You can verify your renewal logic in an afternoon.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pre-Ship Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Successful card charge end-to-end&lt;/li&gt;
&lt;li&gt;[ ] 3D Secure / SCA challenge completes&lt;/li&gt;
&lt;li&gt;[ ] Apple Pay / Google Pay works&lt;/li&gt;
&lt;li&gt;[ ] IAP purchase + restore + refund tested&lt;/li&gt;
&lt;li&gt;[ ] Webhook idempotency (duplicate events don't double-grant)&lt;/li&gt;
&lt;li&gt;[ ] Subscription cancellation eventually revokes access&lt;/li&gt;
&lt;li&gt;[ ] Network drop mid-purchase doesn't lose the receipt&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;PCI scope creep&lt;/strong&gt; — stay in SAQ A by always using PaymentSheet. The moment you build a custom card form, the audit burden explodes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SCA failures in Europe&lt;/strong&gt; — let Stripe pick payment methods automatically; custom flows often skip the 3DS challenge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Idempotency keys&lt;/strong&gt; — pass one on every server write. Retries that create duplicate intents charge the user twice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Currency mismatch&lt;/strong&gt; — store currency next to amount. &lt;code&gt;1999&lt;/code&gt; is not the same revenue in USD vs JPY.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;Payments will always require thoughtful backend and compliance work. But the React Native scaffolding around them — paywalls, settings screens, restore-purchase flows — is repetitive plumbing.&lt;/p&gt;

&lt;p&gt;If you want to skip the boilerplate, &lt;a href="https://www.rapidnative.com/?utm_source=devto&amp;amp;utm_medium=content&amp;amp;utm_campaign=how-to-add-payment-processing-to-your-react-native-app" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt; generates production-ready React Native + Expo code from a sentence. Describe your paywall, iterate visually, drop in your Stripe and RevenueCat keys, and ship.&lt;/p&gt;

&lt;p&gt;What's your stack — Stripe + RevenueCat? Pure react-native-iap? Curious what's working in 2026.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>javascript</category>
      <category>mobile</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
