<?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: Mark F A</title>
    <description>The latest articles on DEV Community by Mark F A (@mark_fa).</description>
    <link>https://dev.to/mark_fa</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3809861%2Fcea830b9-a2f6-4e96-8171-71cd64b7a88b.jpg</url>
      <title>DEV Community: Mark F A</title>
      <link>https://dev.to/mark_fa</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mark_fa"/>
    <language>en</language>
    <item>
      <title>The Non-Technical Founder's Guide to Mobile App Development in 2026</title>
      <dc:creator>Mark F A</dc:creator>
      <pubDate>Mon, 31 Aug 2026 11:02:00 +0000</pubDate>
      <link>https://dev.to/mark_fa/the-non-technical-founders-guide-to-mobile-app-development-in-2026-3j0j</link>
      <guid>https://dev.to/mark_fa/the-non-technical-founders-guide-to-mobile-app-development-in-2026-3j0j</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Four realistic paths in 2026: agency, freelancer, no-code platform, AI-native builder.&lt;/li&gt;
&lt;li&gt;Agencies: $45,000–$150,000, 4–7 months. Freelancers: $15,000–$60,000, 2–5 months. No-code: cheap, but you hit a ceiling and never own code. AI-native: real React Native output, days not months.&lt;/li&gt;
&lt;li&gt;Three decisions are yours before you hire anyone: cross-platform vs native, launch sequencing, and what you refuse to build in v1.&lt;/li&gt;
&lt;li&gt;The 30-day playbook at the bottom assumes the AI-native path. Stretch it a lot for the others.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you are the engineer someone is about to ask "can you build my app idea," this is the piece to send them. It is written for the founder, but the useful part for you is the section on what they should decide before they land in your inbox.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually changed between 2023 and 2026
&lt;/h2&gt;

&lt;p&gt;Three things landed at once.&lt;/p&gt;

&lt;p&gt;AI code generation crossed from demo to production output. Models now write React Native and Expo code that boots on TestFlight without a human rewriting it from scratch. Expo tooling (OTA updates, EAS Build, universal links) stopped requiring a senior mobile engineer to configure. And a category emerged between no-code and code generation: describe the app, real code comes out, you own it.&lt;/p&gt;

&lt;p&gt;The consequence is that a solo non-technical founder can put a working iOS and Android app in front of paying users in days, on budgets that used to cover only design.&lt;/p&gt;

&lt;p&gt;That does not kill the old paths. It means there are four, and picking wrong is the expensive part.&lt;/p&gt;

&lt;h2&gt;
  
  
  Path 1: agency
&lt;/h2&gt;

&lt;p&gt;Brief them, they design and build, they hand over a codebase. Typically 4–7 months for an MVP. $45,000–$150,000 in 2026 for auth, a backend, and one core workflow. Higher in North America, lower with Eastern European or Latin American shops.&lt;/p&gt;

&lt;p&gt;Real upside: polished output, they handle App Store submission, they own the responsibility for shipping. Real downside: you are the least-informed person in every technical meeting, change requests cost money, and if the agency wobbles at month four you are stuck.&lt;/p&gt;

&lt;p&gt;Pick this when you have capital, a fixed deadline, and requirements that genuinely need five specialists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Path 2: freelancer
&lt;/h2&gt;

&lt;p&gt;A senior React Native freelancer runs $60–$120 per hour in the US, less abroad. A lean MVP is 200–500 hours, so $15,000–$60,000 depending on location and scope discipline.&lt;/p&gt;

&lt;p&gt;This works when you find the right person. It also generates the most founder horror stories, because "the right person" is hard to source without a technical network, and if you cannot read code you will not find out you inherited a mess until you try to hand it to someone else.&lt;/p&gt;

&lt;p&gt;Pick this if you have a specific recommendation, or a technical advisor who can review the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Path 3: no-code
&lt;/h2&gt;

&lt;p&gt;Bubble, Adalo, Glide, FlutterFlow. Drag components, configure logic through visual rules. No code output. The app lives in the platform runtime permanently.&lt;/p&gt;

&lt;p&gt;You can genuinely build with zero technical background. You also cannot leave without a rewrite, complex logic turns into nested visual conditions, performance trails a native app, and App Store rejections for wrapped apps keep happening.&lt;/p&gt;

&lt;p&gt;Pick this if the app is genuinely simple (directory, form-heavy internal tool, booking flow) and will not scale past a few thousand users.&lt;/p&gt;

&lt;h2&gt;
  
  
  Path 4: AI-native builders
&lt;/h2&gt;

&lt;p&gt;Describe the app in plain English, sketch it, or paste a PRD. An AI generates React Native and Expo code. You own it from day one, preview on your actual phone, and iterate by describing changes.&lt;/p&gt;

&lt;p&gt;This is what broke the cost math. Tens of dollars in credits instead of tens of thousands in labor, and evenings instead of quarters. Because the output is standard React Native on Expo, if the thing takes off you hand it to an engineer and they extend it like any other project.&lt;/p&gt;

&lt;p&gt;The trade-off is real: these tools are strongest on the common surface area (auth, lists, forms, media, standard navigation) and need human judgment for custom device APIs, complex real-time systems, and novel UI. For a first app, the common surface area is usually the entire product.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cost table
&lt;/h2&gt;

&lt;p&gt;Comparable MVP in every row: auth, a backend, one core workflow, shipped to iOS and Android. These are ranges, not quotes.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;Upfront&lt;/th&gt;
&lt;th&gt;Time to v1&lt;/th&gt;
&lt;th&gt;Ongoing/mo&lt;/th&gt;
&lt;th&gt;Own the code&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Agency&lt;/td&gt;
&lt;td&gt;$45,000–$150,000&lt;/td&gt;
&lt;td&gt;4–7 months&lt;/td&gt;
&lt;td&gt;$0 (or retainer)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Freelancer&lt;/td&gt;
&lt;td&gt;$15,000–$60,000&lt;/td&gt;
&lt;td&gt;2–5 months&lt;/td&gt;
&lt;td&gt;$0 (or hourly)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No-code&lt;/td&gt;
&lt;td&gt;$0–$500&lt;/td&gt;
&lt;td&gt;2–6 weeks&lt;/td&gt;
&lt;td&gt;$50–$500&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI-native&lt;/td&gt;
&lt;td&gt;$0–$200&lt;/td&gt;
&lt;td&gt;1–5 days&lt;/td&gt;
&lt;td&gt;$20–$100&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The low numbers are not typos. An AI-native builder like &lt;a href="https://rapidnative.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=non-technical-founders-guide-mobile-app-development-2026" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt; produces a functional MVP for the cost of dinner, because the labor cost collapsed. What still costs money is everything you did not write: $99/year Apple, $25 one-time Google, $0–$50/month backend at MVP scale on Supabase or similar, and paid acquisition.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three decisions a founder should make before hiring anyone
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Cross-platform vs native.&lt;/strong&gt; Native means Swift plus Kotlin, two codebases, roughly double the cost. Cross-platform means one codebase on React Native or Flutter. In 2026 the answer for almost every startup is cross-platform. The performance gap has effectively closed for standard functionality, and Instagram, Discord, Shopify, and Coinbase all run significant parts of their mobile products on React Native. Legitimate reasons to go native: hardcore graphics, a platform feature with no wrapper yet, or a strategic showcase play. If someone tells a founder otherwise, ask them to name the specific device API they cannot reach.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Launch sequencing.&lt;/strong&gt; Cross-platform gets you both stores, so this is mostly about order. TestFlight for iOS first, 20–50 users, tune the flow, then submit to both together. Apple's review is stricter, so clearing it early prevents a launch-day surprise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you refuse to build.&lt;/strong&gt; The most valuable technical decision a non-technical founder makes is the cut list. The MVP should do one thing well enough that a user recommends it despite everything missing. If you can name three core workflows, you have too many. Push notifications, IAP, social login past Google and Apple, offline mode: all cheap in v2, all expensive forced into v1.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 30-day playbook
&lt;/h2&gt;

&lt;p&gt;Assumes the AI-native path. Stretch the timeline substantially for the others.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Days 1–3.&lt;/strong&gt; One-page PRD: who is the user, what is the single workflow they will thank you for, what does month-one success look like. Show five potential users. Rewrite. Do not skip this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Days 4–6.&lt;/strong&gt; Feed the PRD in. Get the core workflow working end to end, polish absent. Test on your own phone via the preview QR. Click, describe the change, watch it happen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Days 7–10.&lt;/strong&gt; The unglamorous parts. Auth. Settings screen. Empty states. Error states. This is the difference between a demo and something that feels real.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Days 11–15.&lt;/strong&gt; Ten people using it. Not user tests, actual usage. Watch them. Note every hesitation. Do not defend your app.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Days 16–20.&lt;/strong&gt; TestFlight. Same ten plus twenty new ones from the target market.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Days 21–25.&lt;/strong&gt; Fix the crashes, confusing flows, and typos TestFlight surfaces. Add nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Days 26–30.&lt;/strong&gt; Submit to both stores. Apple typically clears in under two days, Google is often same-day, both vary. Screenshots and the first line of the description carry most of the conversion weight.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part engineers actually care about
&lt;/h2&gt;

&lt;p&gt;The reason AI-native builders are worth taking seriously and no-code platforms mostly are not is portability. There is no proprietary runtime and no visual-logic language that exists inside one tool. The exported project is a normal Expo app. You can open it, read it, and extend it without a migration project.&lt;/p&gt;

&lt;p&gt;That means the founder who shipped without you in month one is not handing you a rewrite in month six. They are handing you a codebase.&lt;/p&gt;

&lt;p&gt;Which is a much better conversation to have.&lt;/p&gt;




&lt;p&gt;If you have inherited an app a non-technical founder shipped solo, how did that handoff actually go? Curious whether the "no rewrite required" claim holds up in practice or whether you all have war stories. Drop them in the comments.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>startup</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Reanimated vs Moti vs Skia: stop picking one</title>
      <dc:creator>Mark F A</dc:creator>
      <pubDate>Mon, 31 Aug 2026 11:01:49 +0000</pubDate>
      <link>https://dev.to/mark_fa/reanimated-vs-moti-vs-skia-stop-picking-one-4n7m</link>
      <guid>https://dev.to/mark_fa/reanimated-vs-moti-vs-skia-stop-picking-one-4n7m</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reanimated&lt;/strong&gt; runs animation math on the UI thread via worklets. It is the foundation, and you will end up with it installed regardless.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Moti&lt;/strong&gt; is a declarative wrapper over Reanimated with a Framer Motion-style API. Great for enter/exit, skeletons, staggered lists.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skia&lt;/strong&gt; is Shopify's binding to Google's 2D graphics engine. Paths, shaders, blurs. Not an animation library.&lt;/li&gt;
&lt;li&gt;These are not competitors. They are three layers of one stack, and most production apps use at least two.&lt;/li&gt;
&lt;li&gt;Skia props accept Reanimated shared values directly, with no wrapper component. That single fact is why the combination is everywhere.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The framing problem
&lt;/h2&gt;

&lt;p&gt;Every "X vs Y vs Z" post assumes you are picking one. With these three that assumption is wrong, and it produces two specific failure modes: teams that write 40 lines of Reanimated hooks to fade in a card, and teams that reach for Moti on a pan gesture and then cannot bind it to anything.&lt;/p&gt;

&lt;p&gt;Here is what each one actually is at the runtime level.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reanimated: worklets on the UI thread
&lt;/h2&gt;

&lt;p&gt;Animations that run on the JS thread stutter whenever your app does work. Parsing JSON, rendering a list, handling touch. Reanimated moves the math onto the UI thread using worklets, small JS functions the runtime serializes and executes natively through JSI.&lt;/p&gt;

&lt;p&gt;Three pieces:&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;Animated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;useSharedValue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;useAnimatedStyle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;withSpring&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-reanimated&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Card&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;offset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSharedValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;style&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useAnimatedStyle&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;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;translateX&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;offset&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="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;Animated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onTouchEnd&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="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;withSpring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;useSharedValue&lt;/code&gt; is a reactive primitive living on the UI thread, readable and writable from both sides. &lt;code&gt;useAnimatedStyle&lt;/code&gt; is a worklet that re-runs whenever a shared value it reads changes. &lt;code&gt;withTiming&lt;/code&gt; and &lt;code&gt;withSpring&lt;/code&gt; drive the value over time.&lt;/p&gt;

&lt;p&gt;Updates are scheduled against display frames rather than the JS event loop. That is why the animation stays smooth while your JS thread renders a 50-item list.&lt;/p&gt;

&lt;p&gt;Wire it to a gesture and you get the thing Reanimated is actually for:&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;Gesture&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;GestureDetector&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-gesture-handler&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;pan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Gesture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Pan&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;changeX&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;GestureDetector&lt;/span&gt; &lt;span class="na"&gt;gesture&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;pan&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;Animated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;style&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;GestureDetector&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;runOnJS&lt;/code&gt;, no bridge hop. The gesture callback is a worklet and it mutates a UI-thread value in the same frame.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The cost:&lt;/strong&gt; ceremony. Shared values, worklet directives on nested functions, &lt;code&gt;useAnimatedStyle&lt;/code&gt; for every animated view. It is more machinery than a fade-in deserves.&lt;/p&gt;

&lt;h2&gt;
  
  
  Moti: the same thing, four lines
&lt;/h2&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;MotiView&lt;/span&gt;
  &lt;span class="na"&gt;from&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;opacity&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;translateY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;animate&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;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;translateY&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="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;exit&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;opacity&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="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;transition&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;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;timing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under the hood this is Reanimated. Same worklets, same UI-thread execution. You just never touch &lt;code&gt;useSharedValue&lt;/code&gt; for the common cases. You also get &lt;code&gt;AnimatePresence&lt;/code&gt; for exits, &lt;code&gt;MotiText&lt;/code&gt; and &lt;code&gt;MotiImage&lt;/code&gt;, a &lt;code&gt;&amp;lt;Skeleton /&amp;gt;&lt;/code&gt; loader, and &lt;code&gt;&amp;lt;Sequence /&amp;gt;&lt;/code&gt; for chaining.&lt;/p&gt;

&lt;p&gt;Staggering a list is where the ergonomics really show:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;items&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;item&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="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;MotiView&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;item&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;from&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;opacity&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;translateY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="na"&gt;animate&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;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;translateY&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="si"&gt;}&lt;/span&gt;
    &lt;span class="na"&gt;transition&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;delay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;))}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The cost:&lt;/strong&gt; the moment you need a live gesture driving the animation, you drop back to raw Reanimated. There is no way to bind a &lt;code&gt;MotiView&lt;/code&gt; prop to an ongoing pan translation. &lt;code&gt;useDynamicAnimation&lt;/code&gt; exists, but using it puts you back in imperative code and the abstraction stops paying rent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Skia: not an animation library at all
&lt;/h2&gt;

&lt;p&gt;It is the 2D engine behind Chrome, Flutter, and Android's rendering pipeline, exposed to RN through a declarative component API. You use it for what the view tree cannot express: paths, SkSL shaders, blurs, blend modes, gradients beyond &lt;code&gt;LinearGradient&lt;/code&gt;, text on curves.&lt;/p&gt;

&lt;p&gt;The detail that matters for this comparison:&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;Canvas&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Circle&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;@shopify/react-native-skia&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;useSharedValue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;withTiming&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-reanimated&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;cx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSharedValue&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;cx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;withTiming&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;600&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;Canvas&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;Circle&lt;/span&gt; &lt;span class="na"&gt;cx&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;cx&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;r&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"hotpink"&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;Canvas&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;Skia props accept Reanimated shared values directly. No &lt;code&gt;createAnimatedComponent&lt;/code&gt;, no &lt;code&gt;useAnimatedProps&lt;/code&gt;. The canvas re-renders on the UI thread as the value changes. That is the pattern under Victory Native XL, Reanimated Carousel, and basically every good animated chart in the ecosystem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The cost:&lt;/strong&gt; no Expo Go, so you need a dev client. Roughly 2 MB of native binary. And SkSL is its own idiom with a real learning curve.&lt;/p&gt;

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Reanimated&lt;/th&gt;
&lt;th&gt;Moti&lt;/th&gt;
&lt;th&gt;Skia&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Type&lt;/td&gt;
&lt;td&gt;Animation runtime&lt;/td&gt;
&lt;td&gt;Declarative wrapper&lt;/td&gt;
&lt;td&gt;2D graphics engine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runs on&lt;/td&gt;
&lt;td&gt;UI thread (worklets)&lt;/td&gt;
&lt;td&gt;UI thread (via Reanimated)&lt;/td&gt;
&lt;td&gt;UI thread (JSI)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best for&lt;/td&gt;
&lt;td&gt;Gestures, sequences&lt;/td&gt;
&lt;td&gt;Enter/exit, transitions&lt;/td&gt;
&lt;td&gt;Drawings, shaders&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Depends on&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Reanimated 2/3/4&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Approx. cost&lt;/td&gt;
&lt;td&gt;~150 KB JS + native&lt;/td&gt;
&lt;td&gt;~30 KB JS + Reanimated&lt;/td&gt;
&lt;td&gt;~2 MB native binary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Expo Go&lt;/td&gt;
&lt;td&gt;Yes (SDK 47+)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No, dev client&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maintainer&lt;/td&gt;
&lt;td&gt;Software Mansion&lt;/td&gt;
&lt;td&gt;Fernando Rojo&lt;/td&gt;
&lt;td&gt;Shopify&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What to reach for
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Library&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fade in a card on mount&lt;/td&gt;
&lt;td&gt;Moti&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Drive a value from a pan or pinch&lt;/td&gt;
&lt;td&gt;Reanimated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chain coordinated view animations&lt;/td&gt;
&lt;td&gt;Moti&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Animated chart with line and dots&lt;/td&gt;
&lt;td&gt;Skia + Reanimated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blur a background as a sheet drags up&lt;/td&gt;
&lt;td&gt;Skia + Reanimated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Layout animation on list add/remove&lt;/td&gt;
&lt;td&gt;Reanimated 4 &lt;code&gt;LinearTransition&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Skeleton loader while fetching&lt;/td&gt;
&lt;td&gt;Moti &lt;code&gt;&amp;lt;Skeleton /&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shader-based splash screen&lt;/td&gt;
&lt;td&gt;Skia&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The dependency graph makes composition cheap. Moti pulls in Reanimated, so the incremental cost of "adding" Reanimated once you have Moti is zero. Skia is the only real commit, and you pay for it once.&lt;/p&gt;

&lt;h2&gt;
  
  
  The performance thing nobody mentions
&lt;/h2&gt;

&lt;p&gt;All three run on the UI thread over the same JSI bridge, so raw frame timing is comparable for comparable work. Where it actually breaks in production is almost never the library. It is this pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// This will drop frames&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;style&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useAnimatedStyle&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;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;h&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="c1"&gt;// triggers layout every frame&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;

&lt;span class="c1"&gt;// This will not&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;style&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useAnimatedStyle&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;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;scaleY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;h&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="c1"&gt;// compositor only&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Animating a layout property schedules a layout pass on every frame and the "60fps animation" ends up queued behind it. All three libraries let you do this. If you are chasing a stutter, profile before you refactor.&lt;/p&gt;

&lt;h2&gt;
  
  
  What building an AI generator taught us
&lt;/h2&gt;

&lt;p&gt;We build &lt;a href="https://www.rapidnative.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=react-native-animation-libraries-reanimated-vs-moti-vs-skia" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt;, which turns natural-language prompts into React Native and Expo code. Picking the animation primitive without a human in the loop surfaced two things worth stealing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Users never say "gesture."&lt;/strong&gt; They say "make it feel snappy" or "add some polish." We default to Moti for appearance verbs (fade, slide, pop, stagger), escalate to Reanimated the moment a prompt implies user-driven motion (drag, swipe, pull to refresh, pinch), and only pull in Skia for charts, shaders, gradients, or explicitly artistic phrasing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Composition beats picking a tool.&lt;/strong&gt; The worst early bug was choosing one library per screen. A screen with a draggable card and an animated hero wants Reanimated for the drag and Moti for the hero. Separate concerns, separate primitives.&lt;/p&gt;

&lt;p&gt;That generalizes past AI. Even when a human is choosing, the either/or framing costs you.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Is Moti just Reanimated with less code?&lt;/strong&gt; Mostly. Same worklets, same UI-thread execution, same shared values internally. Moti adds prop-based config, &lt;code&gt;AnimatePresence&lt;/code&gt;, sequence orchestration, and prebuilt components.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use Skia without Reanimated?&lt;/strong&gt; Yes, for static scenes and animations driven by its own value primitives. Almost nobody does, because shared values are the standard driver everywhere else in the app.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does Reanimated 4 replace Moti?&lt;/strong&gt; No. The CSS-like syntax narrows the ergonomic gap, but &lt;code&gt;AnimatePresence&lt;/code&gt; and the declarative prop API are still more concise for common transitions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What about the built-in &lt;code&gt;Animated&lt;/code&gt; API?&lt;/strong&gt; Wrong default for new code. JS thread by default, &lt;code&gt;useNativeDriver&lt;/code&gt; covers only a subset of transforms and opacity, verbose interpolation, painful gesture integration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom line
&lt;/h2&gt;

&lt;p&gt;Install Reanimated and Moti. Leave Skia until the design brief demands it. If you are auditing an existing codebase, the two things you will find are Moti calls that should be Reanimated (wired to a gesture later, never rewritten) and Reanimated hooks that should be Moti (fading a card in, 30 unnecessary lines).&lt;/p&gt;

&lt;p&gt;Pick the primitive per interaction, not per app.&lt;/p&gt;

&lt;p&gt;What is your current animation stack, and have you hit the point where you needed to mix them? Drop a comment with what you are building.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>animation</category>
      <category>javascript</category>
    </item>
    <item>
      <title>The Complete Guide to Push Notifications in React Native (2026)</title>
      <dc:creator>Mark F A</dc:creator>
      <pubDate>Wed, 19 Aug 2026 05:16:15 +0000</pubDate>
      <link>https://dev.to/rapidnative-ai/the-complete-guide-to-push-notifications-in-react-native-2026-5bi0</link>
      <guid>https://dev.to/rapidnative-ai/the-complete-guide-to-push-notifications-in-react-native-2026-5bi0</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;expo-notifications&lt;/code&gt; in 2026, even in a bare React Native workflow. CNG makes the managed/bare distinction mostly irrelevant.&lt;/li&gt;
&lt;li&gt;The token identifies a &lt;strong&gt;device plus install&lt;/strong&gt;, not a user. Plan for rotation from day one.&lt;/li&gt;
&lt;li&gt;On Android you must create a notification channel &lt;strong&gt;before&lt;/strong&gt; requesting permission, or notifications silently never display.&lt;/li&gt;
&lt;li&gt;Android 13+ needs the &lt;code&gt;POST_NOTIFICATIONS&lt;/code&gt; runtime permission, and only if your target SDK is 33 or higher.&lt;/li&gt;
&lt;li&gt;Never call the system permission prompt cold. It's one-shot. Put a screen you own in front of it.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;react-native-push-notification&lt;/code&gt; (Zo0r) is dead. Don't start a new project on it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Push notifications are the single most reliable way to bring a user back into a mobile app. Localytics data pegs day-90 retention at roughly 190 percent higher for apps that use them well, and Airship's 2024 benchmarks show median direct-open rates north of 4 percent across most verticals. Yet notifications are also the feature most React Native teams postpone the longest, because the surface spans two operating systems, two transport providers (APNs and FCM), a set of iOS entitlements that require an Apple Developer account, an Android notification-channel model that changed twice in the last five years, and at least three distinct app states you have to handle differently.&lt;/p&gt;

&lt;p&gt;This guide is the version I wish I'd had when I first shipped push in a React Native app. It walks through the full stack (permissions, tokens, sending, receiving, deep linking, rich content, and testing) using the Expo Notifications SDK, which works for both Expo-managed and bare React Native projects on modern Expo SDK versions. Every code sample runs. Every gotcha is one I've actually hit in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "push notifications" actually means
&lt;/h2&gt;

&lt;p&gt;A push notification is a message your &lt;strong&gt;server&lt;/strong&gt; hands to a &lt;strong&gt;push service&lt;/strong&gt; (APNs for Apple, FCM for Google) which then delivers it to a specific device using a device-specific token. The device wakes up even if your app is killed, OS-level code decodes the payload, and either displays a system UI or fires an event into your app process.&lt;/p&gt;

&lt;p&gt;Three things follow from that definition, and they cause most of the confusion:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;You cannot send a push directly from your React Native app to another user.&lt;/strong&gt; You need a server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The token identifies a device plus app install&lt;/strong&gt;, not a user. When a user reinstalls, logs into a second device, or clears app data, you get a new token. Tokens also rotate for other reasons and can be invalidated by the push service.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What runs inside your app is only half the story.&lt;/strong&gt; The other half is Apple's or Google's OS-level notification presentation logic, which you configure through payload keys, notification channels, and iOS entitlements.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Local notifications, the kind you schedule from inside the app without a server, use most of the same APIs but skip the token and the push service. This guide covers both, since real apps almost always need both.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 2026 landscape: which library
&lt;/h2&gt;

&lt;p&gt;There are three real options for React Native in 2026, and the honest recommendation is: &lt;strong&gt;use &lt;code&gt;expo-notifications&lt;/code&gt;&lt;/strong&gt;, even if you're not on Expo Go and even if you use a bare React Native workflow.&lt;/p&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;Trade-offs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;expo-notifications&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Almost every app&lt;/td&gt;
&lt;td&gt;First-class support in both Expo-managed and bare RN via CNG (Continuous Native Generation). Handles APNs and FCM transparently. Actively maintained.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;@react-native-firebase/messaging&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Apps that already use other Firebase products (Firestore, Auth, Analytics) heavily&lt;/td&gt;
&lt;td&gt;Google-only transport story; you still need a separate iOS setup for APNs credentials.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;react-native-notifications&lt;/code&gt; (Wix)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Advanced native-side customization needs&lt;/td&gt;
&lt;td&gt;Smaller community; more manual native config.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The legacy &lt;code&gt;react-native-push-notification&lt;/code&gt; package (Zo0r) is unmaintained and should not be used in a new project.&lt;/p&gt;

&lt;p&gt;The rest of this guide uses &lt;code&gt;expo-notifications&lt;/code&gt;. Because Expo now supports prebuild (&lt;code&gt;npx expo prebuild&lt;/code&gt;) and CNG, you get the same experience whether your project is Expo-managed or bare: the config plugin generates the correct native code for both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing and configuring
&lt;/h2&gt;

&lt;p&gt;Assuming an Expo SDK 52+ project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx expo &lt;span class="nb"&gt;install &lt;/span&gt;expo-notifications expo-device expo-constants
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;expo-device&lt;/code&gt; is used to skip token registration on simulators. Apple's push service does not deliver to the iOS simulator on Xcode versions below 14, and even on newer Xcode you need a paid developer account and the Simulator's push testing tools. &lt;code&gt;expo-constants&lt;/code&gt; gives you access to &lt;code&gt;easConfig.projectId&lt;/code&gt;, which the Expo Push Service needs to route tokens.&lt;/p&gt;

&lt;p&gt;Add the config plugin 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;"plugins"&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="s2"&gt;"expo-notifications"&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;"icon"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./assets/notification-icon.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"color"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"#111827"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"sounds"&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="s2"&gt;"./assets/notification-sound.wav"&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;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;"bundleIdentifier"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"com.yourco.yourapp"&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;"UIBackgroundModes"&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="s2"&gt;"remote-notification"&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="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;"package"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"com.yourco.yourapp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"googleServicesFile"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./google-services.json"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The notification icon must be a &lt;strong&gt;monochrome PNG with a transparent background&lt;/strong&gt;. Android's icon guidelines are strict, and a full-color icon renders as a white square.&lt;/li&gt;
&lt;li&gt;On iOS, you need to enable the &lt;strong&gt;Push Notifications capability&lt;/strong&gt; and the &lt;strong&gt;Background Modes to Remote notifications&lt;/strong&gt; capability in your Apple Developer account. If you're using EAS Build, &lt;code&gt;expo-notifications&lt;/code&gt; and the &lt;code&gt;UIBackgroundModes&lt;/code&gt; config above handle this automatically.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're standing up a new project rather than retrofitting an existing one, starting from a config that already builds saves an afternoon. &lt;a href="https://www.rapidnative.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=react-native-push-notifications-complete-guide-2026" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt; generates Expo projects with &lt;code&gt;app.json&lt;/code&gt;, the bundle identifier, and the Android package name already in place, so you're editing a working config instead of assembling one from an empty file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Requesting permission the right way
&lt;/h2&gt;

&lt;p&gt;This is where a lot of implementations lose would-be opt-ins. Apple's system prompt is a one-shot event: if the user taps "Don't Allow," you cannot re-prompt from your app. They would have to go into Settings.&lt;/p&gt;

&lt;p&gt;The pattern that works is a &lt;strong&gt;pre-permission screen&lt;/strong&gt;: a screen you own, explaining the value, with a "Turn on notifications" button. Only when the user taps that button do you call the system API.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Notifications&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-notifications&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;Device&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-device&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;Constants&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;expo-constants&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Platform&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-native&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;registerForPushNotifications&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&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;Device&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isDevice&lt;/span&gt;&lt;span class="p"&gt;)&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;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Push notifications require a physical device.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="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;Platform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OS&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;android&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;await&lt;/span&gt; &lt;span class="nx"&gt;Notifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setNotificationChannelAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;default&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;default&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;importance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Notifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AndroidImportance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;vibrationPattern&lt;/span&gt;&lt;span class="p"&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="mi"&gt;250&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;250&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;250&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;lightColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#111827&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;existing&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;Notifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getPermissionsAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;finalStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;existing&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;existing&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;granted&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Notifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;requestPermissionsAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;finalStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;finalStatus&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;granted&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;projectId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Constants&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;expoConfig&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;extra&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;eas&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;projectId&lt;/span&gt;
    &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;Constants&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;easConfig&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;projectId&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="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Notifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getExpoPushTokenAsync&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;projectId&lt;/span&gt; &lt;span class="p"&gt;})).&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few details that matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Android 13+ (API 33) requires runtime permission&lt;/strong&gt; via &lt;code&gt;POST_NOTIFICATIONS&lt;/code&gt;. &lt;code&gt;expo-notifications&lt;/code&gt; handles this for you when you call &lt;code&gt;requestPermissionsAsync()&lt;/code&gt;, but only if your target SDK is 33 or higher.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On Android, you must create a notification channel&lt;/strong&gt; before you request permission. Otherwise notifications will silently fail to display, even though your token is valid. Channels group notifications so users can mute categories independently in system settings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You want the Expo push token, not the native APNs or FCM token&lt;/strong&gt;, unless you're skipping Expo's push service. The Expo token is a single string of the form &lt;code&gt;ExponentPushToken[xxxxxxxxxx]&lt;/code&gt; that Expo's service translates to either APNs or FCM at send time.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;The setup above gets you a valid token and a permission prompt users actually accept. The next layer is the one that decides whether push works in production: sending from your server, handling the payload in all three app states (foreground, background, killed), routing a tap to the right screen, and testing all of it on real hardware.&lt;/p&gt;

&lt;p&gt;What broke first when you shipped push? My money is on the Android channel. Drop yours in the comments.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>javascript</category>
      <category>mobile</category>
    </item>
    <item>
      <title>From Side Project to App Store: A Non-Technical Founder's Guide (2026)</title>
      <dc:creator>Mark F A</dc:creator>
      <pubDate>Fri, 14 Aug 2026 06:59:12 +0000</pubDate>
      <link>https://dev.to/mark_fa/from-side-project-to-app-store-a-non-technical-founders-guide-2026-387i</link>
      <guid>https://dev.to/mark_fa/from-side-project-to-app-store-a-non-technical-founders-guide-2026-387i</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The distance between "I have an app idea" and "it's live on the App Store" collapsed in 2026.&lt;/li&gt;
&lt;li&gt;Stack: AI mobile app builder producing real exportable React Native code, Supabase for data, TestFlight and Play Internal Testing for QA, self-submission to both stores.&lt;/li&gt;
&lt;li&gt;Validate with 10 real interviews and a landing page &lt;em&gt;before&lt;/em&gt; you build anything.&lt;/li&gt;
&lt;li&gt;Budget one week for submission. Most rejections are mechanical: privacy label mismatches, placeholder content, first-minute crashes.&lt;/li&gt;
&lt;li&gt;Retention beats downloads. Day 30 above 20% means you have something.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Somewhere between 70 and 80 percent of side projects never leave a Notion doc. Not because the ideas are bad. Because the distance between "I have an idea for an app" and "my app is live on the App Store" has always been enormous for anyone who can't code. Hiring a developer costs $20,000 to $80,000. Learning React Native takes six months. Cloning boilerplate templates leaves you stuck the moment you want anything custom.&lt;/p&gt;

&lt;p&gt;That distance collapsed in 2026. A non-technical founder can now go from a napkin sketch to a live App Store listing in a matter of weeks, working alone, without writing a line of code, and without shipping a fragile prototype held together with duct tape.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack has finally shifted
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What is the fastest way for a non-technical founder to build a mobile app in 2026?&lt;/strong&gt; Use an AI mobile app builder to generate production-ready React Native code from a prompt, sketch, or PRD; back it with a managed database like Supabase; test with TestFlight and Google Play Internal Testing; and submit to both stores yourself. The whole cycle takes weeks, not months, and the code you own is real code, not a walled-garden template.&lt;/p&gt;

&lt;p&gt;The old paths were expensive, slow, or extractive. Hire an agency and you were out $30k before a single user tapped the icon. Hire a "technical co-founder" and you gave away 40% of your company for someone who could've been replaced by tooling that didn't exist yet. Learn to code and you spent your first year fighting Xcode installation errors instead of talking to users.&lt;/p&gt;

&lt;p&gt;The new stack does three things the old one couldn't:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Generates working, exportable code.&lt;/strong&gt; React Native and Expo apps you can own, edit, and ship, not throwaway prototypes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handles the full stack.&lt;/strong&gt; Screens, navigation, forms, auth, database, and API integration from one generation pipeline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runs a real preview.&lt;/strong&gt; Hold your phone up to a QR code and use your app on iOS the same day you started.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The bottleneck used to be code. Now the bottleneck is your ability to make product decisions, which is exactly the work a founder should be doing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 1: Validate without writing a line of code
&lt;/h2&gt;

&lt;p&gt;One to two weeks. Three artifacts: a landing page with a signup form, 10 completed user interviews, and a written product spec.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The 10-user test.&lt;/strong&gt; Pick 10 people who would actually pay for or use your app. Not friends. Not "the whole world." Ten specific humans. Ask them: "Walk me through the last time you tried to solve [problem]. What did you use? What did you hate about it?" If you can't find 10 people who describe the problem the same way you do, you don't have a product. You have a hunch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Landing page and waitlist.&lt;/strong&gt; Spin up a one-pager on Framer, Carrd, or Webflow. Value prop, three screenshots (mock them in Figma if you have to), and a "get early access" form. Push it in one Slack community, one subreddit, and one Twitter thread. If nobody signs up, that's your answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Write a real PRD.&lt;/strong&gt; Not a novel. One page listing: the target user, the top three screens, the data each screen shows, the actions the user can take, and what happens after each action. This becomes the input for the next phase.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 2: Build the first working version
&lt;/h2&gt;

&lt;p&gt;This is where non-technical founders used to get stuck for six months. Now it takes days.&lt;/p&gt;

&lt;p&gt;Open an AI mobile app builder. Describe your app in plain English: "A workout tracker where users log sets and reps, see weekly volume, and get streak badges." Watch it generate a working Expo app with screens, navigation, forms, and a database schema. Scan the QR code with your phone. Use the app. Notice five things you want to change. Describe them. Regenerate. Done in an afternoon what would have taken a contractor a month.&lt;/p&gt;

&lt;p&gt;Under the hood, the good builders run a multi-step pipeline: interpret the prompt, plan a component tree, generate React Native code, wire up state with Redux or Context, apply theming with NativeWind, and stream the result into a live preview you can interact with in real time. You don't need to know any of that, but knowing it exists is what separates "toy prototype" from "actual app I can submit to Apple."&lt;/p&gt;

&lt;p&gt;Four input modes are worth knowing about:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Prompt to app&lt;/td&gt;
&lt;td&gt;Describe it, get it&lt;/td&gt;
&lt;td&gt;Founders who know what they want&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sketch to app&lt;/td&gt;
&lt;td&gt;Draw wireframes, get working screens&lt;/td&gt;
&lt;td&gt;Ideas more visual than verbal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PRD to app&lt;/td&gt;
&lt;td&gt;Paste your spec, get a full prototype&lt;/td&gt;
&lt;td&gt;Founders who already wrote the doc&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Screenshot to app&lt;/td&gt;
&lt;td&gt;Upload a UX you like, generate something similar&lt;/td&gt;
&lt;td&gt;Cloning proven patterns&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://www.rapidnative.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=side-project-to-app-store-non-technical-founders-guide" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt; supports all four and exports a real Expo project. Pick the mode that matches how the idea actually lives in your head.&lt;/p&gt;

&lt;p&gt;The trick in this phase is &lt;strong&gt;ruthless scope-cutting&lt;/strong&gt;. Every non-technical founder wants push notifications, in-app purchases, social login, offline mode, and dark mode on day one. Build none of that. Notifications and IAP are two-week rabbit holes each, and every screen you add is a screen you'll redesign after your first ten users.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 3: Test on TestFlight and Play Internal Testing
&lt;/h2&gt;

&lt;p&gt;Most founders skip this. Don't. A demo in the simulator is not a real test.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Apple TestFlight.&lt;/strong&gt; Free, ships from your Apple Developer account, distributes to up to 10,000 external testers via email or a public link. Upload a build (EAS handles this), fill out a short test information form, invite testers. Apple runs a brief review on your first build, usually under 24 hours. After that, updates are near-instant.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Google Play Internal Testing.&lt;/strong&gt; Also free, ships from Play Console, supports up to 100 internal testers, distributed by Google account email. More forms than Apple up front, but review is essentially zero. Internal builds go live within minutes.&lt;/p&gt;

&lt;p&gt;Send invites to your 10 interviewees from Phase 1. After a week of use, ask each of them: &lt;em&gt;What did you use it for? What was confusing? Would you pay $5/month for this?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Instrument analytics before you send it out. Expo has PostHog and Mixpanel SDKs that take five minutes to integrate. You want to know which screens users open, where they drop off, and how long sessions last. This data is worth ten times what people tell you in interviews.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 4: The submission gauntlet
&lt;/h2&gt;

&lt;p&gt;The phase founders fear most and, frankly, the one that trips them up least. Budget one week for both stores.&lt;/p&gt;

&lt;h3&gt;
  
  
  Apple App Store
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Apple Developer Program&lt;/strong&gt;: $99/year. Individual account is fine to start.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;App Store Connect listing&lt;/strong&gt;: name (30 chars), subtitle (30 chars), keywords (100 chars, comma-separated, invisible to users), description, screenshots (6.7" and 6.5" iPhone minimum), promotional text, support URL, privacy policy URL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privacy nutrition label&lt;/strong&gt;: declare what data you collect, what links to the user, and what's used for tracking. This is the #1 rejection reason for solo founders.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;App Tracking Transparency&lt;/strong&gt;: required if you use third-party analytics that fingerprints devices.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review time&lt;/strong&gt;: around 24 hours for 90% of submissions in 2026. Complex or first-time submissions can take 2–4 days.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Google Play Store
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Developer account&lt;/strong&gt;: $25 one-time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Play Console listing&lt;/strong&gt;: title (30 chars), short description (80 chars), full description (4000 chars), feature graphic, screenshots, category, content rating questionnaire.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data Safety form&lt;/strong&gt;: Google's equivalent of Apple's privacy label. Mismatches with actual behavior get apps pulled later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Target API level&lt;/strong&gt;: last two API levels required. Modern Expo apps handle this automatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review time&lt;/strong&gt;: a few hours to a couple of days for new apps, faster for updates.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Three most common rejections for solo-founder apps: placeholder content still visible ("Lorem ipsum" screens are an instant reject), crashes in the first minute, and misleading metadata. Ship a build you've used yourself for a full week.&lt;/p&gt;

&lt;p&gt;Your builder should export a complete Expo project you can submit yourself with EAS or hand to a developer for final packaging. This is where owning real, exportable code matters. You're not trapped inside a proprietary builder that can't produce an actual &lt;code&gt;.ipa&lt;/code&gt; or &lt;code&gt;.aab&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 5: Launch and beyond
&lt;/h2&gt;

&lt;p&gt;Getting into the store is not the launch. It's the starting line.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Soft launch first.&lt;/strong&gt; Go live in one small market (Canada and Australia are common) for two weeks before opening globally. Watch crash rates, retention, and reviews. Fix what breaks. Then flip the geo restriction off.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pick one launch channel and go deep.&lt;/strong&gt; Product Hunt, a niche subreddit, an X thread, or an outreach email to 50 users. Not all four in the same day. Product Hunt in particular rewards launches that already have engagement, so warm your list before you post.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Instrument retention, not vanity metrics.&lt;/strong&gt; Downloads are meaningless. Day 1, Day 7, and Day 30 retention are the numbers that matter. Day 30 above 20% for a consumer app means you have something worth pushing. Below 10% and you iterate on the product, not the marketing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Iterate fast.&lt;/strong&gt; Every piece of feedback becomes a prompt. "The signup flow is confusing" becomes a two-minute edit and a new build on TestFlight the same afternoon. Founders who take a week to ship a fix lose to founders who take two hours.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ASO basics.&lt;/strong&gt; Optimize your app name and subtitle around one high-intent keyword each. Use all 100 characters of the App Store keyword field. Ship screenshots that show the value in the first two frames, since that's what users see on the search page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five pitfalls to avoid
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Building too much before showing anyone.&lt;/strong&gt; Every screen you build without a user in front of it is a screen you'll rebuild. Ship five screens to ten users before you build the sixth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ignoring review guidelines.&lt;/strong&gt; Read them once before you submit. The mechanical rejections (privacy label mismatches, missing account-deletion flow, sign-in-with-Apple requirement when you have social login) are easy to avoid and painful to hit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No maintenance plan.&lt;/strong&gt; Apple and Google both push OS updates yearly. If you can't ship at least one build every 6 months, your app will start breaking. A maintenance update in 2026 is a 30-minute regeneration, not a week of contractor work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Undervaluing yourself on price.&lt;/strong&gt; Free apps monetize on scale you don't have yet. $4.99/month or $29/year is a common indie starting price and it filters out users who were never going to convert.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skipping the legal basics.&lt;/strong&gt; Both stores require a privacy policy URL. Termly or Iubenda cost $10–$20 and take 15 minutes. Do it before you submit, not after.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What shipping alone actually looks like now
&lt;/h2&gt;

&lt;p&gt;The old founder journey: idea, then learn to code or raise money or find a co-founder, then build for a year, then ship a bad version, then run out of runway.&lt;/p&gt;

&lt;p&gt;The new one: idea, validate in a week, build in a week, test in two weeks, submit in a week, iterate forever. The whole cycle from opening a browser to a live App Store listing can compress into a single month.&lt;/p&gt;

&lt;p&gt;You still need judgement: what to build, what to cut, who to talk to, how to price. Those are the parts that make you a founder. Everything else has become tooling.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;What phase are you stuck in right now?&lt;/strong&gt; Validation, building, testing, or submission? Drop it in the comments. Curious how many of you are sitting on a Notion doc that's been there over a year.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>startup</category>
      <category>indiehackers</category>
    </item>
    <item>
      <title>How to Build a HIPAA-Compliant App: A Founder's Guide for 2026</title>
      <dc:creator>Mark F A</dc:creator>
      <pubDate>Wed, 12 Aug 2026 07:09:30 +0000</pubDate>
      <link>https://dev.to/rapidnative-ai/how-to-build-a-hipaa-compliant-app-a-founders-guide-for-2026-ha0</link>
      <guid>https://dev.to/rapidnative-ai/how-to-build-a-hipaa-compliant-app-a-founders-guide-for-2026-ha0</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;There is no HIPAA certification.&lt;/strong&gt; No stamp, no badge. It's a continuous state you maintain through contracts, controls, and documentation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Six technical pillars carry most of the weight:&lt;/strong&gt; encryption at rest and in transit, unique auth with MFA, server-enforced RBAC, tamper-evident audit logs, automatic session logoff, and a signed BAA with every vendor that touches PHI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing BAAs are the most common root cause of HIPAA breaches.&lt;/strong&gt; They're also the easiest thing for an auditor to check.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You can use LLMs with PHI in 2026&lt;/strong&gt;, but only with providers that offer a BAA, on the right plan, with identifiers redacted before the call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The $60k-$300k agency number is real but not the only number.&lt;/strong&gt; A React Native and Supabase stack can hit a compliant beta in eight to twelve weeks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every healthcare founder eventually hits the same wall. You have a real idea, sometimes even paying pilot customers, and then a hospital CTO or a payer's legal team asks the question that stops the conversation: "Is your app HIPAA-compliant?"&lt;/p&gt;

&lt;p&gt;Suddenly the six-week MVP plan collides with a body of law from 1996, agencies quoting $150,000 to $300,000 for a "compliant build," and a vendor stack full of tools you love that quietly cannot legally touch patient data.&lt;/p&gt;

&lt;p&gt;Here's how you'd actually do it in 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "HIPAA-compliant" actually means
&lt;/h2&gt;

&lt;p&gt;HIPAA-compliant app development means every point where Protected Health Information (PHI) is created, stored, transmitted, or accessed is covered by administrative, physical, and technical safeguards defined by the HIPAA Security Rule. And every third-party vendor that touches PHI has signed a Business Associate Agreement (BAA) with you.&lt;/p&gt;

&lt;p&gt;Compliance is not a certification you buy. There is no "HIPAA-certified" stamp issued by HHS. What exists is the HIPAA Security Rule, the Privacy Rule, and the Breach Notification Rule, plus the Office for Civil Rights that investigates breaches and complaints. Your job is to demonstrate, with documentation, contracts, and technical controls, that you meet every applicable requirement on the day someone asks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three rules that shape your architecture
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The Privacy Rule&lt;/strong&gt; governs how PHI can be used and disclosed. It introduces the concept every product designer bumps into first: the &lt;strong&gt;minimum necessary standard&lt;/strong&gt;. Your app collects, displays, and transmits only the PHI required to do its job. If your telehealth app doesn't need a full address to run a video visit, don't ask for it. It also defines patient rights (access, amendment, accounting of disclosures) and every one of those maps to a screen or an endpoint you have to build.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Security Rule&lt;/strong&gt; is the one engineers live in. It covers electronic PHI and requires three safeguard categories:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Administrative:&lt;/strong&gt; risk assessments, workforce training, access management policies, incident response.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Physical:&lt;/strong&gt; facility access controls, workstation security, device and media disposal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Technical:&lt;/strong&gt; access controls, audit logs, integrity controls, transmission security. This is the layer your code implements.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The Breach Notification Rule&lt;/strong&gt; gives you a legal duty to notify affected individuals within 60 days, notify HHS, and in some cases notify media. "Unsecured" is the operative word. Properly encrypted PHI that gets stolen is generally treated as a much lower-risk event, which is the strongest practical argument that encryption is not optional.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 6 technical requirements that actually matter
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Encryption at rest and in transit.&lt;/strong&gt; AES-256 server-side and on device. TLS 1.2+ with modern ciphers in flight. Hardware-backed key storage for anything the client persists.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unique user authentication with MFA.&lt;/strong&gt; Unique identifier per user, no shared logins, MFA for anyone accessing PHI. Biometrics count as a second factor on the client.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Role-based access control.&lt;/strong&gt; Patient sees their own record. Nurse sees their unit. Billing clerk sees charge codes but not clinical notes. Enforced on the server, never only in the UI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tamper-evident audit logging.&lt;/strong&gt; Every read and write of PHI logged with who, what, when, and from where. Logs protected from modification, retained six years.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automatic logoff and session controls.&lt;/strong&gt; Inactive sessions terminate. On mobile this means a background timer that clears the session and forces re-auth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Signed BAAs with every vendor that touches PHI.&lt;/strong&gt; The one most first-time healthcare founders miss.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;On mobile, requirement 1 mostly comes down to not persisting PHI in the wrong place:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// DON'T: AsyncStorage is plaintext on disk.&lt;/span&gt;
&lt;span class="c1"&gt;// On Android it's SharedPreferences XML, readable with adb.&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;AsyncStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;patient_mrn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mrn&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// DO: hardware-backed keychain/keystore, device-bound.&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;SecureStore&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;expo-secure-store&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;SecureStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItemAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;session_token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="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;keychainService&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;com.yourcompany.health.auth&lt;/span&gt;&lt;span class="dl"&gt;'&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;// gates read behind Face ID / biometric&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Better still: don't persist PHI on the device at all. Keep it in memory, fetch it per session, and let the server be the only durable store. That turns a lost-phone incident into a non-event.&lt;/p&gt;

&lt;p&gt;Requirement 5 is the one people forget until an auditor asks. A minimal version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// sessionTimer.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;AppState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-native&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TIMEOUT_MS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 15 min inactivity&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;backgroundedAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;AppState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;change&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;next&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;next&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;background&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;backgroundedAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;backgroundedAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;backgroundedAt&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;TIMEOUT_MS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;clearSession&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;       &lt;span class="c1"&gt;// wipe in-memory PHI&lt;/span&gt;
      &lt;span class="nf"&gt;navigateToLogin&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;    &lt;span class="c1"&gt;// force re-authentication&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;backgroundedAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pair that with a foreground inactivity timer reset on user interaction, and log both the timeout and the re-auth to your audit trail.&lt;/p&gt;

&lt;h2&gt;
  
  
  The vendor stack that will actually sign a BAA
&lt;/h2&gt;

&lt;p&gt;This is where most guides fail founders. They say "sign a BAA with your vendors" without saying which vendors will sign one, on which plan, at what price.&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;HIPAA-eligible option in 2026&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cloud infrastructure&lt;/td&gt;
&lt;td&gt;AWS (BAA free via AWS Artifact, ~150 eligible services), Google Cloud, Azure&lt;/td&gt;
&lt;td&gt;AWS is the most-used HIPAA cloud in 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database + auth + storage&lt;/td&gt;
&lt;td&gt;Supabase Team ($599/mo) + HIPAA add-on ($350/mo), or self-host&lt;/td&gt;
&lt;td&gt;Check current Supabase HIPAA docs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frontend (mobile)&lt;/td&gt;
&lt;td&gt;React Native / Expo, no PHI in the bundle&lt;/td&gt;
&lt;td&gt;Most common mobile framework for HIPAA apps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Email (transactional)&lt;/td&gt;
&lt;td&gt;AWS SES (BAA available), Paubox, LuxSci&lt;/td&gt;
&lt;td&gt;Not SendGrid on standard plans&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SMS / voice&lt;/td&gt;
&lt;td&gt;Twilio (HIPAA-eligible products only)&lt;/td&gt;
&lt;td&gt;Sign a BAA and configure specifically for HIPAA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Push notifications&lt;/td&gt;
&lt;td&gt;OneSignal (HIPAA plan), or AWS SNS&lt;/td&gt;
&lt;td&gt;Never put PHI in the notification body&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Error tracking&lt;/td&gt;
&lt;td&gt;Sentry (HIPAA tier), Datadog (HIPAA tier)&lt;/td&gt;
&lt;td&gt;Standard tiers do not qualify&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Analytics&lt;/td&gt;
&lt;td&gt;Server-side only, PHI-free events&lt;/td&gt;
&lt;td&gt;Google Analytics does not sign BAAs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI / LLM&lt;/td&gt;
&lt;td&gt;Anthropic (BAA available), AWS Bedrock, Vertex AI, Azure OpenAI&lt;/td&gt;
&lt;td&gt;See below&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two rules of thumb. If a vendor's marketing site doesn't say "HIPAA eligible" and their sales team can't produce a template BAA within a day, assume they can't. And HIPAA eligibility is almost always plan-specific, so the free tier you prototyped on almost certainly doesn't qualify.&lt;/p&gt;

&lt;h2&gt;
  
  
  Can you use LLMs with PHI?
&lt;/h2&gt;

&lt;p&gt;Yes, but only with specific providers, on specific plans, under a signed BAA.&lt;/p&gt;

&lt;p&gt;The major model providers now offer HIPAA-eligible tiers. Anthropic offers BAAs for enterprise customers using Claude. AWS Bedrock, Google Cloud Vertex AI, and Azure OpenAI all extend the underlying platform BAA to the models hosted on them. What you cannot do is ship PHI to a consumer API endpoint with no BAA. That's a breach the moment the request is sent, regardless of what the model does with the data.&lt;/p&gt;

&lt;p&gt;Even under a BAA, minimize what you send:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Redact identifiers server-side BEFORE the model call.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SAFE_FIELDS&lt;/span&gt; &lt;span class="o"&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;ageBand&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;sex&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;conditionCodes&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;medications&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;toModelPayload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;patient&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;SAFE_FIELDS&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;acc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;k&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;patient&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&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;span class="nx"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;patient&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&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;acc&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="c1"&gt;// Allow-list, not deny-list. A deny-list silently leaks&lt;/span&gt;
&lt;span class="c1"&gt;// every new field someone adds to the patient record.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Allow-list beats deny-list every time here. A deny-list quietly leaks whatever field a teammate adds next sprint. And log every prompt and response as part of your audit trail. If your AI features are purely non-PHI (educational content, appointment reminders with no diagnosis), you can often keep them on your regular non-BAA stack, as long as you can prove PHI never crosses that boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  The build checklist
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Data classification.&lt;/strong&gt; List every data element. Tag each PHI, non-PHI, or de-identified. Remove fields and prefer tokens over raw values.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Written risk assessment.&lt;/strong&gt; A Security Rule requirement, not a nice-to-have. Threats, likelihoods, mitigations. Update annually and on material architecture changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick your HIPAA-eligible stack.&lt;/strong&gt; Sign BAAs before a single byte of PHI reaches any vendor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Implement the six pillars.&lt;/strong&gt; Encryption, MFA, server-side RBAC, audit logs with six-year retention, session logoff, BAAs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secure SDLC.&lt;/strong&gt; Code review, SAST/DAST, dependency pinning, supply chain controls. First thing a serious buyer's security team asks about.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Train your workforce.&lt;/strong&gt; Security Awareness Training for anyone who could touch PHI. Keep records: dates, curricula, attendees.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Policies and incident response plan.&lt;/strong&gt; Breach notification, sanctions, contingency, business continuity. Start from a template, then customize.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Third-party pen test before launch.&lt;/strong&gt; Not required by HIPAA, but every hospital and enterprise buyer will ask for a recent report.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privacy policy and Notice of Privacy Practices.&lt;/strong&gt; User-facing documents, not just legal artifacts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Continuous compliance.&lt;/strong&gt; Quarterly access reviews, monthly log reviews, annual risk reassessment.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What it costs and how long it takes
&lt;/h2&gt;

&lt;p&gt;Every agency guide says $60,000 to $300,000 and six to twelve months. That's real for a bespoke, agency-built telemedicine app with EHR integration. It's not the only number.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Baseline:&lt;/strong&gt; whatever a non-HIPAA build of the same feature set would cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add 15-25%&lt;/strong&gt; for compliance-specific engineering: audit logging, RBAC, session management, encryption plumbing, admin console for access reviews.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vendor upcharge:&lt;/strong&gt; roughly $1,000-$3,000/month at MVP scale once you're on HIPAA tiers across the board.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compliance program:&lt;/strong&gt; policies, risk assessment, training, pen test, BAA legal review. Budget $10,000-$30,000 year one, roughly half that annually after.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Timelines compress the same way. A modern React Native and Supabase HIPAA stack, with the front-end scaffolded in an AI-assisted tool like &lt;a href="https://www.rapidnative.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=how-to-build-hipaa-compliant-app" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt;, can get you from zero to a working compliant beta in eight to twelve weeks. Worth being precise about the boundary, though: a code generation tool accelerates your front-end and scaffolding, not your compliance program. No AI code tool signs a BAA with you, because it doesn't touch your production PHI. And you should not paste PHI into any prompt during development. Scaffold with realistic-but-synthetic data and wire the real backend in your own environment.&lt;/p&gt;

&lt;p&gt;What no tool can compress is the compliance program itself: writing your risk assessment, executing your BAAs, getting the pen test scheduled. Start those in parallel with engineering, not after.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Is React Native suitable for HIPAA-compliant apps?&lt;/strong&gt; Yes. Single codebase to native iOS and Android, access to Keychain and Keystore for hardware-backed encryption, biometric APIs for MFA, same TLS stack as native. HIPAA compliance is a property of your architecture and vendor stack, not your mobile framework.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need a BAA with Apple or Google to publish?&lt;/strong&gt; No. They don't process PHI on your behalf when they distribute your app. You do need to follow their health data policies, and you need BAAs with every cloud, analytics, notification, and backend vendor that actually handles PHI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use Firebase?&lt;/strong&gt; Partially. Firebase Auth, Cloud Firestore, and Cloud Functions on Blaze are covered under the Google Cloud BAA when configured correctly. Firebase Analytics, Crashlytics on defaults, and Cloud Messaging with PHI payloads are not. Confirm each service against Google Cloud's current list.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens in a breach?&lt;/strong&gt; Notify affected individuals within 60 days, notify HHS (immediately for 500+ people, otherwise annually), and in some cases notify media in the affected state. OCR fines range from $137 to over $2 million per violation depending on culpability. Encrypted PHI that's lost is generally a much lower-risk event.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need HIPAA if only patients use my app?&lt;/strong&gt; If you collect health data directly from consumers and aren't acting on behalf of a covered entity, you may not technically be subject to HIPAA. But you're almost certainly subject to the FTC Health Breach Notification Rule, state laws (California's CMIA, Washington's My Health My Data Act), and user expectations. Most direct-to-consumer health apps build to HIPAA-equivalent standards anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part most teams get wrong
&lt;/h2&gt;

&lt;p&gt;HIPAA has a reputation for being expensive and slow because most teams treat compliance as a phase at the end of the project. It isn't. It's an architectural constraint you build in from day one, and once it's in your foundation the incremental cost is much smaller than the guides suggest.&lt;/p&gt;

&lt;p&gt;Scaffold fast so you get to testable UI in days. Choose a HIPAA-eligible stack from the start. Get BAAs signed while engineers are still building. Write the risk assessment early, not the week before your first customer's security review.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;What's your stack?&lt;/strong&gt; Drop it in the comments and I'll flag which pieces will fail an audit. And if you've shipped a HIPAA app already, I want to know which vendor surprised you most by refusing (or agreeing) to sign a BAA.&lt;/p&gt;

</description>
      <category>hipaa</category>
      <category>reactnative</category>
      <category>healthcare</category>
      <category>startup</category>
    </item>
    <item>
      <title>React Native Authentication in 2026: The Refresh-Token Pattern That Actually Scales</title>
      <dc:creator>Mark F A</dc:creator>
      <pubDate>Wed, 12 Aug 2026 06:03:55 +0000</pubDate>
      <link>https://dev.to/mark_fa/react-native-authentication-in-2026-the-refresh-token-pattern-that-actually-scales-4i74</link>
      <guid>https://dev.to/mark_fa/react-native-authentication-in-2026-the-refresh-token-pattern-that-actually-scales-4i74</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Access token in memory, refresh token in &lt;code&gt;expo-secure-store&lt;/code&gt;.&lt;/strong&gt; AsyncStorage is plaintext on disk. Stop using it for credentials.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Access token: 15 minutes. Refresh token: 30 days.&lt;/strong&gt; Different signing secrets. The split is about blast radius.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One singleton &lt;code&gt;refreshPromise&lt;/code&gt;&lt;/strong&gt; stops five concurrent 401s from firing five refresh calls (four of which will fail once rotation kicks in).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Always pass an explicit &lt;code&gt;keychainService&lt;/code&gt;.&lt;/strong&gt; Omit it and users get logged out after every TestFlight update, iOS only.&lt;/li&gt;
&lt;li&gt;Total cost: roughly 200 lines of app code, no managed auth vendor required.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most React Native auth tutorials show you how to log a user in. Almost none of them show you how the app behaves at 3 AM when five in-flight requests all hit an expired token at the same time, or how your carefully-stored token silently disappears after a TestFlight update. This post is the pattern that survived three production apps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why most tutorials fail
&lt;/h2&gt;

&lt;p&gt;You've seen this pattern a hundred times:&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;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;AsyncStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;access_token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three problems compound in production:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;AsyncStorage is not secure.&lt;/strong&gt; It's plaintext on disk. Any process that can read your app's sandbox can read the token.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No refresh handling.&lt;/strong&gt; The token expires, users get 401s, and your fix is "log them out and back in." That's a leaky bucket.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Race conditions on refresh.&lt;/strong&gt; When multiple requests fire while the token is expiring, they all try to refresh at once and only one wins.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The pattern below solves all three.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two-token model
&lt;/h2&gt;

&lt;p&gt;Issue two tokens from your API on login:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Access token:&lt;/strong&gt; short-lived (15 minutes), signed with &lt;code&gt;ACCESS_SECRET&lt;/code&gt;. Sent with every authenticated request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refresh token:&lt;/strong&gt; long-lived (30 days), signed with a different &lt;code&gt;REFRESH_SECRET&lt;/code&gt;. Used only to mint new access tokens.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The point of the split is blast radius. If your access token leaks (network sniffer, log line), the attacker has 15 minutes. The refresh token never leaves the keychain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to store each token
&lt;/h2&gt;

&lt;p&gt;The rule that took me two apps to internalize:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Access token → in-memory only.&lt;/strong&gt; A module-level variable. It dies with the JS bundle. That's fine. The app will refresh from disk if needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refresh token → &lt;code&gt;expo-secure-store&lt;/code&gt;.&lt;/strong&gt; Backed by iOS Keychain / Android Keystore. Never AsyncStorage.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// tokens.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;SecureStore&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;expo-secure-store&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;accessToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getAccessToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;accessToken&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;setAccessToken&lt;/span&gt; &lt;span class="o"&gt;=&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;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;accessToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;t&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;getRefreshToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;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;refresh_token&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;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;keychainService&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;com.yourcompany.yourapp.auth&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;setRefreshToken&lt;/span&gt; &lt;span class="o"&gt;=&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;=&amp;gt;&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;refresh_token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;t&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;keychainService&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;com.yourcompany.yourapp.auth&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two options on &lt;code&gt;SecureStore&lt;/code&gt; matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;WHEN_UNLOCKED_THIS_DEVICE_ONLY&lt;/code&gt;: the token is gone if the device is restored onto a new device. Prevents session cloning.&lt;/li&gt;
&lt;li&gt;Explicit &lt;code&gt;keychainService&lt;/code&gt;: survives TestFlight builds (more on this below).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The refresh middleware pattern
&lt;/h2&gt;

&lt;p&gt;Wrap every authenticated fetch in a middleware that handles 401s transparently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// authFetch.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;getAccessToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;setAccessToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;getRefreshToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;setRefreshToken&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;./tokens&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;API&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://your-api.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;refreshPromise&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;performRefresh&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;rt&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;getRefreshToken&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;rt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;NO_REFRESH_TOKEN&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;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;API&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/auth/refresh`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&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;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;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="na"&gt;refresh_token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rt&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Refresh token invalid, user must log in again&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;setRefreshToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setAccessToken&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;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;REFRESH_FAILED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;access_token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;refresh_token&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;r&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="nf"&gt;setAccessToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;access_token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;setRefreshToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;refresh_token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// rotation: new RT every refresh&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;access_token&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;authFetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;opts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;doRequest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="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;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="p"&gt;...(&lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;{}),&lt;/span&gt;
      &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;getAccessToken&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;response&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;doRequest&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;refreshPromise&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;refreshPromise&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;performRefresh&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;refreshPromise&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;refreshPromise&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// all in-flight 401'd requests await the same refresh&lt;/span&gt;
    &lt;span class="nx"&gt;response&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;doRequest&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;response&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;strong&gt;singleton &lt;code&gt;refreshPromise&lt;/code&gt;&lt;/strong&gt; is the piece most tutorials miss. Without it, five concurrent requests hitting an expired token cause five refresh calls. Four of them will 401, because the refresh-token rotation on the first call already invalidated the shared refresh token.&lt;/p&gt;

&lt;h2&gt;
  
  
  TestFlight and iOS keychain: the accessGroup trap
&lt;/h2&gt;

&lt;p&gt;Here's a bug I've watched three teams hit:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Users are getting logged out after every TestFlight update, but only on iOS."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The cause: iOS keychain items are scoped by &lt;code&gt;accessGroup&lt;/code&gt;. When TestFlight installs a new build, it &lt;em&gt;may&lt;/em&gt; preserve keychain access, but only if the &lt;code&gt;keychainService&lt;/code&gt; string is consistent across builds. If your code omits &lt;code&gt;keychainService&lt;/code&gt;, &lt;code&gt;expo-secure-store&lt;/code&gt; generates a random-ish default that changes across builds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; always pass an explicit &lt;code&gt;keychainService&lt;/code&gt; (as in the &lt;code&gt;tokens.js&lt;/code&gt; above). Once set, it survives TestFlight promotions, production releases, and version bumps.&lt;/p&gt;

&lt;p&gt;While you're at it, add a startup check that re-validates the token against your API:&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;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;bootstrapAuth&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getRefreshToken&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;authenticated&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="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Try any authenticated endpoint that returns quickly&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;r&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;authFetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;API&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/me`&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;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;authenticated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;user&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;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;authenticated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;authenticated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This way a truly-invalidated session (user was banned server-side, refresh token was revoked) doesn't sit stale in the app UI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Anti-patterns
&lt;/h2&gt;

&lt;p&gt;Stop doing these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Storing tokens in AsyncStorage.&lt;/strong&gt; It's not encrypted. On Android it's SharedPreferences XML, readable with &lt;code&gt;adb&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bearer tokens in URL query strings.&lt;/strong&gt; They end up in server logs, analytics captures, and browser history.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A single fetch wrapper per API call.&lt;/strong&gt; You'll forget one, and that endpoint will silently break auth. Wrap it once at the module level.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manual logout that only clears the access token.&lt;/strong&gt; If the refresh token is still in the keychain, the next &lt;code&gt;bootstrapAuth()&lt;/code&gt; re-authenticates the user.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refresh-token rotation without server-side invalidation.&lt;/strong&gt; If your API doesn't invalidate the old refresh token on rotation, an attacker who steals it can refresh indefinitely.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What to ship first
&lt;/h2&gt;

&lt;p&gt;If you're starting from a fresh React Native app:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Set up the two-token model in your API. &lt;code&gt;djangorestframework-simplejwt&lt;/code&gt;, or &lt;code&gt;jose&lt;/code&gt; for Node, both handle rotation out of the box.&lt;/li&gt;
&lt;li&gt;Copy the &lt;code&gt;tokens.js&lt;/code&gt; and &lt;code&gt;authFetch.js&lt;/code&gt; above.&lt;/li&gt;
&lt;li&gt;Call &lt;code&gt;bootstrapAuth()&lt;/code&gt; in your root component's &lt;code&gt;useEffect(() =&amp;gt; {}, [])&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Add a global 401 → redirect-to-login handler for the terminal case (refresh token invalid).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's roughly 200 lines of app code. It'll outlive whichever managed auth service you were considering, and the security posture is stricter than most of them. If you'd rather not wire it by hand, &lt;a href="https://rapidnative.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=react-native-refresh-token-pattern" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt; scaffolds Expo apps with this pattern preconfigured, though the pattern itself is straightforward enough that most teams should own it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related patterns worth reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Biometric-gated refresh:&lt;/strong&gt; use &lt;code&gt;expo-local-authentication&lt;/code&gt; to require Face ID before the refresh call. Great for financial and health apps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Silent token rotation on app-foreground:&lt;/strong&gt; refresh preemptively when the app comes back from background. Catches stale-token edge cases.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Encrypted-at-rest offline queue:&lt;/strong&gt; for apps that need to work offline and sync later, queue requests in encrypted storage until the token refreshes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Auth done right is invisible. Ship it once, ship it well, and don't touch it again.&lt;/p&gt;

&lt;p&gt;What's your current setup? Drop a comment with how you're handling refresh in production, especially if you've found a cleaner way to dedupe concurrent refresh calls.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>security</category>
      <category>mobile</category>
    </item>
    <item>
      <title>The 2026 React Native Performance Playbook (New Arch, FlashList, Reanimated 4)</title>
      <dc:creator>Mark F A</dc:creator>
      <pubDate>Mon, 10 Aug 2026 07:01:59 +0000</pubDate>
      <link>https://dev.to/mark_fa/the-2026-react-native-performance-playbook-new-arch-flashlist-reanimated-4-48g2</link>
      <guid>https://dev.to/mark_fa/the-2026-react-native-performance-playbook-new-arch-flashlist-reanimated-4-48g2</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Measure first. A before number and an after number in every PR beats every trick in this post.&lt;/li&gt;
&lt;li&gt;Turn on the New Architecture. Biggest single win available: ~40% faster cold start, 35 to 43% faster list rendering, ~25% lower memory.&lt;/li&gt;
&lt;li&gt;Confirm Hermes is on in your &lt;strong&gt;release&lt;/strong&gt; build, not just dev, and upload your source maps.&lt;/li&gt;
&lt;li&gt;Swap &lt;code&gt;FlatList&lt;/code&gt; for &lt;code&gt;FlashList&lt;/code&gt; past ~50 items.&lt;/li&gt;
&lt;li&gt;Every animation goes to Reanimated 4 worklets. No exceptions.&lt;/li&gt;
&lt;li&gt;Put a budget in CI with Flashlight so regressions break the build instead of your App Store rating.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most React Native apps do not need more code to feel fast. They need less of the wrong code, in the right places.&lt;/p&gt;

&lt;p&gt;The platform moved. New Architecture is default. Hermes is default. FlashList is stable. Reanimated 4 is stable. So the advice you memorized in 2022 (memoize everything, throw &lt;code&gt;shouldComponentUpdate&lt;/code&gt; at every list item, dread the bridge) is now either handled for you or actively counterproductive.&lt;/p&gt;

&lt;p&gt;This is the current playbook, ordered by impact. It assumes one thing: &lt;strong&gt;you measured the problem before you changed any code.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What "fast" actually means
&lt;/h2&gt;

&lt;p&gt;"It feels slow" is not a spec. On a mid-tier Android device (Pixel 6a, Samsung A34), a shippable app in 2026 hits:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Target&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cold start (Android)&lt;/td&gt;
&lt;td&gt;Under 2.0s, tap to first interactive frame&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cold start (iPhone 13)&lt;/td&gt;
&lt;td&gt;Under 1.2s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sustained scroll&lt;/td&gt;
&lt;td&gt;58+ fps on 500+ item lists with images&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interaction latency&lt;/td&gt;
&lt;td&gt;Under 100ms, touch to visible state change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JS heap&lt;/td&gt;
&lt;td&gt;Under 180MB, no monotonic growth over 10 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Install size&lt;/td&gt;
&lt;td&gt;Under 30MB base binary, App Store thin variant&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you cannot state your current numbers against these, that is the first fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Profile first, stop guessing
&lt;/h2&gt;

&lt;p&gt;React Native's performance surface spans three worlds: the JS thread, the UI/main thread, and native modules. A bug in one looks identical to a bug in another until you profile.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hermes Sampling Profiler.&lt;/strong&gt; Ships with RN, near-zero overhead, flame graphs open in Chrome DevTools or Perfetto. Start here every time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;React DevTools Profiler.&lt;/strong&gt; Commit-by-commit view of what rendered and why. The "Highlight updates when components render" toggle finds re-render storms in about ten seconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flashlight.&lt;/strong&gt; CLI for FPS, CPU, memory and JS thread health during scripted runs. Puts real numbers on regressions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Perfetto (Android) / Instruments (iOS).&lt;/strong&gt; For anything crossing the native boundary: cold start, module init, layout, gestures.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expo's perf inspector.&lt;/strong&gt; FPS meter and JS heap sampler in the dev menu. Good enough for 80% of day-to-day work.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rule that matters more than any tool: &lt;strong&gt;before number, after number, both in the PR description.&lt;/strong&gt; No numbers, no merge. This single policy will make your app faster than any optimization on this list.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Be on the New Architecture
&lt;/h2&gt;

&lt;p&gt;RN 0.76 made it the default. Expo SDK 52 followed. If you are still opted out you are leaving 30 to 40% of your cold start and roughly all of your bridge overhead on the table.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;JSI&lt;/strong&gt; replaces the async JSON bridge with direct synchronous calls. Serialize, queue, deserialize, return, deserialize is now a function pointer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fabric&lt;/strong&gt; is the new renderer. Concurrent-aware, layout on the UI thread, no more commit storms during scroll-plus-fetch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TurboModules&lt;/strong&gt; load lazily on first use. That alone can shave 200 to 400ms off cold start in apps with a lot of linked modules.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Enabling it is a flag:&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="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;app.json&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(Expo)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expo"&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;"newArchEnabled"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="c"&gt;# gradle.properties (bare)
&lt;/span&gt;&lt;span class="py"&gt;newArchEnabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;ios/Podfile.properties.json&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(bare)&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;"newArchEnabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What breaks is third-party native modules that never got upgraded. Check each dependency's issue tracker before you flip it.&lt;/p&gt;

&lt;p&gt;Teams that finished the migration in 2025 consistently report cold start down ~40%, list rendering up ~35 to 43%, memory down ~25%, and frame rates going from the high 40s to a steady 58 to 59 fps. That is not incremental. It is the largest performance change RN has ever shipped.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Hermes is default, so do not break it
&lt;/h2&gt;

&lt;p&gt;Hermes is the engine on both platforms now. The value is not just runtime speed, it is ahead-of-time bytecode compilation at build time. You ship precompiled bytecode instead of raw JS, which is why cold starts land 20 to 40% ahead of JSC.&lt;/p&gt;

&lt;p&gt;Three things to actually do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Confirm Hermes is on in release&lt;/strong&gt;, not just dev. Easy to miss when migrating off JSC. &lt;code&gt;hermesEnabled=true&lt;/code&gt; in &lt;code&gt;gradle.properties&lt;/code&gt;, &lt;code&gt;:hermes_enabled =&amp;gt; true&lt;/code&gt; in the Podfile, or the Expo equivalent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Precompile bytecode in CI&lt;/strong&gt; so it is baked at build time instead of first launch on device.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Upload your source maps.&lt;/strong&gt; Hermes bytecode makes Sentry and Bugsnag stack traces unreadable without them. This is the footgun that bites teams in production, every time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You do not need to tune the engine. You just need to not accidentally disable it.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. FlashList for every list that matters
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;FlatList&lt;/code&gt; is fine for short lists. Past ~50 items, images, or variable row heights, use &lt;strong&gt;FlashList&lt;/strong&gt;. This is the highest-leverage optimization that requires you to write actual code.&lt;/p&gt;

&lt;p&gt;FlashList recycles cells instead of unmounting them and does not allocate a new view per item. On a Pixel 6a with 1,000 image-plus-text rows:&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;FlatList&lt;/th&gt;
&lt;th&gt;FlashList&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sustained FPS&lt;/td&gt;
&lt;td&gt;30 to 40, with freezes&lt;/td&gt;
&lt;td&gt;58 to 60&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cell handling&lt;/td&gt;
&lt;td&gt;Unmount and remount&lt;/td&gt;
&lt;td&gt;Recycled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Views allocated&lt;/td&gt;
&lt;td&gt;One per item&lt;/td&gt;
&lt;td&gt;Pooled&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;FlashList&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@shopify/flash-list&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;Row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;memo&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onPress&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Pressable&lt;/span&gt; &lt;span class="na"&gt;onPress&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;onPress&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FastImage&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;thumb&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;thumb&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Pressable&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Feed&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onSelect&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;FlashList&lt;/span&gt;
      &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;estimatedItemSize&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;88&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;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="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="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;Row&lt;/span&gt; &lt;span class="na"&gt;item&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="na"&gt;onPress&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;onSelect&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="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 rules to actually get the speedup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Set &lt;code&gt;estimatedItemSize&lt;/code&gt;.&lt;/strong&gt; Off by 30% and you still get most of the win. Missing entirely and FlashList falls back to slow measurement passes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No dynamic per-item work inside &lt;code&gt;renderItem&lt;/code&gt; closures.&lt;/strong&gt; Memoize row components and their handlers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Variable heights you cannot estimate? Use the median. Do not overthink it.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Kill re-renders, but only the ones that matter
&lt;/h2&gt;

&lt;p&gt;Re-renders are the most over-optimized problem in React Native. Wrapping everything in &lt;code&gt;React.memo&lt;/code&gt; bloats your bundle and slows mounting. Profile first, memoize where the profiler shows a hot path.&lt;/p&gt;

&lt;p&gt;That said, four anti-patterns cause 80%+ of real re-render bugs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inline object props.&lt;/strong&gt; New object identity every parent render, which invalidates every downstream &lt;code&gt;React.memo&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Bad&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Card&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;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;

&lt;span class="c1"&gt;// Good&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;styles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;StyleSheet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;card&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Card&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;card&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Inline handler props.&lt;/strong&gt; &lt;code&gt;&amp;lt;Button onPress={() =&amp;gt; doThing(id)} /&amp;gt;&lt;/code&gt; is a new function every render. &lt;code&gt;useCallback&lt;/code&gt;, or better, move the handler into a memoized child that owns the id.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Context provider sprawl.&lt;/strong&gt; Every value change re-renders every consumer. Split by update frequency: identity (rare), theme (rare), live data (constant). Do not put them in one provider.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Selector reference issues.&lt;/strong&gt; Redux/Zustand selectors returning fresh object references re-render every consumer even when values are identical.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Bad: new object every store update&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;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;avatar&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useStore&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="o"&gt;=&amp;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;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;avatar&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;avatar&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;

&lt;span class="c1"&gt;// Good&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;useShallow&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;zustand/react/shallow&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;avatar&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nf"&gt;useShallow&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="o"&gt;=&amp;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;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;avatar&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;avatar&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;why-did-you-render&lt;/code&gt; is still the fastest way to find these in dev.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Get animations off the JS thread
&lt;/h2&gt;

&lt;p&gt;An animation on the JS thread drops frames the moment JS is busy, which is always, because JS is where your business logic lives.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reanimated 4&lt;/strong&gt; for state, gesture, or timing driven animation. Worklets run on the UI thread as native functions. A 60fps spring stays at 60fps while the JS thread parses a 500KB JSON response.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gesture Handler&lt;/strong&gt; for drags, swipes, pinches. &lt;code&gt;PanResponder&lt;/code&gt; is JS-thread-bound and will jank.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skia&lt;/strong&gt; for anything painterly: charts, custom drawing, complex transitions. Bypasses the React view tree, renders straight to a GPU canvas.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you are writing &lt;code&gt;Animated.Value&lt;/code&gt; with &lt;code&gt;useNativeDriver: false&lt;/code&gt;, stop. Convert it, or find out why &lt;code&gt;useNativeDriver: true&lt;/code&gt; is not an option (usually it is a prop Reanimated supports and old Animated does not).&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Cold start, the number users actually notice
&lt;/h2&gt;

&lt;p&gt;Native init + bundle load + bundle execute + first render. Also the metric App Store reviewers judge you on. In order of impact:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;New Architecture on.&lt;/strong&gt; Biggest win.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hermes bytecode precompiled.&lt;/strong&gt; Second biggest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bundle size.&lt;/strong&gt; Every 100KB of JS costs 20 to 40ms of parse-plus-execute on mid-tier Android. Run &lt;code&gt;npx react-native-bundle-visualizer&lt;/code&gt; and delete what surprises you. &lt;code&gt;moment.js&lt;/code&gt;, full &lt;code&gt;lodash&lt;/code&gt;, and five date-picker libraries you forgot about are the usual suspects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lazy-load routes.&lt;/strong&gt; &lt;code&gt;React.lazy&lt;/code&gt; + &lt;code&gt;Suspense&lt;/code&gt; at route boundaries. Your login screen does not need to parse the dashboard's 400KB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Native module audit.&lt;/strong&gt; Every linked module runs init at startup. Removing the JS import does not unlink the native code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Font loading.&lt;/strong&gt; Preload only the weights on the first screen. &lt;code&gt;Font.loadAsync&lt;/code&gt; for 12 weights blocks first paint for hundreds of ms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Splash strategy.&lt;/strong&gt; Hold the native splash until the first interactive screen is ready. Cross-fade early and you get a flash of empty content.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Instrument with &lt;code&gt;AppRegistry.setWrapperComponentProvider&lt;/code&gt; plus a manual mark at first meaningful paint, log to analytics, and watch the median. Not the mean. The mean lies.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Memory, the silent killer
&lt;/h2&gt;

&lt;p&gt;Leaks rarely crash a React Native app. They just make it slower, and slower, until the OS kills it in the background and the user thinks "the app forgot me." Three sources cover nearly every leak in the wild:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Uncleaned listeners.&lt;/strong&gt; Every &lt;code&gt;addEventListener&lt;/code&gt;, &lt;code&gt;subscribe&lt;/code&gt;, and &lt;code&gt;AppState&lt;/code&gt; handler needs cleanup in the &lt;code&gt;useEffect&lt;/code&gt; return. exhaustive-deps will not catch these.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unbounded image cache.&lt;/strong&gt; &lt;code&gt;react-native-fast-image&lt;/code&gt; caches aggressively. Set a &lt;code&gt;maxMemoryPolicy&lt;/code&gt; or hold hundreds of MB of thumbnails.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Uncleared timers.&lt;/strong&gt; &lt;code&gt;setInterval&lt;/code&gt; in a component that mounts and unmounts on navigation is the classic.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sub&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;AppState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;change&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;poll&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;clearInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;poll&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Heap snapshot at 10 minutes, another at 30, compare. Delta not roughly flat means you have a leak. Instruments Allocations on iOS, Android Studio Memory Profiler on Android.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Network, the invisible half of "fast"
&lt;/h2&gt;

&lt;p&gt;Users do not distinguish "the app is slow" from "the network is slow." Hide the network from them.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;TanStack Query&lt;/strong&gt; for every fetch with a cache key. Dedup, background refetch, stale-while-revalidate for free. The single library that most improved perceived RN performance in the last two years.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HTTP/2 or HTTP/3.&lt;/strong&gt; On HTTP/1.1 you pay a full round trip per request. Cloudflare, Fastly, AWS ALB all default to HTTP/2 now.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image CDN with responsive sizing.&lt;/strong&gt; Serve 400x400 to a phone, not 4000x4000.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Promise.all&lt;/code&gt;&lt;/strong&gt; for anything that does not depend on a previous result.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optimistic updates&lt;/strong&gt; for any action that succeeds 99% of the time. Users feel every millisecond of spinner.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  10. Ship a performance budget in CI
&lt;/h2&gt;

&lt;p&gt;All of the above is worthless if someone ships a regression next Tuesday.&lt;/p&gt;

&lt;p&gt;Flashlight runs a scripted E2E test on a real or emulated Android device, records CPU/FPS/memory, and fails the build when a metric exceeds budget. Point it at five flows (cold start, login, main list scroll, detail view, checkout), set budgets 10 to 20% above current numbers, and now the regression conversation happens in code review instead of in your App Store reviews six weeks later.&lt;/p&gt;

&lt;h2&gt;
  
  
  When NOT to optimize
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The profiler shows no hotspot.&lt;/strong&gt; You are not fixing anything, you are writing code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The user cannot perceive it.&lt;/strong&gt; Nobody notices 12ms to 8ms on a screen already at 60fps. They notice 30fps to 60fps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It removes a real feature.&lt;/strong&gt; Slower with the feature beats faster without it, unless the feature is optional.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where AI-generated code fits
&lt;/h2&gt;

&lt;p&gt;Here is the part that surprised me. Generating a React Native project with an AI builder like &lt;a href="https://www.rapidnative.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=react-native-performance-optimization-guide-2026" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt; now gives you the New Architecture enabled, Hermes on, FlashList on meaningful lists, Reanimated wired up, and TanStack Query for data, because those are the current defaults and the model trained on the current stack. Steps 2, 3, 4 and 6 of this playbook come pre-done.&lt;/p&gt;

&lt;p&gt;What it cannot do is your measurement work. It does not know your product screen renders 400 image cards and needs a memoized row, or that your context provider grew three orders of magnitude too much state. You get the correct baseline for free. The profiling and tuning is still yours.&lt;/p&gt;

&lt;p&gt;So the fast path in 2026: generate the correct baseline, ship v1, measure real user performance, apply this playbook to whatever the profiler actually flags. Skip the six weeks of boilerplate and spend them on the parts of the app only you understand.&lt;/p&gt;




&lt;p&gt;What is the biggest perf win you have shipped this year? Drop it in the comments, especially the ones that surprised you after profiling. I want to know which of these ten actually moved your numbers and which did nothing.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>performance</category>
      <category>javascript</category>
    </item>
    <item>
      <title>How to Build a Pet Care App with React Native (2026)</title>
      <dc:creator>Mark F A</dc:creator>
      <pubDate>Fri, 31 Jul 2026 09:29:50 +0000</pubDate>
      <link>https://dev.to/rapidnative-ai/how-to-build-a-pet-care-app-with-react-native-2026-69i</link>
      <guid>https://dev.to/rapidnative-ai/how-to-build-a-pet-care-app-with-react-native-2026-69i</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pet care is a well-scoped React Native project: bounded MVP, cross-platform users, forgiving early adopters.&lt;/li&gt;
&lt;li&gt;Stack: Expo SDK 52+, TypeScript, Expo Router, Supabase, NativeWind, &lt;code&gt;expo-notifications&lt;/code&gt;, FlashList.&lt;/li&gt;
&lt;li&gt;Store &lt;code&gt;next_due_at&lt;/code&gt; on the vaccination row, not derived — vaccination schedules vary by jurisdiction.&lt;/li&gt;
&lt;li&gt;Store UTC in the DB, render local with &lt;code&gt;date-fns-tz&lt;/code&gt;, and reschedule reminders on &lt;code&gt;AppState.change&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Honest timeline: 4–6 weeks solo, most of it boilerplate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pet care market keeps growing, and mobile apps are eating an increasing share of it — vet booking, vaccination reminders, feeding schedules. If you've been looking for a well-scoped React Native project to ship, a pet care app is genuinely one of the best options: bounded feature set, cross-platform relevance, and a user base that's forgiving of a rough v1 as long as it solves a real problem.&lt;/p&gt;

&lt;p&gt;Here's the practical build guide, from stack decisions through App Store submission.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why React Native fits pet care apps
&lt;/h2&gt;

&lt;p&gt;Pet care apps need cross-platform reach (pet owners split evenly between iOS and Android), rich UI (photo-heavy profiles), and native features like push notifications and camera — the exact sweet spot for React Native with Expo. They also have a naturally scoped MVP: pet profiles, health records, appointments, reminders. You can ship in weeks, not months.&lt;/p&gt;

&lt;h2&gt;
  
  
  The MVP feature set
&lt;/h2&gt;

&lt;p&gt;Ship these five features first. Adding a sixth before nailing edge cases (multi-species schedules, deleted pets, timezone-sensitive reminders) is the fastest way to never launch.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pet profile&lt;/strong&gt; — name, species, breed, DOB, weight, photo, microchip ID&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Health record&lt;/strong&gt; — vaccinations with next-due dates, medications, allergies&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Appointment tracker&lt;/strong&gt; — vet visits, grooming, boarding with reminders&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Feeding log&lt;/strong&gt; — food type, portion, schedule&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Push notifications&lt;/strong&gt; — vaccinations, medications, appointments&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;React Native&lt;/strong&gt; with &lt;strong&gt;Expo SDK 52+&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript&lt;/strong&gt; (non-negotiable for anything you ship)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expo Router&lt;/strong&gt; for file-based routing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Supabase&lt;/strong&gt; for hosted Postgres + auth&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NativeWind&lt;/strong&gt; for Tailwind-style styling&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;expo-notifications&lt;/strong&gt; for local push&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FlashList&lt;/strong&gt; (not &lt;code&gt;FlatList&lt;/code&gt;) for photo-heavy timelines&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Scaffold it
&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 pet-care-app &lt;span class="nt"&gt;--template&lt;/span&gt;
&lt;span class="c"&gt;# choose "Navigation (TypeScript)"&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;pet-care-app
npx expo &lt;span class="nb"&gt;install &lt;/span&gt;expo-notifications expo-image-picker expo-file-system
npm &lt;span class="nb"&gt;install &lt;/span&gt;nativewind zustand @supabase/supabase-js date-fns
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Route structure
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app/
  (tabs)/
    _layout.tsx           # Pets | Schedule | Records | Settings
    index.tsx             # pet list
    schedule.tsx
    records.tsx
    settings.tsx
  pet/
    [id].tsx
    [id]/health.tsx
    [id]/appointments.tsx
  appointment/new.tsx
  vaccination/new.tsx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Decide the route tree before writing screens. Nesting everything under &lt;code&gt;(tabs)/pets/[id]/...&lt;/code&gt; will bite you when you need to push a full-screen modal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data model
&lt;/h2&gt;

&lt;p&gt;Four entities cover the MVP:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Entity&lt;/th&gt;
&lt;th&gt;Key fields&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pets&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;id, owner_id, name, species, breed, date_of_birth, weight_kg, photo_url&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;vaccinations&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;id, pet_id, name, administered_at, next_due_at&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;appointments&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;id, pet_id, type, starts_at, location, notes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;feedings&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;id, pet_id, food_name, portion_grams, scheduled_at, given&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two things matter more than you'd expect:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Store &lt;code&gt;next_due_at&lt;/code&gt; on the row.&lt;/strong&gt; Rabies is annual in some jurisdictions, triennial in others. Don't hardcode.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Store UTC in the DB, render local with &lt;code&gt;date-fns-tz&lt;/code&gt;.&lt;/strong&gt; Get this wrong and a reminder set for 8 AM triggers at 3 AM after a flight.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Push notifications the right way
&lt;/h2&gt;

&lt;p&gt;The single feature that separates useful pet apps from forgettable ones:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Notifications&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-notifications&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;Notifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scheduleNotificationAsync&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;pet&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="s2"&gt;'s &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;vaccination&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="s2"&gt; is due`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Book a vet appointment to stay on schedule.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;petId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;vaccinationId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;vaccination&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;trigger&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;nextDueAt&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 rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Request permissions gracefully — show a "why" screen before the OS prompt.&lt;/li&gt;
&lt;li&gt;Reschedule on &lt;code&gt;AppState.change&lt;/code&gt; when the app foregrounds. Timezone/DST drift is real.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The 4–6 week problem
&lt;/h2&gt;

&lt;p&gt;Honest timeline for a solo developer: 4–6 weeks for a shippable MVP if you're comfortable with React Native. 10–12 weeks if you're learning Expo/TypeScript/Supabase alongside.&lt;/p&gt;

&lt;p&gt;Most of that time is boilerplate — layout, navigation wiring, spacing, empty states, forms. None of it is the interesting part of building a pet care app.&lt;/p&gt;

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

&lt;p&gt;Tools like &lt;a href="https://www.rapidnative.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=how-to-build-a-pet-care-app-with-react-native" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt; generate a working React Native + Expo codebase from a natural-language description. The workflow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Describe the app: &lt;em&gt;"A pet care app with pet profiles, vaccination tracking, appointment reminders, and a schedule tab across all pets."&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Get a working app in a few minutes.&lt;/li&gt;
&lt;li&gt;Scan a QR code, preview on your real phone.&lt;/li&gt;
&lt;li&gt;Click any element to describe changes ("make these cards bigger, add a soft green tint when a task is done").&lt;/li&gt;
&lt;li&gt;Export the full React Native + Expo source, or publish directly to the stores.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Same output — a real React Native app — different path. Worth trying on the boring 80% of the build so you can spend time on the interesting 20%.&lt;/p&gt;

&lt;h2&gt;
  
  
  App Store gotchas specific to pet apps
&lt;/h2&gt;

&lt;p&gt;Three things reviewers catch:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Health claims.&lt;/strong&gt; Track vaccinations/meds without positioning as a medical device. Be explicit in the description.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Background location.&lt;/strong&gt; If you add walk tracking, justify the permission concretely: &lt;em&gt;"To log your dog's walk route while your phone is in your pocket."&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Screenshots.&lt;/strong&gt; Empty-state screenshots underperform dramatically. Show real pets with real data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use &lt;code&gt;eas submit&lt;/code&gt; — it handles both stores and eliminates most rejection loops.&lt;/p&gt;

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

&lt;p&gt;The features are the easy part. Reliable reminders, offline-first data, and fast photo handling are what separate 5-star pet apps from abandoned ones. Nail those, ship a focused MVP, and let owners tell you what's missing.&lt;/p&gt;

&lt;p&gt;If you've shipped a pet app — or any app where reminders are the core value — how did you handle timezone drift on scheduled notifications? Drop a comment, I'd like to hear what actually held up in production.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>mobile</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Build a News Aggregator App with AI Recommendations (React Native + Expo)</title>
      <dc:creator>Mark F A</dc:creator>
      <pubDate>Wed, 29 Jul 2026 06:43:33 +0000</pubDate>
      <link>https://dev.to/rapidnative-ai/build-a-news-aggregator-app-with-ai-recommendations-react-native-expo-207n</link>
      <guid>https://dev.to/rapidnative-ai/build-a-news-aggregator-app-with-ai-recommendations-react-native-expo-207n</guid>
      <description>&lt;p&gt;The average reader engages with about 4% of the news they scroll past. The remaining 96% is duplication, noise, or algorithmically boosted outrage. That gap is the whole reason a well-built AI news aggregator still has room in the market — and thanks to modern tooling, it's now a weekend project instead of a quarter-long roadmap.&lt;/p&gt;

&lt;p&gt;Here's the architecture that keeps showing up in successful indie news apps, plus a shortcut for shipping one fast.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Ingestion&lt;/strong&gt; — NewsAPI, RSS, publisher APIs, GDELT.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Understanding&lt;/strong&gt; — embeddings, categorization, dedup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ranking&lt;/strong&gt; — hybrid: content + collaborative + recency decay + diversity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Presentation&lt;/strong&gt; — reader mode, saves, offline, push, deep links.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Skip any layer and you have a directory nobody opens. Nail all four and you have a daily habit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reference tech stack
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Choice&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Client&lt;/td&gt;
&lt;td&gt;React Native + Expo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Feed API&lt;/td&gt;
&lt;td&gt;NewsAPI + curated RSS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage&lt;/td&gt;
&lt;td&gt;Postgres (Supabase)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Embeddings&lt;/td&gt;
&lt;td&gt;OpenAI &lt;code&gt;text-embedding-3-small&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vector search&lt;/td&gt;
&lt;td&gt;pgvector&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Push&lt;/td&gt;
&lt;td&gt;Expo Notifications&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Offline&lt;/td&gt;
&lt;td&gt;AsyncStorage&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Nothing exotic. The hard part is the product decisions, not the pieces.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building it fast
&lt;/h2&gt;

&lt;p&gt;The traditional way: &lt;code&gt;npx create-expo-app&lt;/code&gt;, set up navigation, build 12 screens by hand, wire the API, add state management, style everything, then start on the ranker.&lt;/p&gt;

&lt;p&gt;The 2026 way: describe the app in a prompt to an AI app builder (I've been using &lt;a href="https://www.rapidnative.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=build-news-aggregator-app-ai-recommendations" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt; for this kind of thing) and get a working Expo project as output — real code, exportable, no lock-in. Then hand-tune the parts that actually matter (the ranker, the sources, the cold-start UX).&lt;/p&gt;

&lt;p&gt;A prompt that produces a strong first version:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Build a news aggregator app called Skim. Vertical feed of article cards with hero image, source logo, headline, summary. Top pill row for categories: For You, Top, Tech, Business, Science, Sports. Tap opens a reader-mode view with share, save, and "More like this." Bottom tabs: Feed, Search, Saved, Profile. Dark theme, pull-to-refresh, shimmer loading.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You get a working multi-screen app in about a minute, previewable on your phone via QR.&lt;/p&gt;

&lt;h2&gt;
  
  
  The recommendation engine
&lt;/h2&gt;

&lt;p&gt;Once you have a live feed, add three ranking signals:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Content-based.&lt;/strong&gt; Embed each article. Store a rolling user "taste vector" as the mean of embeddings of articles they read/saved/lingered on. Cosine-similarity a new article against the user vector. Solves cold-start well.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Collaborative.&lt;/strong&gt; Once you have enough interaction data, "users who read X also read Y" is often stronger than pure text similarity — it captures fun, authority, contrarianism, things the text doesn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recency + diversity re-ranking.&lt;/strong&gt; Multiply scores by &lt;code&gt;exp(-age_hours / half_life)&lt;/code&gt; with a 12–24h half-life. Then, when selecting the top N, penalize each candidate by its similarity to already-selected items. This is what stops the feed from becoming five versions of the same story.&lt;/p&gt;

&lt;p&gt;Rough weights I've seen work: &lt;code&gt;0.4 * content + 0.3 * collab + 0.2 * recency + 0.1 * diversity&lt;/code&gt;. Tune with real data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cold-start UX matters more than ranker quality
&lt;/h2&gt;

&lt;p&gt;The first five swipes decide whether a new user comes back tomorrow. Default to a strong editorial mix (top stories across categories) on session 1–2. Bring personalization online after session 3. Every good news app I've studied does some version of this.&lt;/p&gt;

&lt;h2&gt;
  
  
  The retention features people underinvest in
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Save-for-later with offline caching (people read on the subway)&lt;/li&gt;
&lt;li&gt;"Why am I seeing this?" per-story explainer&lt;/li&gt;
&lt;li&gt;Explicit topic mute/follow controls&lt;/li&gt;
&lt;li&gt;Weekly digest push or email&lt;/li&gt;
&lt;li&gt;Notifications capped at 3–5/day, gated on the user's taste vector&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Copyright, briefly
&lt;/h2&gt;

&lt;p&gt;Show headline + short summary + hero image + link back. Don't cache full article bodies at scale without a license. Reader mode is fine if fetched on-device (Instapaper pattern).&lt;/p&gt;

&lt;h2&gt;
  
  
  The full workflow
&lt;/h2&gt;

&lt;p&gt;If you want to see this in action end-to-end, RapidNative turns the prompt above into a real Expo project you can extend. Free tier is 20 credits, no card. The exported code is standard Expo — you own it.&lt;/p&gt;

&lt;p&gt;Happy to answer questions on the ranker specifics in the comments.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>ai</category>
      <category>mobile</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>A Design System for Push Notifications: 12 Components, 3 States, One Figma File</title>
      <dc:creator>Mark F A</dc:creator>
      <pubDate>Wed, 29 Jul 2026 06:18:29 +0000</pubDate>
      <link>https://dev.to/rapidnative-ai/a-design-system-for-push-notifications-12-components-3-states-one-figma-file-408f</link>
      <guid>https://dev.to/rapidnative-ai/a-design-system-for-push-notifications-12-components-3-states-one-figma-file-408f</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Push is the surface users see 20x a week — it belongs in the design system, not in a marketing sprint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;12 notification archetypes&lt;/strong&gt; cover almost every app: social-mention, social-activity, DM, group-message, transaction-confirm, transaction-update, reminder-user-set, reminder-streak, security-alert, content-drop, system-status, promotional.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;3 states get skipped every time:&lt;/strong&gt; grouped, expanded, and the lock-screen/Notification-Center/banner size variants.&lt;/li&gt;
&lt;li&gt;The handoff artifact is a &lt;strong&gt;YAML file per component&lt;/strong&gt; that maps 1:1 to an &lt;code&gt;expo-notifications&lt;/code&gt; payload. If code diverges from the YAML, code review fails.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;weekly 30-minute notification review&lt;/strong&gt; catches copy drift before users do.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Most design systems stop at the app icon. Push notifications live outside the app, on the lock screen, in the notification shade, in the app-switcher preview — and they get designed ad-hoc, by whoever is writing the marketing send that week. Then a user sees seven notifications from your brand and none of them look like they came from the same company.&lt;/p&gt;

&lt;p&gt;Six months ago I built a push-notification design system for a React Native app the team was scaling from 20k to 200k users. Twelve components, three states each, one Figma file, and a handoff spec that maps every component to a specific expo-notifications payload shape. Here's the whole thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Push Notifications Belong in the Design System
&lt;/h2&gt;

&lt;p&gt;Push is the most visible surface of your product for users who aren't in the app. If your in-app UI is polished but your notifications look like they were written by a bot, the notification is what users remember — it's the surface they see 20 times a week, versus the in-app UI they see three times.&lt;/p&gt;

&lt;p&gt;When push isn't in the design system, three things break:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Copy voice drifts.&lt;/strong&gt; Marketing writes chirpy, engineering writes terse, and the same user gets both.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Icon and color usage is inconsistent.&lt;/strong&gt; Some notifications use the brand accent, some use system defaults, some use whatever emoji the sender liked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rich media (images, actions, replies) get added case-by-case,&lt;/strong&gt; with no consistent affordance for users to know what interactions are available.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A design system fixes all three by making the notification a first-class component with reviewable specs, not an afterthought at the end of a marketing sprint.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 12 Components
&lt;/h2&gt;

&lt;p&gt;After working across four apps, the same twelve notification archetypes keep showing up. Design one component per archetype:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Social-mention&lt;/strong&gt; — someone tagged/mentioned/replied to the user.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Social-activity&lt;/strong&gt; — someone the user follows did something.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Direct-message&lt;/strong&gt; — a 1:1 message from another user.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Group-message&lt;/strong&gt; — a message in a group, showing group name.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transaction-confirm&lt;/strong&gt; — order placed, payment received.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transaction-update&lt;/strong&gt; — delivery moved, ride arrived, booking changed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reminder-user-set&lt;/strong&gt; — the alarm/reminder the user configured.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reminder-streak&lt;/strong&gt; — 'you're one workout from your streak' style.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security-alert&lt;/strong&gt; — new login, fraud check, 2FA code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Content-drop&lt;/strong&gt; — new episode, new article from a followed source.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;System-status&lt;/strong&gt; — app update available, service outage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Promotional&lt;/strong&gt; — offer, discount, event (use sparingly).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each component gets its own frame in Figma with a filled-in example, a spec sheet, and links to the two engineers who own the code path that sends it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three States Designers Forget
&lt;/h2&gt;

&lt;p&gt;Designers default to designing the single-notification-on-lock-screen state. Then production shows three other states that were never designed:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Grouped state (iOS + Android).&lt;/strong&gt; When your app sends 3+ notifications in a session, the OS collapses them into a stack. The stack header is 'YourApp' — but if you set &lt;code&gt;threadIdentifier&lt;/code&gt; (iOS) or &lt;code&gt;channelId&lt;/code&gt; + &lt;code&gt;group&lt;/code&gt; (Android), you get semantic grouping like '4 messages from Sarah.' Design what those group headers look like. Design what happens at 15 notifications (do they collapse further?). Design the tap-to-expand animation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Expanded state.&lt;/strong&gt; Long-press on iOS or swipe-down on Android reveals the expanded notification, which can show images, quick-reply fields, action buttons. Most apps ship the default (a bigger version of the collapsed view). You can do better: this is prime real estate for a mini-interaction that doesn't require opening the app.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lock-screen preview vs Notification Center vs Banner.&lt;/strong&gt; Same content, three different sizing constraints. The banner (top of screen when phone is unlocked) truncates at ~40 chars. The lock-screen preview shows 2-3 lines. Notification Center shows 4-5 lines and stacks. Design the copy for the tightest constraint first, then progressively enhance for the roomier surfaces.&lt;/p&gt;

&lt;h2&gt;
  
  
  Figma-to-Code Handoff That Actually Survives
&lt;/h2&gt;

&lt;p&gt;The handoff spec I ship looks like this — one file per component, machine-readable:&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;component&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;social-mention&lt;/span&gt;
&lt;span class="na"&gt;trigger&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;user_mentioned_in_comment&lt;/span&gt;
&lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{actor.display_name}}&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;mentioned&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;you"&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{excerpt}}"&lt;/span&gt;
  &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;deep_link&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;app://comments/{{comment_id}}"&lt;/span&gt;
    &lt;span class="na"&gt;actor_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{actor.id}}"&lt;/span&gt;
    &lt;span class="na"&gt;thread_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{parent_id}}"&lt;/span&gt;
&lt;span class="na"&gt;ios&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;category_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SOCIAL_MENTION&lt;/span&gt;
  &lt;span class="na"&gt;thread_identifier&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mentions_{{parent_id}}"&lt;/span&gt;
  &lt;span class="na"&gt;interruption_level&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;active&lt;/span&gt;
  &lt;span class="na"&gt;relevance_score&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.7&lt;/span&gt;
&lt;span class="na"&gt;android&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;channel_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;social_mentions&lt;/span&gt;
  &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mentions&lt;/span&gt;
  &lt;span class="na"&gt;priority&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
&lt;span class="na"&gt;copy_rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;excerpt max 60 chars, ellipsize&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;never include reactor emojis in title&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;if actor is verified, prepend U+2713&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The React Native code that sends it maps 1:1:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Notifications&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-notifications&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sendMentionNotification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;actorName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;actorVerified&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;excerpt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;commentId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;parentId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;actorVerified&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`\u2713 &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;actorName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; mentioned you`&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;actorName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; mentioned you`&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;Notifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scheduleNotificationAsync&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;truncate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;excerpt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;categoryIdentifier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SOCIAL_MENTION&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;deep_link&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`app://comments/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;commentId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;actor_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;actorName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;thread_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parentId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;trigger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The yaml file is the contract. If the code diverges from the yaml, code review fails. If a designer wants to change the notification, they change the yaml, which triggers a code PR. Everyone's working from the same spec, and copy can't drift silently.&lt;/p&gt;

&lt;p&gt;Wiring the channels, categories, and deep-link routing by hand is the part that eats a sprint — the &lt;a href="https://rapidnative.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=push-notification-design-system" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt; starters ship with the expo-notifications scaffolding and channel config already in place, so you can go straight to writing the specs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Weekly Notification Design Review
&lt;/h2&gt;

&lt;p&gt;Even with a design system, notification copy drifts. Marketing writes a one-off. Engineering hardcodes a fallback. QA never sees it because notifications only fire in production. Six months in, you have 40 slightly-off notifications your team can't remember shipping.&lt;/p&gt;

&lt;p&gt;Run a weekly 30-minute notification design review. The agenda:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Screenshot every notification the app sent in the last 7 days (via the log I mentioned above — instrument every send).&lt;/li&gt;
&lt;li&gt;Compare each against its yaml spec. Flag drift.&lt;/li&gt;
&lt;li&gt;Look at delivery + open rates per component. Retire any component with under 5% open rate for two weeks running.&lt;/li&gt;
&lt;li&gt;Discuss the copy of any new components proposed this week.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Thirty minutes, weekly. It's the single highest-leverage design meeting I've ever run — small enough that engineering shows up, focused enough that decisions actually get made, and it catches the drift before users notice.&lt;/p&gt;

&lt;p&gt;When we ship this notification design system in RapidNative starters, teams stop treating push as marketing exhaust and start treating it as a product surface. The difference shows up in retention within a quarter.&lt;/p&gt;




&lt;p&gt;How many of the 12 archetypes does your app actually send — and how many of them were designed on purpose? Drop a comment with the notification you know is drifting.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>design</category>
      <category>mobile</category>
    </item>
    <item>
      <title>How to Build a Booking App for Service Businesses with AI (2026 Guide)</title>
      <dc:creator>Mark F A</dc:creator>
      <pubDate>Wed, 29 Jul 2026 05:17:09 +0000</pubDate>
      <link>https://dev.to/rapidnative-ai/how-to-build-a-booking-app-for-service-businesses-with-ai-2026-guide-2791</link>
      <guid>https://dev.to/rapidnative-ai/how-to-build-a-booking-app-for-service-businesses-with-ai-2026-guide-2791</guid>
      <description>&lt;p&gt;If you've ever tried to spec out a booking app for a service business — salon, cleaning company, mobile mechanic, personal trainer — you know the "obvious" features hide 40 nasty edge cases. Buffer time. Timezones. Double-booking races. No-show economics. Reminders that don't wake people up at 3 am.&lt;/p&gt;

&lt;p&gt;This post walks through the actual engineering shape of a service-business booking app in 2026, and how AI app builders (&lt;a href="https://www.rapidnative.com/?utm_source=devto&amp;amp;utm_medium=content&amp;amp;utm_campaign=how-to-build-a-booking-app-for-service-businesses" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt;, in the examples below) collapse the 8–14 week custom build into 1–3 days. The generated output is real React Native + Expo you own and export, not a wrapped webview.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 8 must-have features (skip the rest for v1)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Service catalog (name, duration, price)&lt;/li&gt;
&lt;li&gt;Real-time availability keyed by staff or resource&lt;/li&gt;
&lt;li&gt;Buffer time between appointments&lt;/li&gt;
&lt;li&gt;Deposits or full prepayment (single biggest no-show reducer)&lt;/li&gt;
&lt;li&gt;Push + SMS reminders (24h push, 2h SMS)&lt;/li&gt;
&lt;li&gt;One-tap reschedule / cancel with a policy window&lt;/li&gt;
&lt;li&gt;Customer profile with past appointments&lt;/li&gt;
&lt;li&gt;Staff / admin view for daily calendar and blocking time&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Gift cards, packages, loyalty, in-app chat — all v2. Ship v1 first.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five availability checks nobody documents
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;is_bookable(slot_time T, staff S, service_duration D, buffer B)&lt;/code&gt; returns true iff:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;[T, T + D + B]&lt;/code&gt; sits inside &lt;code&gt;S.working_hours&lt;/code&gt; for that day&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;S&lt;/code&gt; has no other appointment overlapping &lt;code&gt;[T − B, T + D + B]&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;S&lt;/code&gt; has not personally blocked that window&lt;/li&gt;
&lt;li&gt;Date is not on the business's holiday list&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;T &amp;gt;= now() + min_lead_time&lt;/code&gt; (typically 60m)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Miss any one of these and a customer will discover it for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The workflow (works for any AI builder)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Write a one-page spec.&lt;/strong&gt; Services, staff, hours, buffer, deposit, cancellation window, min-lead-time. This is the real bottleneck — not the code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Paste it as a prompt.&lt;/strong&gt; Example for a hair salon:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Build a booking app for "Wave &amp;amp; Co." salon. Services: Cut $45/45m, Cut+Color $150/120m, Blowout $60/45m. Stylists: Alex, Sam, Jamie. $10 Stripe deposit. Push reminder 24h out. My Bookings screen with cancel-if-&amp;gt;24h. Staff view per stylist.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;3. Preview on a real phone via QR.&lt;/strong&gt; Never trust a booking flow you haven't tapped through on-device. Calendar UX, tap targets, and keyboard behavior on a phone-number field only make sense on real hardware.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Point-and-edit instead of re-prompting.&lt;/strong&gt; Click an element in the preview and describe the change ("default the date picker to today"). &lt;a href="https://www.rapidnative.com/?utm_source=devto&amp;amp;utm_medium=content&amp;amp;utm_campaign=how-to-build-a-booking-app-for-service-businesses" rel="noopener noreferrer"&gt;RapidNative's point-and-edit mode&lt;/a&gt; preserves everything you already got right, which full regeneration does not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Wire real integrations.&lt;/strong&gt; Stripe &lt;code&gt;PaymentIntents&lt;/code&gt; with &lt;code&gt;capture_method: manual&lt;/code&gt; for deposits held until no-show, plus Expo Push and Twilio (~$0.008/SMS in the US).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Ship it.&lt;/strong&gt; &lt;code&gt;eas build --platform all&lt;/code&gt; — Expo handles the code signing pain and publishes to both stores under your developer account.&lt;/p&gt;

&lt;h2&gt;
  
  
  Traditional dev vs AI-built (2026 numbers)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;Time to v1&lt;/th&gt;
&lt;th&gt;Upfront&lt;/th&gt;
&lt;th&gt;You own code&lt;/th&gt;
&lt;th&gt;Your brand on stores&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Agency custom build&lt;/td&gt;
&lt;td&gt;8–14 weeks&lt;/td&gt;
&lt;td&gt;$10k–$25k&lt;/td&gt;
&lt;td&gt;Depends on contract&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SaaS (Fresha/Booksy)&lt;/td&gt;
&lt;td&gt;1 day&lt;/td&gt;
&lt;td&gt;$0–$60/mo&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI app builder (&lt;a href="https://www.rapidnative.com/?utm_source=devto&amp;amp;utm_medium=content&amp;amp;utm_campaign=how-to-build-a-booking-app-for-service-businesses" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt;)&lt;/td&gt;
&lt;td&gt;1–3 days&lt;/td&gt;
&lt;td&gt;Credit-based&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Production gotchas
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Timezones&lt;/strong&gt;: store every appointment in UTC, render in user's local, never trust the client clock for availability.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;End-of-day buffer&lt;/strong&gt;: a 5:15pm 45m cut + 10m buffer ends 6:10pm — don't offer that slot if the shop closes at 6.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Race conditions&lt;/strong&gt;: booking creation must be transactional. Lock, verify, insert or fail.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reminders&lt;/strong&gt;: never schedule a push for 3 am local. Clamp to &lt;code&gt;max(now+24h, 9am_local_appointment_day)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No-show deposits&lt;/strong&gt;: default deposit to a real amount ($10–$25 consumer, more for high-ticket). Overriding it manually is fine; defaulting to zero is not.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prompt templates for 6 niches
&lt;/h2&gt;

&lt;p&gt;Personal trainer, cleaning company, mobile mechanic, tutor, dog groomer, massage therapist — the full library is in the canonical post. They all share the same skeleton; the details (pricing, buffer, intake fields) are what generate different apps.&lt;/p&gt;

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

&lt;p&gt;If you want to skip straight to it: &lt;a href="https://www.rapidnative.com/?utm_source=devto&amp;amp;utm_medium=content&amp;amp;utm_campaign=how-to-build-a-booking-app-for-service-businesses" rel="noopener noreferrer"&gt;rapidnative.com&lt;/a&gt; — free tier, 20 credits, paste a prompt, get an app on your phone. Full walkthrough with all six prompt templates and the comparison table is on the &lt;a href="https://www.rapidnative.com/blogs/how-to-build-a-booking-app-for-service-businesses" rel="noopener noreferrer"&gt;canonical post&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Happy building.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>ai</category>
      <category>mobile</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Prototype a Mobile App in Hours (Wireframe React Native)</title>
      <dc:creator>Mark F A</dc:creator>
      <pubDate>Fri, 24 Jul 2026 12:06:54 +0000</pubDate>
      <link>https://dev.to/rapidnative-ai/how-to-prototype-a-mobile-app-in-hours-wireframe-react-native-1aei</link>
      <guid>https://dev.to/rapidnative-ai/how-to-prototype-a-mobile-app-in-hours-wireframe-react-native-1aei</guid>
      <description>&lt;h1&gt;
  
  
  How to Prototype a Mobile App in Hours (Wireframe → React Native)
&lt;/h1&gt;

&lt;p&gt;TL;DR:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The wireframe → clickable-prototype → coded-prototype pipeline is now collapsible into a single step&lt;/li&gt;
&lt;li&gt;Four phases: capture wireframe (20-40 min) → generate base app (2-3 min) → iterate visually (1-2 hrs) → install on device via QR (30 sec)&lt;/li&gt;
&lt;li&gt;First testable build: 1-3 hours vs. 2-4 weeks traditional&lt;/li&gt;
&lt;li&gt;Includes an hour-by-hour log of a real 5-screen food-delivery MVP shipped in one afternoon&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The three artifacts most guides conflate
&lt;/h2&gt;

&lt;p&gt;Before we get into the workflow, one clarification that trips up most prototyping conversations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Wireframe&lt;/strong&gt; — static layout. No interactivity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clickable prototype&lt;/strong&gt; — hotspots wired between static screens (Figma, InVision). Can't run real logic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Working prototype&lt;/strong&gt; — real React Native code, real navigation, real state, installable on device via QR.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Production app&lt;/strong&gt; — the working prototype plus backend, auth, analytics, store listing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most "prototyping" tutorials stop at rung two. The interesting compression is going from rung one straight to rung three.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four-phase workflow
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Capture the wireframe (20-40 min)
&lt;/h3&gt;

&lt;p&gt;Any medium that captures screens + elements + tap behavior works: hand sketch, whiteboard photo, Excalidraw, Figma frame, competitor screenshot, or a written paragraph. Treat the wireframe as disposable — its job is to be input to the generator, not a deliverable.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Generate the base app (2-3 min)
&lt;/h3&gt;

&lt;p&gt;Feed the wireframe + a one-paragraph description into an AI mobile app builder (&lt;a href="https://www.rapidnative.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=mobile-app-prototyping-wireframe-to-working-app" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt; is what we use — sketch/prompt/screenshot → React Native + Expo → QR preview in one editor, free tier with 20 credits and no card). You get back a live React Native + Expo app with routing, styled screens, and stubbed data. It's about 70% right — good enough to iterate against.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Iterate visually (1-2 hours)
&lt;/h3&gt;

&lt;p&gt;The base app has ~30% wrong. Point-and-edit is the accelerator here: click any element in the live preview, describe the change ("make cards rounder, move price to top-right"), and the underlying code updates. Product teams typically do 30-50 micro-iterations in the first hour.&lt;/p&gt;

&lt;p&gt;Two rules for this phase:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Don't accept the first version of any screen.&lt;/li&gt;
&lt;li&gt;Don't over-specify visuals before validating flow. Colors are cheap; navigation is expensive.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Ship to real hardware (30 sec)
&lt;/h3&gt;

&lt;p&gt;Scan a QR code, install into a preview client on iPhone or Android, and use the app on real hardware. This is where the bugs Figma can't show you actually show up: thumb-tap zones, safe areas, keyboard behavior, dark mode.&lt;/p&gt;

&lt;h2&gt;
  
  
  Traditional vs AI-native — the timeline table
&lt;/h2&gt;

&lt;p&gt;For a 5-screen MVP:&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;Traditional&lt;/th&gt;
&lt;th&gt;AI-native&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Wireframing&lt;/td&gt;
&lt;td&gt;4-8 hours&lt;/td&gt;
&lt;td&gt;20-40 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hi-fi design&lt;/td&gt;
&lt;td&gt;2-5 days&lt;/td&gt;
&lt;td&gt;Skipped&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clickable prototype&lt;/td&gt;
&lt;td&gt;4-8 hours&lt;/td&gt;
&lt;td&gt;Skipped&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coded prototype&lt;/td&gt;
&lt;td&gt;1-3 weeks&lt;/td&gt;
&lt;td&gt;2-3 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Iteration cycle&lt;/td&gt;
&lt;td&gt;1-3 days per round&lt;/td&gt;
&lt;td&gt;30 sec to 2 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Real-device install&lt;/td&gt;
&lt;td&gt;4-8 hours (Xcode/TestFlight)&lt;/td&gt;
&lt;td&gt;30 sec (QR)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;First testable build&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2-4 weeks&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1-3 hours&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The compression is mostly the elimination of role handoffs. When one operator drives the whole loop, the "waiting for someone else" hours evaporate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which wireframe inputs convert best
&lt;/h2&gt;

&lt;p&gt;Ranked worst → best in signal-to-noise:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Napkin sketch photo&lt;/strong&gt; — works, but expect nuance loss. Pair with a written description.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Whiteboard photo&lt;/strong&gt; — better; whiteboards force larger, more legible drawings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Excalidraw / FigJam&lt;/strong&gt; — excellent. Digital medium produces clean shapes and labels.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Figma frame&lt;/strong&gt; — very good. Layout and hierarchy transfer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Written PRD paragraph&lt;/strong&gt; — underrated. A precise 3-4 sentence description often beats a bad sketch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Competitor screenshot&lt;/strong&gt; — highest signal. Already a mobile UI, so layout, typography, and interaction all transfer.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Hour-by-hour: food-delivery MVP, one afternoon
&lt;/h2&gt;

&lt;p&gt;Two-person team, five screens (restaurant list → detail → cart → checkout → order tracking):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;0:00-0:30&lt;/strong&gt; — Whiteboard sketch, phone photo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;0:30-0:38&lt;/strong&gt; — Upload + one-paragraph description → base app generated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;0:38-1:20&lt;/strong&gt; — First iteration pass. Point-and-edit ~40 changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1:20-1:35&lt;/strong&gt; — QR install on both team phones. Two nav bugs surface; fix in 3 min.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1:35-2:20&lt;/strong&gt; — Send QR to three test users, collect feedback in shared doc.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2:20-3:15&lt;/strong&gt; — Second iteration pass. Address top-10 UX notes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;3:15-3:30&lt;/strong&gt; — Publish shareable preview URL, drop in Slack.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Three-and-a-half hours end-to-end, iOS + Android, on real phones.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to NOT use this workflow
&lt;/h2&gt;

&lt;p&gt;The AI loop isn't universal. Stay traditional when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Novel interaction patterns with no precedent (generators pattern-match; no pattern = no output quality).&lt;/li&gt;
&lt;li&gt;Regulated industries (medical, aviation, financial trading) that need documented decision trails.&lt;/li&gt;
&lt;li&gt;Pixel-perfect exec-review demos with no budget for a "70%-then-iterate" opening state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the other 90% — CRUD apps, marketplaces, social, dashboards, internal tools — the compressed loop wins on speed, iteration count, and cost.&lt;/p&gt;

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

&lt;p&gt;The whole compression comes down to one operator driving one loop: sketch it, generate it, poke at it on a real phone, repeat. Curious how the four phases feel end-to-end for others — drop a comment with what you're prototyping, and I'll answer questions for the next couple of days.&lt;/p&gt;

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