<?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.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 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>
    <item>
      <title>React Native Testing in 2026: Jest, Detox, and Maestro Compared</title>
      <dc:creator>Famitha M A</dc:creator>
      <pubDate>Wed, 06 May 2026 13:58:39 +0000</pubDate>
      <link>https://dev.to/famitha_ma_b9c13ab1d324e/react-native-testing-in-2026-jest-detox-and-maestro-compared-45cg</link>
      <guid>https://dev.to/famitha_ma_b9c13ab1d324e/react-native-testing-in-2026-jest-detox-and-maestro-compared-45cg</guid>
      <description>&lt;p&gt;The React Native testing consensus has fractured. For years it was "Jest for units, Detox for E2E, that's the answer." In 2026, Maestro has crossed 10,000 GitHub stars, Detox holds at 11,800, and most production teams now run a layered combination of all three.&lt;/p&gt;

&lt;p&gt;Here's a practical, opinionated breakdown of when to use each — based on actually shipping React Native apps, not just reading the docs.&lt;/p&gt;

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;What it's for&lt;/th&gt;
&lt;th&gt;Setup time&lt;/th&gt;
&lt;th&gt;Flakiness&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Jest&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unit + component tests&lt;/td&gt;
&lt;td&gt;Built-in&lt;/td&gt;
&lt;td&gt;&amp;lt;0.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Detox&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Gray-box E2E with native synchronization&lt;/td&gt;
&lt;td&gt;1-3 days&lt;/td&gt;
&lt;td&gt;&amp;lt;2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Maestro&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Black-box E2E with YAML&lt;/td&gt;
&lt;td&gt;15 minutes&lt;/td&gt;
&lt;td&gt;&amp;lt;1%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you only read this far: &lt;strong&gt;start with Jest + Maestro&lt;/strong&gt;. Add Detox only when you feel the specific pain it solves.&lt;/p&gt;

&lt;h2&gt;
  
  
  Jest: the layer everyone keeps
&lt;/h2&gt;

&lt;p&gt;Jest plus React Native Testing Library is the universally accepted baseline. Jest 30 (late 2025) brought meaningful performance improvements, and RNTL 12.x now plays nicely with the New Architecture.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Pure logic (reducers, hooks, utils)&lt;/li&gt;
&lt;li&gt;Component render tests with &lt;code&gt;testID&lt;/code&gt; and accessibility queries&lt;/li&gt;
&lt;li&gt;Mocking native modules without touching a simulator&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The ceiling is anything involving the actual rendering pipeline, real native modules, or multi-screen flows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Detox: powerful, but you'll feel the setup
&lt;/h2&gt;

&lt;p&gt;Detox's superpower is gray-box synchronization — it hooks into your app's internals and waits for the JavaScript thread, network calls, animations, and timers to genuinely idle before firing the next action. That's how Wix runs thousands of Detox tests with sub-2% flakiness.&lt;/p&gt;

&lt;p&gt;The catch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# What setting up Detox actually involves&lt;/span&gt;
1. Install detox CLI + detox-cli global
2. Add detox config to package.json &lt;span class="o"&gt;(&lt;/span&gt;testRunner, apps, devices, configurations&lt;span class="o"&gt;)&lt;/span&gt;
3. Create separate Detox build &lt;span class="k"&gt;for &lt;/span&gt;iOS &lt;span class="o"&gt;(&lt;/span&gt;Podfile, AppDelegate patches&lt;span class="o"&gt;)&lt;/span&gt;
4. Same &lt;span class="k"&gt;for &lt;/span&gt;Android &lt;span class="o"&gt;(&lt;/span&gt;build.gradle changes&lt;span class="o"&gt;)&lt;/span&gt;
5. Wire up Jest-based &lt;span class="nb"&gt;test &lt;/span&gt;runner with Detox globals
6. Configure CI with macOS &lt;span class="k"&gt;for &lt;/span&gt;iOS + Android emulator hardware accel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Worth it for native-heavy apps with a dedicated platform team. Painful for two-person teams.&lt;/p&gt;

&lt;h2&gt;
  
  
  Maestro: the tool indie devs keep choosing
&lt;/h2&gt;

&lt;p&gt;A complete login E2E test in Maestro:&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="na"&gt;appId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;com.mycompany.myapp&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;launchApp&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;tapOn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Email"&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;inputText&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;test@example.com"&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;tapOn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Password"&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;inputText&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;supersecret"&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;tapOn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Log&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;in"&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;assertVisible&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Welcome&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;back"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the entire file. One CLI install, point at your built app, &lt;code&gt;maestro test login.yaml&lt;/code&gt;. Done.&lt;/p&gt;

&lt;p&gt;Why teams are picking it up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cross-platform by default — same YAML runs on iOS and Android&lt;/li&gt;
&lt;li&gt;Built-in flakiness tolerance (auto-retries, waits for content)&lt;/li&gt;
&lt;li&gt;Maestro Studio for record-and-playback&lt;/li&gt;
&lt;li&gt;Maestro Cloud for parallel real-device runs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The trade-off: it's not a great fit for "verify this specific selector returns the right value when the network is offline." That's still Jest's job.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical 2026 stack
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Jest                  → every commit, runs in seconds
React Native TL       → component tests for happy + worst-case paths
Maestro               → every PR, covers your top 5-15 user flows
Detox (optional)      → nightly or pre-release, native-heavy flows only
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most mobile teams should ignore the classic 70/20/10 testing pyramid. UI bugs are what users actually see. A 50/20/30 split usually serves better.&lt;/p&gt;

&lt;h2&gt;
  
  
  The new wrinkle: testing AI-generated apps
&lt;/h2&gt;

&lt;p&gt;A category that didn't exist three years ago: testing React Native code your team didn't write. Tools like RapidNative generate full Expo apps from a prompt or screenshot. The code is real and exportable, but you didn't internalize the structure as you went.&lt;/p&gt;

&lt;p&gt;Maestro is a near-perfect fit here. Because the tests are written from the user's perspective ("tap Login, type email, assert Welcome back"), they don't depend on the internal implementation of components you didn't write yourself. You can write a robust smoke test in 5 minutes via Maestro Studio.&lt;/p&gt;

&lt;p&gt;Workflow that works:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Export the project, run &lt;code&gt;npm install &amp;amp;&amp;amp; npx expo start&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Write one Maestro test for the most critical user flow&lt;/li&gt;
&lt;li&gt;After the next AI-driven iteration, re-run Maestro — anything that breaks is a real regression&lt;/li&gt;
&lt;li&gt;Layer Jest tests on the parts you start modifying by hand&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How to actually decide
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Solo or small team, JS-heavy app&lt;/strong&gt; → Jest + Maestro&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Large team, native modules, custom animations&lt;/strong&gt; → Jest + Detox&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Both? Critical-path safety net + broad smoke coverage&lt;/strong&gt; → All three&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Testing an AI-generated app you'll iterate on&lt;/strong&gt; → Jest + Maestro, lean on Maestro Studio&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The teams I see struggle most are the ones that pick Detox because it sounds serious, then never finish the setup. A working Maestro suite catching 80% of regressions beats a half-configured Detox setup catching none.&lt;/p&gt;




&lt;p&gt;If you're spending more time configuring testing than writing tests, that's a signal — pick the lighter tool, ship, then upgrade when you feel real pain.&lt;/p&gt;

&lt;p&gt;What's your current React Native testing stack? Curious how many people have made the Detox → Maestro switch.&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 or Wednesday, 8-10 AM EST (peak Dev.to traffic)&lt;/li&gt;
&lt;li&gt;Tags: &lt;code&gt;reactnative&lt;/code&gt;, &lt;code&gt;testing&lt;/code&gt;, &lt;code&gt;mobile&lt;/code&gt;, &lt;code&gt;javascript&lt;/code&gt; (Dev.to allows max 4)&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;canonical_url&lt;/code&gt; in frontmatter to RapidNative blog URL (do not skip — this protects SEO)&lt;/li&gt;
&lt;li&gt;Engage in comments within the first 4 hours; Dev.to weighs early engagement heavily for the homepage&lt;/li&gt;
&lt;li&gt;Don't add the schema markup recommendations here — Dev.to handles its own structured data&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>reactnative</category>
      <category>testing</category>
      <category>mobile</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Mobile App Security Best Practices Every Developer Should Know in 2026</title>
      <dc:creator>Famitha M A</dc:creator>
      <pubDate>Thu, 30 Apr 2026 11:48:28 +0000</pubDate>
      <link>https://dev.to/famitha_ma_b9c13ab1d324e/mobile-app-security-best-practices-every-developer-should-know-in-2026-2lo7</link>
      <guid>https://dev.to/famitha_ma_b9c13ab1d324e/mobile-app-security-best-practices-every-developer-should-know-in-2026-2lo7</guid>
      <description>&lt;p&gt;A single hardcoded API key. A forgotten debug log. A token cached in &lt;code&gt;AsyncStorage&lt;/code&gt; instead of the keychain. Any one of these — in an app that took six months to build — can hand an attacker the keys to your users' data on day one of launch.&lt;/p&gt;

&lt;p&gt;Mobile is now the dominant attack surface. Industry analysts project mobile-targeted attacks will grow more than &lt;strong&gt;40% by the end of 2026&lt;/strong&gt;, driven by the explosion of fintech, health, and AI-assistant apps that hold sensitive data on-device. And yet the gap between "we shipped" and "we shipped securely" has never been wider, especially for teams using AI app builders, no-code tools, or React Native templates that abstract the underlying platform away.&lt;/p&gt;

&lt;p&gt;This guide is a pragmatic walkthrough of the mobile app security best practices that actually move the needle — ordered roughly by impact-per-hour-of-work, grounded in the OWASP Mobile Application Security Verification Standard (MASVS), and written specifically with &lt;strong&gt;React Native and Expo&lt;/strong&gt; developers in mind.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1563013544-824ae1b704d3%3Fw%3D1200%2520align%3D" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1563013544-824ae1b704d3%3Fw%3D1200%2520align%3D" alt="A smartphone surrounded by code and a padlock icon symbolizing mobile app security best practices" width="720" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Photo by FLY:D on Unsplash&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What are mobile app security best practices? (40-second answer)
&lt;/h2&gt;

&lt;p&gt;Mobile app security best practices are the controls that protect a mobile app's code, data, and users from theft, tampering, and impersonation. The essential set covers eight areas: secure authentication and session management, encrypted storage of sensitive data, TLS plus optional certificate pinning for traffic, hardened API access with short-lived tokens, no secrets in client code, code obfuscation and tamper detection for high-risk apps, dependency hygiene, and a mobile-specific threat model aligned with the OWASP MASVS framework.&lt;/p&gt;

&lt;p&gt;That's the short version. Below is what each one actually looks like in code.&lt;/p&gt;




&lt;h2&gt;
  
  
  The 2026 Mobile Threat Model in One Picture
&lt;/h2&gt;

&lt;p&gt;Before any checklist, you need a mental model of what you're defending against. A useful simplification: every mobile attack lives in one of four buckets.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Attack surface&lt;/th&gt;
&lt;th&gt;What attackers do&lt;/th&gt;
&lt;th&gt;Who's most at risk&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;The device&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Extract data from local storage, abuse a stolen unlocked phone, exploit jailbroken/rooted OS&lt;/td&gt;
&lt;td&gt;Banking, health, enterprise&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;The network&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Man-in-the-middle, downgrade TLS, intercept tokens on public Wi-Fi&lt;/td&gt;
&lt;td&gt;Any app with auth or payments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;The app binary&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Reverse-engineer the IPA/APK, pull hardcoded secrets, repackage with malicious code&lt;/td&gt;
&lt;td&gt;Any app distributing on app stores&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;The backend&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Hit your API directly, abuse misconfigured endpoints, replay old tokens&lt;/td&gt;
&lt;td&gt;Every app — your client is not your perimeter&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you can name your top two threats from this table for &lt;em&gt;your&lt;/em&gt; product, the rest of this article tells you what to build. If you can't, that's the very first task — the &lt;a href="https://mas.owasp.org/MASVS/" rel="noopener noreferrer"&gt;OWASP MASVS&lt;/a&gt; gives you a structured way to do it in an afternoon.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Authentication and Session Management
&lt;/h2&gt;

&lt;p&gt;Most real-world mobile breaches don't happen because cryptography failed — they happen because authentication failed. Solve this first.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use a battle-tested identity provider, not your own auth
&lt;/h3&gt;

&lt;p&gt;Roll-your-own auth is one of the few decisions that reliably ages badly. Use Auth0, Clerk, Supabase Auth, Firebase Auth, or AWS Cognito. They handle password hashing, rate limiting, breach detection, and JWKS rotation — all the boring parts that get exploited when DIYed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Make MFA the default, not the upsell
&lt;/h3&gt;

&lt;p&gt;In 2026, multi-factor authentication is table stakes — not a premium feature. Offer at minimum:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;TOTP&lt;/strong&gt; (Google Authenticator, 1Password, Authy) — universal, free, phishing-resistant when paired with passkeys&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Biometric step-up&lt;/strong&gt; (&lt;code&gt;expo-local-authentication&lt;/code&gt;) — Face ID / Touch ID for re-auth on sensitive actions&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Passkeys / WebAuthn&lt;/strong&gt; — the modern default, supported on iOS 16+ and Android 9+&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For React Native, &lt;a href="https://docs.expo.dev/versions/latest/sdk/local-authentication/" rel="noopener noreferrer"&gt;&lt;code&gt;expo-local-authentication&lt;/code&gt;&lt;/a&gt; wraps both platforms in one API and falls back gracefully when biometrics aren't enrolled.&lt;/p&gt;

&lt;h3&gt;
  
  
  Treat tokens like cash
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Access tokens&lt;/strong&gt;: short-lived (15 minutes is a sane default), JWT, signed with RS256 or EdDSA — never HS256 with a shared secret.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Refresh tokens&lt;/strong&gt;: longer-lived but &lt;strong&gt;rotated on every use&lt;/strong&gt; with reuse detection. If a refresh token is ever submitted twice, revoke the entire session family.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Logout must invalidate server-side&lt;/strong&gt;, not just clear the client. A token sitting in memory after logout is still valid until expiry.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1556742111-a301076d9d18%3Fw%3D1200%2520align%3D" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1556742111-a301076d9d18%3Fw%3D1200%2520align%3D" alt="Hands holding a smartphone displaying a login screen, illustrating secure authentication for mobile app security best practices" width="800" height="534"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Photo by Jonas Leupe on Unsplash&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Secure Storage: Don't Put Tokens in AsyncStorage
&lt;/h2&gt;

&lt;p&gt;This is the single most common React Native security mistake. &lt;code&gt;AsyncStorage&lt;/code&gt; is a glorified key-value file in your app's sandbox. It's &lt;strong&gt;not encrypted&lt;/strong&gt; on iOS by default and is trivially readable on a jailbroken or rooted device.&lt;/p&gt;

&lt;p&gt;For anything sensitive — auth tokens, refresh tokens, encryption keys, PII — use the platform keystore.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use expo-secure-store (or react-native-keychain)
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://docs.expo.dev/versions/latest/sdk/securestore/" rel="noopener noreferrer"&gt;&lt;code&gt;expo-secure-store&lt;/code&gt;&lt;/a&gt; wraps the right primitive on each platform:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;iOS&lt;/strong&gt;: stores values in the iOS Keychain as &lt;code&gt;kSecClassGenericPassword&lt;/code&gt;, encrypted by the Secure Enclave and bound to your bundle ID.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Android&lt;/strong&gt;: uses Android Keystore-backed encrypted SharedPreferences. Hardware-backed (TEE / StrongBox) when available.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The API is intentionally tiny:&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="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;refreshToken&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="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;keychainAccessible&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SecureStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;WHEN_UNLOCKED_THIS_DEVICE_ONLY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;requireAuthentication&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="c1"&gt;// Face ID / Touch ID gate&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;token&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;SecureStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItemAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;refreshToken&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;
  
  
  What goes where
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Data type&lt;/th&gt;
&lt;th&gt;Storage&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Auth tokens, encryption keys&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;expo-secure-store&lt;/code&gt; / Keychain&lt;/td&gt;
&lt;td&gt;Hardware-backed, OS-protected&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User preferences, theme&lt;/td&gt;
&lt;td&gt;&lt;code&gt;AsyncStorage&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Non-sensitive, fine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cached API responses (PII)&lt;/td&gt;
&lt;td&gt;Encrypted SQLite (e.g., &lt;code&gt;op-sqlite&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Encrypt at rest with a Keychain-derived key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Files (images, documents)&lt;/td&gt;
&lt;td&gt;App sandbox + file-level encryption&lt;/td&gt;
&lt;td&gt;iOS Data Protection class &lt;code&gt;NSFileProtectionComplete&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Encrypt sensitive databases
&lt;/h3&gt;

&lt;p&gt;If you cache PII offline — messages, health records, financial data — use SQLCipher or &lt;code&gt;op-sqlite&lt;/code&gt; with an encryption key generated and stored in Secure Storage. Never hardcode the database key in source.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Data in Transit: TLS, HTTPS, and Certificate Pinning
&lt;/h2&gt;

&lt;h3&gt;
  
  
  HTTPS everywhere, no exceptions
&lt;/h3&gt;

&lt;p&gt;Both iOS App Transport Security (ATS) and Android Network Security Config now enforce HTTPS by default. &lt;strong&gt;Resist the urge to disable them for "convenience"&lt;/strong&gt; during development. If you genuinely need cleartext for local dev, scope the exception to a single domain and never ship that config to release builds. React Native's &lt;a href="https://reactnative.dev/docs/security" rel="noopener noreferrer"&gt;official security docs&lt;/a&gt; cover the platform-specific config files.&lt;/p&gt;

&lt;h3&gt;
  
  
  TLS 1.3, AES-256, and modern cipher suites
&lt;/h3&gt;

&lt;p&gt;You don't usually pick these directly — your platform networking stack does — but you should verify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;TLS 1.3 with TLS 1.2 fallback only&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;HSTS on your APIs&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Forward secrecy via ECDHE&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Modern AES-GCM or ChaCha20-Poly1305 cipher suites&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run your endpoint through SSL Labs once a quarter. A score below A is a bug.&lt;/p&gt;

&lt;h3&gt;
  
  
  Certificate pinning for high-risk apps
&lt;/h3&gt;

&lt;p&gt;Certificate pinning ensures your app only trusts your server's certificate (or its issuing CA), even if a malicious root is installed on the device. In React Native, &lt;a href="https://www.npmjs.com/package/react-native-ssl-public-key-pinning" rel="noopener noreferrer"&gt;&lt;code&gt;react-native-ssl-public-key-pinning&lt;/code&gt;&lt;/a&gt; is the standard library — it pins the public-key hash, which is more rotation-friendly than pinning the leaf cert.&lt;/p&gt;

&lt;p&gt;Honest take: pinning has a real maintenance cost. Pins must rotate before your TLS cert does, or you'll brick every installed app. For most consumer apps, &lt;strong&gt;enforced HTTPS plus HSTS is enough&lt;/strong&gt;. Pin if you're handling money, health data, or auth tokens for high-value accounts. And always ship pinning behind an over-the-air update mechanism (&lt;code&gt;expo-updates&lt;/code&gt;) so you can rotate without an app-store release.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1555949963-aa79dcee981c%3Fw%3D1200%2520align%3D" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1555949963-aa79dcee981c%3Fw%3D1200%2520align%3D" alt="A laptop with code on screen and a smartphone showing a secure connection icon, illustrating data-in-transit security" width="760" height="507"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Photo by Markus Spiske on Unsplash&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  4. API and Backend Security
&lt;/h2&gt;

&lt;p&gt;Your client is not your perimeter. &lt;strong&gt;Every&lt;/strong&gt; input from the mobile app must be revalidated server-side, every authorization decision made server-side, and every secret kept server-side.&lt;/p&gt;

&lt;h3&gt;
  
  
  Never put secrets in the bundle
&lt;/h3&gt;

&lt;p&gt;Anything bundled into your IPA or APK is publicly readable. Stripe secret keys, OpenAI keys, Twilio tokens, AWS credentials — all of these belong in your backend, behind your authenticated API. The mobile app calls &lt;em&gt;your&lt;/em&gt; server, &lt;em&gt;your&lt;/em&gt; server calls the third party.&lt;/p&gt;

&lt;p&gt;A scary number of teams break this rule because they're moving fast or because an AI builder wired up the integration wrong. Audit your &lt;code&gt;.env&lt;/code&gt; files, your &lt;code&gt;Constants.expoConfig.extra&lt;/code&gt; payload, and any &lt;code&gt;process.env.*&lt;/code&gt; references in client code before every release.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use OAuth 2.0 with PKCE
&lt;/h3&gt;

&lt;p&gt;If you call third-party APIs on behalf of the user, use &lt;strong&gt;OAuth 2.0 with PKCE&lt;/strong&gt;. PKCE was designed precisely for native apps that can't keep a client secret. &lt;code&gt;expo-auth-session&lt;/code&gt; implements it correctly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rate-limit and shape traffic at the edge
&lt;/h3&gt;

&lt;p&gt;Your API will be hit directly — not just from your app. Cloudflare, AWS WAF, or your API gateway should enforce:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Per-IP and per-user rate limits&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Bot detection on auth endpoints (signup, login, password reset)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Schema validation rejecting unexpected payloads&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Anomaly detection on payment and admin endpoints&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Validate everything, trust nothing
&lt;/h3&gt;

&lt;p&gt;Server-side, every request body, query parameter, and header is hostile until proven otherwise. Use a schema library (Zod, Yup, Pydantic) to validate inputs before they reach business logic. Store user IDs from the &lt;strong&gt;JWT claim&lt;/strong&gt;, never from a request field — otherwise a user can simply pass &lt;code&gt;userId: 7&lt;/code&gt; and read someone else's data. (This class of bug, &lt;em&gt;broken object-level authorization&lt;/em&gt;, is OWASP API #1 for a reason.)&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Code-Level Defenses
&lt;/h2&gt;

&lt;p&gt;These matter most for apps that handle high-value assets — banking, crypto wallets, paid streaming — and matter little for a B2B internal tool.&lt;/p&gt;

&lt;h3&gt;
  
  
  Code obfuscation
&lt;/h3&gt;

&lt;p&gt;JavaScript bundles in React Native are readable. Hermes bytecode raises the bar slightly but isn't real obfuscation. For high-risk apps, layer in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Minification + property mangling&lt;/strong&gt; (Metro + Terser settings)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Native obfuscation&lt;/strong&gt; for any custom native modules (ProGuard / R8 on Android, bitcode + symbol stripping on iOS)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Commercial RASP&lt;/strong&gt; (Runtime Application Self-Protection) like Guardsquare DexGuard or Promon Shield — overkill for most apps, essential for banking&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Jailbreak / root detection
&lt;/h3&gt;

&lt;p&gt;A jailbroken or rooted device throws every device-bound assumption out the window. For sensitive apps, detect and respond:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;jail-monkey&lt;/code&gt; (React Native) gives you boolean signals for jailbreak, debugger attached, mock location, and emulator&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Don't crash the app — that's user-hostile and easy to bypass. Instead, raise the friction (require re-auth, disable offline mode, add server-side anomaly score)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Tamper detection and integrity attestation
&lt;/h3&gt;

&lt;p&gt;Use &lt;strong&gt;Apple's DeviceCheck / App Attest&lt;/strong&gt; and &lt;strong&gt;Google's Play Integrity API&lt;/strong&gt; to verify the app binary hasn't been modified and is running on a genuine device. These are server-validated and much harder to spoof than client-side checks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Disable debug features in production
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Strip &lt;code&gt;console.log&lt;/code&gt; from release builds (Metro's &lt;code&gt;inlineRequires&lt;/code&gt; + Babel &lt;code&gt;transform-remove-console&lt;/code&gt;)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Disable React Native debugger and Flipper in release variants&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Turn off network inspection tools&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Set &lt;code&gt;__DEV__&lt;/code&gt; correctly — don't ship dev-only code paths&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  6. Dependency and Supply-Chain Hygiene
&lt;/h2&gt;

&lt;p&gt;The npm ecosystem is enormous and hostile. A single malicious post-install script in a transitive dependency can exfiltrate your entire dev environment.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Run &lt;code&gt;npm audit&lt;/code&gt; (or Snyk / Socket.dev) on every PR. Block CI on high/critical findings.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Use &lt;strong&gt;lockfiles&lt;/strong&gt; (&lt;code&gt;package-lock.json&lt;/code&gt;, &lt;code&gt;yarn.lock&lt;/code&gt;, &lt;code&gt;pnpm-lock.yaml&lt;/code&gt;) and &lt;strong&gt;never commit edits to them by hand&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Pin to specific versions for security-sensitive packages. Floating ranges (&lt;code&gt;^&lt;/code&gt;, &lt;code&gt;~&lt;/code&gt;) are fine for utilities but risky for crypto, auth, and networking libs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Scan native dependencies too — CocoaPods and Gradle have their own vulnerability databases.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Maintain an &lt;strong&gt;SBOM&lt;/strong&gt; (Software Bill of Materials) so you can audit exposure when the next big CVE drops.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  7. The OWASP MASVS Framework
&lt;/h2&gt;

&lt;p&gt;If you take one thing from this article, take this: &lt;strong&gt;align your security work to the&lt;/strong&gt; &lt;a href="https://mas.owasp.org/MASVS/" rel="noopener noreferrer"&gt;&lt;strong&gt;OWASP Mobile Application Security Verification Standard&lt;/strong&gt;&lt;/a&gt;. It's the industry standard, free, and structured.&lt;/p&gt;

&lt;p&gt;MASVS is split into eight control groups (architecture, storage, crypto, auth, network, platform, code, resilience) and three verification levels:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;MASVS-L1&lt;/strong&gt; — baseline security every app should meet. Most consumer apps target this.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;MASVS-L2&lt;/strong&gt; — defense-in-depth for apps handling regulated data (banking, healthcare, government).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;MASVS-R&lt;/strong&gt; — anti-reverse-engineering and resilience controls for apps under active threat.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;OWASP also publishes the &lt;strong&gt;MASTG&lt;/strong&gt; (Mobile Application Security Testing Guide) with concrete tests, and a &lt;a href="https://owasp.org/blog/2024/10/02/Securing-React-Native-Mobile-Apps-with-OWASP-MAS.html" rel="noopener noreferrer"&gt;dedicated guide for React Native apps&lt;/a&gt; since hybrid frameworks aren't the MASTG's primary focus.&lt;/p&gt;

&lt;p&gt;Pick a level based on your threat model, then walk the checklist. It's the closest the industry has to a definitive answer for "are we secure enough?"&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1551288049-bebda4e38f71%3Fw%3D1200%2520align%3D" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1551288049-bebda4e38f71%3Fw%3D1200%2520align%3D" alt="A developer reviewing security audit checklist on a laptop" width="720" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Photo by Carlos Muza on Unsplash&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  People Also Ask
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How do you secure a mobile app?
&lt;/h3&gt;

&lt;p&gt;Secure a mobile app by layering controls across four surfaces: device (encrypted secure storage, biometric step-up, jailbreak detection), network (HTTPS with TLS 1.3, optional certificate pinning), binary (code obfuscation, tamper detection, no hardcoded secrets), and backend (short-lived JWTs, OAuth 2.0 with PKCE, server-side authorization). Map each control to OWASP MASVS L1 as your minimum bar.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the OWASP MASVS?
&lt;/h3&gt;

&lt;p&gt;The OWASP Mobile Application Security Verification Standard (MASVS) is the industry-standard framework for mobile app security requirements. It defines eight control groups across architecture, storage, cryptography, authentication, network, platform, code quality, and resilience, with three verification levels — L1 baseline, L2 defense-in-depth, and R for reverse-engineering resistance. It's free, maintained by OWASP, and pairs with the MASTG testing guide.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is React Native secure?
&lt;/h3&gt;

&lt;p&gt;React Native is as secure as the patterns you use with it. The framework itself ships safe defaults — HTTPS-by-default, sandboxed storage, native crypto primitives — but common mistakes like storing tokens in &lt;code&gt;AsyncStorage&lt;/code&gt;, hardcoding API keys in &lt;code&gt;Constants.expoConfig.extra&lt;/code&gt;, or shipping debug builds undermine all of that. Follow OWASP MASVS, use &lt;a href="https://docs.expo.dev/versions/latest/sdk/securestore/" rel="noopener noreferrer"&gt;&lt;code&gt;expo-secure-store&lt;/code&gt;&lt;/a&gt;, and you can match the security of native apps.&lt;/p&gt;




&lt;h2&gt;
  
  
  What AI App Builders Get Right (and What's Still on You)
&lt;/h2&gt;

&lt;p&gt;If you're building with an AI app builder like &lt;a href="https://www.rapidnative.com/?utm_source=blog_devto&amp;amp;utm_medium=content&amp;amp;utm_campaign=mobile-app-security-best-practices" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt; — which generates production-ready React Native and Expo code from natural-language prompts — a real chunk of the security baseline comes pre-wired. Generated code uses Expo's modern SDK defaults: &lt;code&gt;expo-secure-store&lt;/code&gt; instead of &lt;code&gt;AsyncStorage&lt;/code&gt; for tokens, HTTPS-enforced fetch wrappers, environment-aware config, and modern auth patterns when you connect Supabase or Firebase. Our &lt;a href="https://www.rapidnative.com/blogs/inside-rapidnatives-export-pipeline-from-ai-generated-code-to-app-store?utm_source=blog_devto&amp;amp;utm_medium=content&amp;amp;utm_campaign=mobile-app-security-best-practices" rel="noopener noreferrer"&gt;export pipeline&lt;/a&gt; ships clean, auditable code you fully own — no lock-in, no black boxes.&lt;/p&gt;

&lt;p&gt;But — and this is the honest part — no AI builder, no template, and no boilerplate can decide your threat model for you. Whether you need certificate pinning, jailbreak detection, or App Attest depends on what your app &lt;em&gt;does&lt;/em&gt; and who wants to attack it. The same applies to vibe-coded apps in general; we wrote about the &lt;a href="https://www.rapidnative.com/blogs/vibe-coding-risks-best-practices?utm_source=blog&amp;amp;utm_medium=content&amp;amp;utm_campaign=mobile-app-security-best-practices" rel="noopener noreferrer"&gt;risks and best practices of vibe coding&lt;/a&gt; for exactly this reason. Treat AI-generated code as a strong starting point — then run it through the MASVS checklist before you ship.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Pre-Launch Mobile App Security Checklist
&lt;/h2&gt;

&lt;p&gt;Print this. Pin it above your monitor. Walk it before every release.&lt;/p&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;th&gt;Quick check&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;No hardcoded secrets in the bundle&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;grep&lt;/code&gt; your build output for &lt;code&gt;sk_&lt;/code&gt;, &lt;code&gt;Bearer&lt;/code&gt;, AWS keys&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Tokens in &lt;code&gt;expo-secure-store&lt;/code&gt;, not &lt;code&gt;AsyncStorage&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Audit storage calls&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;HTTPS enforced, ATS / NSC unmodified&lt;/td&gt;
&lt;td&gt;Check &lt;code&gt;Info.plist&lt;/code&gt; and &lt;code&gt;network_security_config.xml&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Short-lived access tokens + refresh rotation&lt;/td&gt;
&lt;td&gt;Test refresh-token reuse triggers session revocation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;OAuth 2.0 with PKCE for third parties&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;expo-auth-session&lt;/code&gt; with &lt;code&gt;usePKCE: true&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Server-side authorization on every endpoint&lt;/td&gt;
&lt;td&gt;Try IDOR with a second test account&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Rate limiting on auth endpoints&lt;/td&gt;
&lt;td&gt;Hammer login with curl; expect 429&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;console.log&lt;/code&gt; stripped in release builds&lt;/td&gt;
&lt;td&gt;Inspect the release bundle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;npm audit&lt;/code&gt; clean (no high/critical)&lt;/td&gt;
&lt;td&gt;CI gate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Dependencies pinned, lockfile committed&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;git status&lt;/code&gt; clean after &lt;code&gt;npm ci&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;Privacy manifest + permissions justified&lt;/td&gt;
&lt;td&gt;iOS 17+ requires &lt;code&gt;PrivacyInfo.xcprivacy&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;Crash reporting scrubs PII&lt;/td&gt;
&lt;td&gt;Sentry &lt;code&gt;beforeSend&lt;/code&gt; filter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;App Attest / Play Integrity wired (if high-risk)&lt;/td&gt;
&lt;td&gt;Verify server-side&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;MASVS L1 self-assessment passed&lt;/td&gt;
&lt;td&gt;OWASP MAS checklist&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Closing Thought: Security Is a Feature, Not a Phase
&lt;/h2&gt;

&lt;p&gt;The biggest shift in mobile security thinking since 2020 is that "we'll add security at the end" no longer works. Modern apps move faster — fueled by AI builders, faster CI, and OTA updates — which means security has to move with them, not after them. Build the right defaults in from day one, align to MASVS, and revisit the checklist before every release.&lt;/p&gt;

&lt;p&gt;The good news: the tooling is dramatically better than it was even two years ago. Expo's secure primitives, modern identity providers, integrity APIs, and AI builders that ship safe defaults out of the box mean a small team can ship something genuinely well-secured without a dedicated security engineer.&lt;/p&gt;

&lt;p&gt;If you're starting a new React Native or Expo app and want safe defaults baked in from the first prompt, &lt;a href="https://www.rapidnative.com/?utm_source=blog_devto&amp;amp;utm_medium=content&amp;amp;utm_campaign=mobile-app-security-best-practices" rel="noopener noreferrer"&gt;try RapidNative free&lt;/a&gt; — describe your idea, get production-ready code, and iterate from there. Pair it with this checklist, and you'll be ahead of the security curve where most launches are still behind.&lt;/p&gt;

&lt;p&gt;Want to go deeper on the React Native side? Read &lt;a href="https://www.rapidnative.com/blogs/what-is-react-native?utm_source=blog_devto&amp;amp;utm_medium=content&amp;amp;utm_campaign=mobile-app-security-best-practices" rel="noopener noreferrer"&gt;What is React Native&lt;/a&gt;, and our &lt;a href="https://www.rapidnative.com/blogs/expo-vs-react-native?utm_source=blog_devto&amp;amp;utm_medium=content&amp;amp;utm_campaign=mobile-app-security-best-practices" rel="noopener noreferrer"&gt;Expo vs React Native&lt;/a&gt;&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>security</category>
      <category>mobile</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Building an AI Chat Interface in React Native with Streaming Responses</title>
      <dc:creator>Famitha M A</dc:creator>
      <pubDate>Wed, 29 Apr 2026 08:35:40 +0000</pubDate>
      <link>https://dev.to/famitha_ma_b9c13ab1d324e/building-an-ai-chat-interface-in-react-native-with-streaming-responses-1b8g</link>
      <guid>https://dev.to/famitha_ma_b9c13ab1d324e/building-an-ai-chat-interface-in-react-native-with-streaming-responses-1b8g</guid>
      <description>&lt;h1&gt;
  
  
  Building an AI Chat Interface in React Native with Streaming Responses
&lt;/h1&gt;

&lt;p&gt;Tap "send", wait three seconds, then a wall of text appears at once. That's the difference between an AI chat that feels alive and one that feels broken. Streaming — token-by-token rendering as the model thinks — turns a slow API call into something users perceive as a conversation. On the web, the Vercel AI SDK and &lt;code&gt;useChat&lt;/code&gt; make it almost trivial. On React Native, you have to think harder: there's no &lt;code&gt;EventSource&lt;/code&gt; in the runtime, &lt;code&gt;FlatList&lt;/code&gt; can't be naïve about re-renders, and Markdown doesn't render itself like it does in the browser.&lt;/p&gt;

&lt;p&gt;This post walks through a production-ready streaming AI chat in React Native and Expo end-to-end — server endpoint, client wiring, and the mobile-specific gotchas (network drops, scroll behavior, persistence) most tutorials skip.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stack:&lt;/strong&gt; Expo SDK 54+, React Native 0.81, the Vercel AI SDK (&lt;code&gt;ai&lt;/code&gt; v4.3+), and any major LLM provider — Anthropic Claude, OpenAI GPT, or xAI Grok via the &lt;code&gt;@ai-sdk/*&lt;/code&gt; adapters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why streaming changes the UX
&lt;/h2&gt;

&lt;p&gt;Streaming isn't a performance optimization, it's a perception fix. A non-streaming chat call that takes 6 seconds feels broken — the user assumes the app froze. The same call streamed token-by-token feels responsive within 200ms because the first words land before the brain decides "this is taking too long."&lt;/p&gt;

&lt;p&gt;On mobile this matters more than on web. Cellular latency varies wildly, App Store reviews punish perceived slowness, and a static spinner doesn't telegraph progress the way moving text does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture: three moving parts
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The mobile client&lt;/strong&gt; — Expo app, holding messages in state, rendering tokens as they arrive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An edge endpoint&lt;/strong&gt; — Next.js API route or similar HTTP boundary that holds your provider API key and proxies the stream.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The model provider&lt;/strong&gt; — Anthropic, OpenAI, Bedrock, etc., emitting SSE chunks.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You never call the provider directly from the mobile app. Your API key would leak into the JS bundle, and most providers reject mobile origins or rate-limit them aggressively. The edge endpoint is mandatory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bunx create-expo-app@latest chat-app &lt;span class="nt"&gt;--template&lt;/span&gt; tabs
&lt;span class="nb"&gt;cd &lt;/span&gt;chat-app
bun &lt;span class="nb"&gt;install &lt;/span&gt;ai @ai-sdk/anthropic zod react-native-markdown-display
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same &lt;code&gt;ai&lt;/code&gt; package on server and client — the AI SDK 4 normalizes streaming protocols (&lt;code&gt;x-vercel-ai-data-stream&lt;/code&gt;) so both sides speak the same wire format.&lt;/p&gt;

&lt;h2&gt;
  
  
  The server endpoint
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/api/chat/route.ts (Next.js)&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;anthropic&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;@ai-sdk/anthropic&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;streamText&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;ai&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zod&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enum&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;assistant&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;system&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
    &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;})).&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&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;Request&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;messages&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;req&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="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="nf"&gt;streamText&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;anthropic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;claude-sonnet-4-5&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;system&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;You are a concise, helpful assistant. Use Markdown for code.&lt;/span&gt;&lt;span class="dl"&gt;'&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="na"&gt;maxTokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1024&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;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toDataStreamResponse&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;streamText&lt;/code&gt; returns lazily, &lt;code&gt;toDataStreamResponse()&lt;/code&gt; emits the AI SDK's data-stream protocol, and Zod validates the message shape. Always cap message count — 50 is a reasonable ceiling — to prevent prompt-injection-via-history-bloat.&lt;/p&gt;

&lt;p&gt;For production, add a per-user rate limiter (Upstash sliding window works well) and an auth check before you ever hit the provider. Every dropped request that never reaches the model is dollars saved.&lt;/p&gt;

&lt;h2&gt;
  
  
  The React Native client
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/(tabs)/index.tsx&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;useChat&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;ai/react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;FlatList&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;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;useRef&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;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;ChatScreen&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;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handleInputChange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handleSubmit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;stop&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nf"&gt;useChat&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;api&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://your-api.com/api/chat&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;streamProtocol&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data&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;listRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;FlatList&lt;/span&gt;&lt;span class="o"&gt;&amp;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="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FlatList&lt;/span&gt;
        &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;listRef&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;keyExtractor&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&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;m&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;renderItem&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;MessageBubble&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;item&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="na"&gt;onContentSizeChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
          &lt;span class="nx"&gt;listRef&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="nf"&gt;scrollToEnd&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;animated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="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;ChatInput&lt;/span&gt; &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;...{&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handleInputChange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handleSubmit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isLoading&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two React Native specifics: &lt;code&gt;handleInputChange&lt;/code&gt; expects a synthetic event, so you cast a fake event shape because &lt;code&gt;TextInput&lt;/code&gt; only gives you a string. Auto-scroll on &lt;code&gt;onContentSizeChange&lt;/code&gt; is correct because each new token expands content height — don't scroll on &lt;code&gt;onChangeText&lt;/code&gt; or you'll fight the user when they read earlier messages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rendering Markdown
&lt;/h2&gt;

&lt;p&gt;LLMs return Markdown. React Native's &lt;code&gt;Text&lt;/code&gt; won't render it. Three options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;react-native-markdown-display&lt;/code&gt;&lt;/strong&gt; — pure JS, fast for chat-sized output, custom renderers for code blocks. Default choice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;@expo/html-elements&lt;/code&gt; + Markdown-to-HTML&lt;/strong&gt; — heavier but reusable if you render web content elsewhere.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom parser&lt;/strong&gt; — only if you need strict typography control.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Don't use &lt;code&gt;react-native-render-html&lt;/code&gt;. It's slow and visibly janks during streaming.&lt;/p&gt;

&lt;h2&gt;
  
  
  Network resilience
&lt;/h2&gt;

&lt;p&gt;Mobile networks drop. A 30-second Claude response can lose its connection three times when the user walks into an elevator.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;stop()&lt;/code&gt; from &lt;code&gt;useChat&lt;/code&gt; calls &lt;code&gt;AbortController.abort()&lt;/code&gt; on the underlying fetch — wire it to a stop button. For retries, restart-on-fail is almost always the right call: discard the partial response and re-send. Resume-marker patterns are complex and only worth it for very long generations. Add exponential backoff (1s, 2s, 4s, give up at 8s).&lt;/p&gt;

&lt;h2&gt;
  
  
  Persistence
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;useChat&lt;/code&gt; keeps messages in memory. Persist them with AsyncStorage:&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="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;isLoading&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&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;chat:messages&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;messages&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;isLoading&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Don't write on every token (hundreds of writes per response). Debounce to 300ms or only write when &lt;code&gt;isLoading&lt;/code&gt; flips to &lt;code&gt;false&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance on real devices
&lt;/h2&gt;

&lt;p&gt;A naïve &lt;code&gt;FlatList&lt;/code&gt; re-renders the entire conversation per token. At 30 tokens/sec on 50 messages, you drop frames.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Memoize bubbles&lt;/strong&gt; with &lt;code&gt;React.memo&lt;/code&gt; + custom comparator: re-render only on &lt;code&gt;id&lt;/code&gt;, &lt;code&gt;content&lt;/code&gt;, or &lt;code&gt;isStreaming&lt;/code&gt; change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use &lt;code&gt;keyExtractor&lt;/code&gt; correctly&lt;/strong&gt; — return &lt;code&gt;message.id&lt;/code&gt;, not array index.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batch updates&lt;/strong&gt; — wrap mutations in &lt;code&gt;requestAnimationFrame&lt;/code&gt; if managing messages manually.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Test on a real low-end Android device. Streaming jank is invisible on M-series Macs and brutal on a $150 Pixel.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Edge endpoint validates with Zod&lt;/li&gt;
&lt;li&gt;Per-user rate limit&lt;/li&gt;
&lt;li&gt;Stop button visible during &lt;code&gt;isLoading&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Markdown renderer with code highlighting&lt;/li&gt;
&lt;li&gt;Messages persist across restarts&lt;/li&gt;
&lt;li&gt;Auto-scroll on content-size change, not input change&lt;/li&gt;
&lt;li&gt;Tested on real low-end Android&lt;/li&gt;
&lt;li&gt;Sentry on the streaming endpoint&lt;/li&gt;
&lt;li&gt;Stream timeout configured (30s soft cap)&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;A great React Native AI chat is mostly mobile craft, not AI craft. Streaming is solved at the SDK layer; the work is rendering tokens smoothly on a $150 Android, persisting messages, surviving cellular handoffs.&lt;/p&gt;

&lt;p&gt;If you want to skip the boilerplate, &lt;a href="https://www.rapidnative.com/?utm_source=blog&amp;amp;utm_medium=content&amp;amp;utm_campaign=devto-rn-ai-chat-streaming" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt; generates the streaming endpoint + &lt;code&gt;useChat&lt;/code&gt; wiring + Markdown bubbles + persistence layer from a natural-language prompt. Output is plain Expo code you own and can extend.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>ai</category>
      <category>mobile</category>
      <category>expo</category>
    </item>
    <item>
      <title>Cross-Platform vs Native in 2026: Why the Debate Is Actually Over</title>
      <dc:creator>Famitha M A</dc:creator>
      <pubDate>Mon, 27 Apr 2026 11:02:53 +0000</pubDate>
      <link>https://dev.to/famitha_ma_b9c13ab1d324e/cross-platform-vs-native-in-2026-why-the-debate-is-actually-over-1i6</link>
      <guid>https://dev.to/famitha_ma_b9c13ab1d324e/cross-platform-vs-native-in-2026-why-the-debate-is-actually-over-1i6</guid>
      <description>&lt;h1&gt;
  
  
  Cross-Platform vs Native in 2026: Why the Debate Is Actually Over
&lt;/h1&gt;

&lt;p&gt;Every mobile team has had the same argument for a decade. Swift + Kotlin, or pick a cross-platform framework and eat the "feels slightly off" tax forever?&lt;/p&gt;

&lt;p&gt;In 2026, that argument is mostly theater. Here's the technical case, grounded in what actually changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bridge is gone
&lt;/h2&gt;

&lt;p&gt;React Native's historical performance gap came from the async JavaScript bridge. Every call to native had to be serialized and queued.&lt;/p&gt;

&lt;p&gt;That's gone. The &lt;strong&gt;New Architecture&lt;/strong&gt; (default in 0.76+) replaces it with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;JSI (JavaScript Interface)&lt;/strong&gt; — a C++ layer giving JS direct synchronous access to native code&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fabric renderer&lt;/strong&gt; — composes views synchronously on the UI thread, matching UIKit/Compose&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TurboModules&lt;/strong&gt; — lazy-loaded, directly callable native modules&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Result: 60fps (or 120fps on ProMotion) on the same list-scroll and animation workloads that used to stutter. Meta, Shopify, Discord, Microsoft, Coinbase, and Tesla all run production apps on this stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  API parity is effectively solved
&lt;/h2&gt;

&lt;p&gt;"Native gets new APIs day one" used to mean months of waiting on cross-platform.&lt;/p&gt;

&lt;p&gt;In 2026 it means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Expo Modules API + config plugins let you write Swift/Kotlin and expose it to JS&lt;/li&gt;
&lt;li&gt;Community modules ship within weeks of iOS/Android releases&lt;/li&gt;
&lt;li&gt;HealthKit, Live Activities, App Intents, Core ML — all have working integrations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The real gap is spatial computing and bleeding-edge launch-day features. Everything else is covered.&lt;/p&gt;

&lt;h2&gt;
  
  
  UI fidelity — it looks native because it &lt;em&gt;is&lt;/em&gt; native
&lt;/h2&gt;

&lt;p&gt;React Native renders actual platform views:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;TextInput&amp;gt;&lt;/code&gt; → &lt;code&gt;UITextField&lt;/code&gt; / &lt;code&gt;EditText&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;ScrollView&amp;gt;&lt;/code&gt; → &lt;code&gt;UIScrollView&lt;/code&gt; / &lt;code&gt;RecyclerView&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Gestures, accessibility, keyboard behavior — handled by the OS. This is why Shopify, Outlook Mobile, and the Meta apps don't feel "off."&lt;/p&gt;

&lt;p&gt;Flutter takes the other route — draws its own widgets via Skia/Impeller. Trades native feel for pixel-perfect consistency.&lt;/p&gt;

&lt;h2&gt;
  
  
  Developer experience: hot reload still wins
&lt;/h2&gt;

&lt;p&gt;SwiftUI previews and Compose live edits closed part of the gap. But:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sub-second full-app reload on a physical device&lt;/li&gt;
&lt;li&gt;Same codebase runs on iOS + Android simultaneously — parity issues surface instantly&lt;/li&gt;
&lt;li&gt;No Xcode rebuild, no Gradle sync&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A team iterating 10x faster on UI compounds hard over a release cycle.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 95/5 split
&lt;/h2&gt;

&lt;p&gt;Large production apps don't hit 100% code sharing. Shopify reports ~95%. The remaining 5% is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Platform-specific integrations&lt;/li&gt;
&lt;li&gt;Native modules for perf-critical paths&lt;/li&gt;
&lt;li&gt;Store configuration&lt;/li&gt;
&lt;li&gt;Occasional platform UI divergence&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The useful question is "is 95% enough to justify one codebase?" Almost always yes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Native&lt;/th&gt;
&lt;th&gt;React Native (2026)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Codebases&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Team size&lt;/td&gt;
&lt;td&gt;6-8&lt;/td&gt;
&lt;td&gt;3-4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UI performance&lt;/td&gt;
&lt;td&gt;Native&lt;/td&gt;
&lt;td&gt;Native (New Arch)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UI fidelity&lt;/td&gt;
&lt;td&gt;Native&lt;/td&gt;
&lt;td&gt;Native (same components)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hot reload&lt;/td&gt;
&lt;td&gt;Previews only&lt;/td&gt;
&lt;td&gt;Full app, sub-second&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code sharing&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;90-95%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Talent pool&lt;/td&gt;
&lt;td&gt;Swift + Kotlin specialists&lt;/td&gt;
&lt;td&gt;Any TypeScript dev&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  When to still go native
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Games / real-time graphics (Metal, Vulkan)&lt;/li&gt;
&lt;li&gt;AR/VR / spatial computing (Vision Pro, Quest)&lt;/li&gt;
&lt;li&gt;Deep hardware integration (specialized BLE, sensors)&lt;/li&gt;
&lt;li&gt;Teams with zero frontend capacity&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's it. "My app is too important" doesn't qualify — Instagram and Shopify are also important.&lt;/p&gt;

&lt;h2&gt;
  
  
  The AI angle nobody's talking about
&lt;/h2&gt;

&lt;p&gt;TypeScript + JSX is the most densely represented stack in LLM training data. Cross-platform generation works because one React Native file targets both platforms; keeping a SwiftUI file and a Compose file in sync is a much harder AI problem.&lt;/p&gt;

&lt;p&gt;This is why AI-native builders like &lt;a href="https://www.rapidnative.com/?utm_source=blog-devto&amp;amp;utm_medium=content&amp;amp;utm_campaign=cross-platform-vs-native-debate-over" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt; can turn a prompt, sketch, or screenshot into a production React Native app in minutes — and why the equivalent on native-only stacks doesn't exist yet.&lt;/p&gt;

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

&lt;p&gt;The debate didn't end because one side won an argument. It ended because the performance gap closed, the tooling overtook native on DX, and the largest apps on earth quietly migrated.&lt;/p&gt;

&lt;p&gt;If you're picking a stack for a new mobile app in 2026, the default is React Native + Expo unless you're building a game, an AR/VR app, or have deep native-only expertise.&lt;/p&gt;

&lt;p&gt;What's your current stack, and what's keeping you on it? Drop a comment.&lt;/p&gt;

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