<?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: Hugo Rus</title>
    <description>The latest articles on DEV Community by Hugo Rus (@hugo_rus_630dd942fcf7cc62).</description>
    <link>https://dev.to/hugo_rus_630dd942fcf7cc62</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%2F4004016%2F5d302419-e375-4c77-b600-eeb065755471.png</url>
      <title>DEV Community: Hugo Rus</title>
      <link>https://dev.to/hugo_rus_630dd942fcf7cc62</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hugo_rus_630dd942fcf7cc62"/>
    <language>en</language>
    <item>
      <title>Real-Time Location Tracking in React Native, Including Live Sharing</title>
      <dc:creator>Hugo Rus</dc:creator>
      <pubDate>Fri, 28 Aug 2026 11:17:07 +0000</pubDate>
      <link>https://dev.to/hugo_rus_630dd942fcf7cc62/real-time-location-tracking-in-react-native-including-live-sharing-848</link>
      <guid>https://dev.to/hugo_rus_630dd942fcf7cc62/real-time-location-tracking-in-react-native-including-live-sharing-848</guid>
      <description>&lt;p&gt;The moving dot on a delivery app looks trivial. Underneath it: a permission sequence that fails silently if you get the order wrong, a background task the OS actively wants to kill, a battery budget you have to defend, and a connection that has to survive a phone going into a pocket.&lt;/p&gt;

&lt;p&gt;Most tutorials stop at &lt;code&gt;watchPositionAsync&lt;/code&gt; on a demo screen. This goes through to live sharing between two devices.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Request foreground permission first, background second, and never on launch&lt;/li&gt;
&lt;li&gt;Define the background task at module top level, not inside a component&lt;/li&gt;
&lt;li&gt;Android needs a foreground service or the OS kills the task within minutes&lt;/li&gt;
&lt;li&gt;Use broadcast for live position, not database inserts&lt;/li&gt;
&lt;li&gt;Coarsest accuracy and longest interval that still feels responsive&lt;/li&gt;
&lt;/ul&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx expo &lt;span class="nb"&gt;install &lt;/span&gt;expo-location react-native-maps expo-task-manager
npm &lt;span class="nb"&gt;install&lt;/span&gt; @supabase/supabase-js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;expo-location&lt;/code&gt; handles foreground and background GPS plus permissions. &lt;code&gt;react-native-maps&lt;/code&gt; renders Apple Maps on iOS and Google Maps on Android. &lt;code&gt;expo-task-manager&lt;/code&gt; is required for background location. Supabase provides the realtime channel for the sharing half.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Permissions
&lt;/h2&gt;

&lt;p&gt;This is where most of the bugs live.&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-location"&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;"locationAlwaysAndWhenInUsePermission"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow $(PRODUCT_NAME) to share your live location."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"isIosBackgroundLocationEnabled"&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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"isAndroidBackgroundLocationEnabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ios"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"infoPlist"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"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;"location"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fetch"&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="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;Request foreground first, background second. Always that order.&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="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Location&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;expo-location&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;requestLocationPermissions&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;fg&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;Location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;requestForegroundPermissionsAsync&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;fg&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;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;Location required&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="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bg&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;Location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;requestBackgroundPermissionsAsync&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;bg&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="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;Background disabled&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;And do not request background on app launch. Wait until the user taps something that needs it, like "start sharing". Asking for always-on location from a stranger is how you get a permanent denial on the first screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Foreground tracking
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Location&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;expo-location&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useLiveLocation&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;location&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setLocation&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LocationObject&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="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="na"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LocationSubscription&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;=&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;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="nx"&gt;sub&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;Location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;watchPositionAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;accuracy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Accuracy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;BestForNavigation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;timeInterval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;distanceInterval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="nx"&gt;setLocation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;})();&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;location&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 knobs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;accuracy&lt;/code&gt;.&lt;/strong&gt; &lt;code&gt;Balanced&lt;/code&gt; for delivery ETAs, &lt;code&gt;BestForNavigation&lt;/code&gt; for anything where a few metres matters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;timeInterval&lt;/code&gt;.&lt;/strong&gt; Minimum milliseconds between fixes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;distanceInterval&lt;/code&gt;.&lt;/strong&gt; Minimum metres moved before firing. Set this to 5 or 10, otherwise a stationary user on a poor fix floods your UI with jitter.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one is the difference between a dot that sits still and a dot that vibrates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: The map
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;MapView&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Marker&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-maps&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;TrackingScreen&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;location&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useLiveLocation&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;location&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;region&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;coords&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;coords&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;latitudeDelta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.005&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;longitudeDelta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.005&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;MapView&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="na"&gt;region&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;region&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;showsUserLocation&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Marker&lt;/span&gt; &lt;span class="na"&gt;coordinate&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;region&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"You are here"&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;MapView&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;Use &lt;code&gt;region&lt;/code&gt;, not &lt;code&gt;initialRegion&lt;/code&gt;, if you want the map to follow. For a marker that slides between fixes rather than teleporting, animate the coordinate rather than setting it directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Background tracking
&lt;/h2&gt;

&lt;p&gt;Define the task at &lt;strong&gt;module top level&lt;/strong&gt;. Not inside a component, not inside a hook. The OS invokes it before your React tree exists.&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="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;TaskManager&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-task-manager&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;Location&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;expo-location&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;LOCATION_TASK&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-location-task&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;TaskManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;defineTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;LOCATION_TASK&lt;/span&gt;&lt;span class="p"&gt;,&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;error&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;error&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&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;locations&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;locations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LocationObject&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;sendLocationsToServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;locations&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;startBackgroundTracking&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;Location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startLocationUpdatesAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;LOCATION_TASK&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;accuracy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Accuracy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Balanced&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;timeInterval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;distanceInterval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;foregroundService&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;notificationTitle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Sharing your location&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;notificationBody&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Tap to open the app.&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;pausesUpdatesAutomatically&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;activityType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ActivityType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Fitness&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;Two non-negotiables. On Android, &lt;code&gt;foregroundService&lt;/code&gt; or the OS kills the task within minutes. On iOS, &lt;code&gt;UIBackgroundModes: ["location"]&lt;/code&gt; or you get rejected at review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Live sharing
&lt;/h2&gt;

&lt;p&gt;The part most tutorials skip.&lt;/p&gt;

&lt;p&gt;A Supabase Realtime channel must be subscribed before it will send anything. Creating a channel and immediately calling &lt;code&gt;send&lt;/code&gt; transmits nothing, and it fails quietly, which makes it a genuinely unpleasant afternoon.&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;createClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@supabase/supabase-js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SUPABASE_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;SUPABASE_ANON_KEY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// create and subscribe once, then reuse&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;createSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;channel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`session:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;sessionId&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;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;shareLocation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;loc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LocationObject&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;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&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;broadcast&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;position&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;loc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;coords&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;lng&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;loc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;coords&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;heading&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;loc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;coords&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;heading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the observing device:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;channel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`session:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;broadcast&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;position&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;payload&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setPeerLocation&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lng&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unsubscribe&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;sessionId&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;Broadcast, not database inserts.&lt;/strong&gt; Inserting every fix burns through row quota and forces observers to poll. If you also need history for a breadcrumb trail or route replay, write to a table on a much slower cadence in addition to the broadcast. Two different jobs, two different mechanisms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Battery
&lt;/h2&gt;

&lt;p&gt;Continuous high-accuracy GPS is one of the most expensive things an app can do, and the cost scales with accuracy and frequency in roughly the way you'd expect.&lt;/p&gt;

&lt;p&gt;The ordering that matters:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Config&lt;/th&gt;
&lt;th&gt;Relative cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;BestForNavigation&lt;/code&gt;, 1 to 2s, foreground&lt;/td&gt;
&lt;td&gt;Highest&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Balanced&lt;/code&gt;, 10s, background&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Lowest&lt;/code&gt;, 30s, background&lt;/td&gt;
&lt;td&gt;Lowest&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Absolute numbers depend heavily on device, OS version, and whether the screen is on, so measure on your own target hardware rather than trusting anyone's table, including this one.&lt;/p&gt;

&lt;p&gt;The rule that generalises: use the coarsest accuracy and longest interval that still feels responsive, and set &lt;code&gt;pausesUpdatesAutomatically: true&lt;/code&gt;, which lets iOS suspend tracking when the user has stopped moving. That last flag is close to free and it does more than most tuning.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Blank map on Android.&lt;/strong&gt; Missing &lt;code&gt;android.config.googleMaps.apiKey&lt;/code&gt; in app config.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;watchPositionAsync&lt;/code&gt; never fires in the simulator.&lt;/strong&gt; The iOS simulator has no GPS. Set a custom location from the simulator's location menu.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Permission dialog on every launch.&lt;/strong&gt; You're calling &lt;code&gt;request*&lt;/code&gt; where you should call &lt;code&gt;get*&lt;/code&gt; first and only request when the answer is undetermined.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Background works locally, dies in production.&lt;/strong&gt; Missing &lt;code&gt;UIBackgroundModes&lt;/code&gt; on iOS or &lt;code&gt;foregroundService&lt;/code&gt; on Android. Both work fine in development, which is what makes this one expensive.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Skipping the plumbing
&lt;/h2&gt;

&lt;p&gt;If you want the scaffold without the permission archaeology, &lt;a href="https://www.rapidnative.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=real-time-location-tracking" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt; generates the Expo and Supabase project from a description, and the output is real React Native you can export and finish wherever you normally work.&lt;/p&gt;

&lt;p&gt;The parts above still need understanding. Generated or not, the OS is going to kill your background task if the foreground service isn't configured, and no scaffold argues with that.&lt;/p&gt;




&lt;p&gt;What's your background tracking horror story? Mine was a task that worked for three weeks and then stopped, because the notification the foreground service depends on had been silently disabled by the user.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>tutorial</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Reanimated vs Moti vs Skia: Which React Native Animation Library Should You Actually Use?</title>
      <dc:creator>Hugo Rus</dc:creator>
      <pubDate>Mon, 24 Aug 2026 11:52:58 +0000</pubDate>
      <link>https://dev.to/hugo_rus_630dd942fcf7cc62/reanimated-vs-moti-vs-skia-which-react-native-animation-library-should-you-actually-use-2j7m</link>
      <guid>https://dev.to/hugo_rus_630dd942fcf7cc62/reanimated-vs-moti-vs-skia-which-react-native-animation-library-should-you-actually-use-2j7m</guid>
      <description>&lt;p&gt;Every React Native team eventually asks the same question: &lt;strong&gt;Reanimated, Moti, or Skia?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The framing is wrong. These three don't compete, they compose. Here's the mental model that will save you a bad refactor.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reanimated&lt;/strong&gt; is the runtime. The other two depend on it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Moti&lt;/strong&gt; is a declarative wrapper over Reanimated. Less code, less control.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skia&lt;/strong&gt; is a rendering engine, not an animation library. It accepts Reanimated shared values directly.&lt;/li&gt;
&lt;li&gt;Install Reanimated and Moti on day one. Add Skia when the design demands it.&lt;/li&gt;
&lt;li&gt;Pick per interaction, not per app.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The one-sentence version of each
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Reanimated.&lt;/strong&gt; The animation runtime. Runs worklets on the UI thread through JSI. Everything else you install depends on it directly or indirectly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Moti.&lt;/strong&gt; A Framer Motion-style declarative wrapper &lt;em&gt;over&lt;/em&gt; Reanimated. Less code for common transitions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Skia.&lt;/strong&gt; Google's 2D graphics engine, the one behind Chrome and Flutter, exposed as React Native components. Not an animation library. A rendering engine that happens to animate beautifully when you feed it shared values.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reanimated in about ten lines
&lt;/h2&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="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="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;x&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;x&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="c1"&gt;// somewhere in a handler:&lt;/span&gt;
&lt;span class="nx"&gt;x&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;200&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;box&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&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;/&amp;gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;x&lt;/code&gt; is a &lt;strong&gt;shared value&lt;/strong&gt;, and it lives on the UI thread. &lt;code&gt;useAnimatedStyle&lt;/code&gt; is a &lt;strong&gt;worklet&lt;/strong&gt; that reruns when &lt;code&gt;x&lt;/code&gt; changes. A blocked JS thread doesn't stutter the animation, which is the entire point.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same thing in Moti
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;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;translateX&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;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;translateX&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="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;spring&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same worklets underneath. Half the code.&lt;/p&gt;

&lt;p&gt;That's the trade: you lose direct access to the shared value, which matters the moment you want to drive it from a gesture. For a mount transition you'll never miss it. For a drag you'll rewrite it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where each one wins
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;You need&lt;/th&gt;
&lt;th&gt;Use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A fade-in on mount&lt;/td&gt;
&lt;td&gt;Moti&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A pan gesture moving a card&lt;/td&gt;
&lt;td&gt;Reanimated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A skeleton loader&lt;/td&gt;
&lt;td&gt;Moti&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pinch-to-zoom on an image&lt;/td&gt;
&lt;td&gt;Reanimated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;An animated chart with 60+ points&lt;/td&gt;
&lt;td&gt;Skia + Reanimated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A shader-driven splash screen&lt;/td&gt;
&lt;td&gt;Skia&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A stagger animation on a list&lt;/td&gt;
&lt;td&gt;Moti&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A blur that follows a bottom sheet&lt;/td&gt;
&lt;td&gt;Skia + Reanimated&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The composition detail people miss
&lt;/h2&gt;

&lt;p&gt;Skia components accept Reanimated shared values as props &lt;strong&gt;directly&lt;/strong&gt;. No &lt;code&gt;createAnimatedComponent&lt;/code&gt;, no &lt;code&gt;useAnimatedProps&lt;/code&gt; wrapper.&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;progress&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="c1"&gt;// progress updates, Skia canvas repaints, all on the UI thread&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;progress&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;That single fact is what made Victory Native XL and every other modern chart library possible. Before it, animating a canvas meant round-tripping through JS every frame.&lt;/p&gt;

&lt;h2&gt;
  
  
  The performance gotcha
&lt;/h2&gt;

&lt;p&gt;You will not hit a performance ceiling because you picked the "wrong" library. All three drive animations on the UI thread.&lt;/p&gt;

&lt;p&gt;You will hit one if you animate a property that forces a layout pass every frame. Reanimated can push a shared value at 120fps, but if consuming it triggers layout, you're queued behind that layout and you drop frames regardless of which library issued the update.&lt;/p&gt;

&lt;p&gt;Animate &lt;code&gt;transform&lt;/code&gt; and &lt;code&gt;opacity&lt;/code&gt;. Animating &lt;code&gt;width&lt;/code&gt;, &lt;code&gt;height&lt;/code&gt;, &lt;code&gt;top&lt;/code&gt;, or &lt;code&gt;left&lt;/code&gt; is the version of this mistake people actually ship, and all three libraries will let you do it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule I use
&lt;/h2&gt;

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

&lt;p&gt;A screen with a draggable card and a hero image wants Reanimated for the drag and Moti for the hero. Separate primitives, separate concerns.&lt;/p&gt;

&lt;p&gt;The moment you try to standardise on one library, you make one of those two interactions worse. Usually the drag, because that's the one where the abstraction costs you the handle you needed.&lt;/p&gt;

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

&lt;p&gt;Install Reanimated and Moti on day one. Add Skia when the design demands it. Compose freely.&lt;/p&gt;

&lt;p&gt;The interesting part of this generalises past animation: the "pick per interaction" rule is roughly how &lt;a href="https://www.rapidnative.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=react-native-animation-libraries" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt; decides which primitive to emit for a given screen, since a prompt describing a drag and a prompt describing a fade want different code.&lt;/p&gt;




&lt;p&gt;What's your default? I'm curious how many people reach for Reanimated directly on things Moti would handle in three lines, because I did that for about a year.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>javascript</category>
      <category>mobile</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>We Made Checkout 600ms Faster. Not One User Noticed.</title>
      <dc:creator>Hugo Rus</dc:creator>
      <pubDate>Wed, 19 Aug 2026 06:41:56 +0000</pubDate>
      <link>https://dev.to/hugo_rus_630dd942fcf7cc62/we-made-checkout-600ms-faster-not-one-user-noticed-2ich</link>
      <guid>https://dev.to/hugo_rus_630dd942fcf7cc62/we-made-checkout-600ms-faster-not-one-user-noticed-2ich</guid>
      <description>&lt;p&gt;We spent a sprint on checkout performance. Prefetched tokens, warmed the payment sheet, bundle-split the checkout screen. End-to-end timing improved by roughly 600ms, which is a real number and visible in the traces.&lt;/p&gt;

&lt;p&gt;Then we retested with users. Not one person mentioned speed. Not in the sessions where it was faster, and not in the earlier ones where it wasn't.&lt;/p&gt;

&lt;p&gt;Everything they &lt;em&gt;did&lt;/em&gt; mention fell into one category, and it wasn't performance.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Measurable speed improvements produced zero user comments in moderated retests&lt;/li&gt;
&lt;li&gt;Every drop-off users could articulate was a trust problem, not a latency problem&lt;/li&gt;
&lt;li&gt;Biometric confirmation changed how users described an otherwise identical flow&lt;/li&gt;
&lt;li&gt;Stripe's default decline message reliably reads as "your card is fraud-flagged"&lt;/li&gt;
&lt;li&gt;We now review payment flows against four trust questions before touching perf&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a follow-up. The earlier findings from this testing (the double-tap problem and the total-mismatch problem) are in &lt;a href="https://dev.to/"&gt;the first writeup&lt;/a&gt;; this post covers what surfaced afterwards.&lt;/p&gt;

&lt;h2&gt;
  
  
  Users drop off at trust moments, not slow moments
&lt;/h2&gt;

&lt;p&gt;That's the headline and it took us a sprint of wasted effort to learn.&lt;/p&gt;

&lt;p&gt;When someone abandons a checkout, they can usually tell you why, and the reason is almost always about confidence rather than time. Nobody says "that took 900 milliseconds and I was unwilling to wait." They say some version of "I wasn't sure what was happening" or "that didn't feel right."&lt;/p&gt;

&lt;p&gt;Latency matters when it &lt;em&gt;creates&lt;/em&gt; a trust problem. A tap that produces no visible response for half a second reads as a broken button, and the user taps again. That's a trust failure caused by latency, and fixing the latency fixes it. But shaving 600ms off a flow that already felt responsive changed nothing anyone could perceive.&lt;/p&gt;

&lt;p&gt;If you have a fixed budget for a payment flow rebuild, spend it on the four things below before you open a profiler.&lt;/p&gt;

&lt;h2&gt;
  
  
  Biometrics change how the same flow feels
&lt;/h2&gt;

&lt;p&gt;This one surprised us most.&lt;/p&gt;

&lt;p&gt;Users describing the flow &lt;em&gt;with&lt;/em&gt; a Face ID confirmation on the final tap used the word "safe." The same users describing the flow &lt;em&gt;without&lt;/em&gt; it reached for "kind of sketchy," "is this the real app," and "I don't know if it went through."&lt;/p&gt;

&lt;p&gt;Same gateway. Same UI. Same latency. The only delta was a native biometric prompt before presenting the sheet.&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="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;LocalAuthentication&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;expo-local-authentication&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;confirmAndPay&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;hasHardware&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;LocalAuthentication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hasHardwareAsync&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;isEnrolled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;LocalAuthentication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isEnrolledAsync&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;hasHardware&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;isEnrolled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;LocalAuthentication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;authenticateAsync&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;promptMessage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Confirm your purchase&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;fallbackLabel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Use passcode&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;success&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;presentPaymentSheet&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;Note the graceful degradation. Devices without enrolled biometrics fall through to the sheet rather than blocking the purchase, which matters more than it sounds — gating checkout behind hardware some users don't have is a worse problem than the one you're solving.&lt;/p&gt;

&lt;p&gt;The interesting part is that nobody asked for this. No user in any session said "I wish this had Face ID." They just described the version without it as untrustworthy, in language they'd never have produced in response to a survey.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stripe's default decline copy is actively harmful
&lt;/h2&gt;

&lt;p&gt;"Your card was declined."&lt;/p&gt;

&lt;p&gt;Technically accurate. In sessions, users read it as &lt;em&gt;your card has been flagged&lt;/em&gt;, assumed something was wrong with their account rather than with this transaction, and several closed the app entirely rather than trying another card.&lt;/p&gt;

&lt;p&gt;The fix is mapping decline codes to plain language plus a recovery action:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DECLINE_COPY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;insufficient_funds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Your bank says there are insufficient funds on this card.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Try a different card&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;incorrect_cvc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;The security code didn't match.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Re-enter card details&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;expired_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;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;This card has expired.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Use a different card&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;generic_decline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Your bank declined this charge. This is usually temporary.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Try a different card&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details that matter more than the copy itself. Attribute the decline to the &lt;em&gt;bank&lt;/em&gt;, not to your app, because users who think your app rejected them do not retry. And make the recovery action reopen the sheet with the cart intact, rather than dumping them back at the top of checkout, which is a second abandonment opportunity you built yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Show the real total before the sheet opens
&lt;/h2&gt;

&lt;p&gt;Related to but distinct from the total-mismatch problem in the first writeup: it isn't only that the numbers must match, it's &lt;em&gt;when&lt;/em&gt; the user learns the final number.&lt;/p&gt;

&lt;p&gt;Fees and taxes computed server-side and revealed inside the payment sheet are technically visible, but by then the user has anchored on the cart price. A higher number appearing at the moment of payment reads as a bait-and-switch even when the delta is trivial. Forty cents is enough.&lt;/p&gt;

&lt;p&gt;Compute the full total, including tax and fees, on the cart screen, with a breakdown available on tap. The number the user commits to should be the number they pay.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four questions we ask now
&lt;/h2&gt;

&lt;p&gt;Before any payment flow work, in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Is the complete total, including all fees, visible before the sheet opens?&lt;/li&gt;
&lt;li&gt;Is there a biometric confirmation on the final tap, with a sensible fallback?&lt;/li&gt;
&lt;li&gt;Does the tap produce a visible response immediately, ideally by prewarming the sheet?&lt;/li&gt;
&lt;li&gt;Are error states mapped to plain-English copy with a working recovery path?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Visual polish, sheet customisation, and perf tuning all come after these. Not because they don't matter, but because in our testing they didn't move the thing we were trying to move.&lt;/p&gt;

&lt;p&gt;If you're scaffolding a new project rather than repairing one, starting from checkout screens that already have the biometric gate, prewarmed sheet, and mapped error states wired in removes most of this before it exists. It's part of what &lt;a href="https://www.rapidnative.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=checkout-trust-not-speed" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt; generates for Expo projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell past me
&lt;/h2&gt;

&lt;p&gt;Run the sessions before the sprint, not after.&lt;/p&gt;

&lt;p&gt;We built the perf work because it was legible, measurable, and felt like the responsible engineering choice. It was also unfalsifiable in the only way that mattered: we could prove it worked in a trace and never prove it worked on a person.&lt;/p&gt;

&lt;p&gt;Twelve moderated sessions cost less than the sprint did.&lt;/p&gt;




&lt;p&gt;What's the last checkout change you shipped that moved a real number? I'm collecting the ones that worked, and my suspicion is that almost none of them are performance.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>ux</category>
      <category>payments</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How AI Is Making Mobile Accessibility Easier Than Ever</title>
      <dc:creator>Hugo Rus</dc:creator>
      <pubDate>Mon, 17 Aug 2026 08:18:54 +0000</pubDate>
      <link>https://dev.to/hugo_rus_630dd942fcf7cc62/how-ai-is-making-mobile-accessibility-easier-than-ever-20n3</link>
      <guid>https://dev.to/hugo_rus_630dd942fcf7cc62/how-ai-is-making-mobile-accessibility-easier-than-ever-20n3</guid>
      <description>&lt;ul&gt;
&lt;li&gt;The OS is now covering for your mistakes: VoiceOver and TalkBack generate descriptions for unlabeled elements on device&lt;/li&gt;
&lt;li&gt;Automated audits catch more than they used to, but still not everything&lt;/li&gt;
&lt;li&gt;Generated alt text and captions fill in content nobody authored&lt;/li&gt;
&lt;li&gt;The real shift is at code-generation time, where accessibility props come free with the component&lt;/li&gt;
&lt;li&gt;None of it replaces turning on a screen reader and trying to complete your own primary flow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;a href="https://webaim.org/projects/million/" rel="noopener noreferrer"&gt;WebAIM Million 2026 report&lt;/a&gt; found detectable accessibility failures on &lt;strong&gt;95.9% of the top one million websites&lt;/strong&gt;, averaging 56 errors per page. Mobile apps fare no better. For every "download now" button on the App Store, there are thousands of screens that fail a basic screen-reader test.&lt;/p&gt;

&lt;p&gt;The gap between the number of apps shipping and the number shipping &lt;em&gt;accessibly&lt;/em&gt; has been widening for a decade. What changed in the last twenty-four months is that AI, on both sides of the shipping fence, is finally starting to narrow it: sometimes in the runtime layer (the phone itself), sometimes at the tooling layer, and most importantly at the code-generation layer where the app is being built in the first place.&lt;/p&gt;

&lt;p&gt;This is a survey of what's actually working in 2026, what still isn't, and what you should assume when writing or generating a mobile app today. AI mobile accessibility isn't one thing. It's four overlapping shifts happening at once.&lt;/p&gt;

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

&lt;p&gt;Mobile accessibility is harder than web accessibility because native apps don't share a common semantic model. There's no HTML. Each platform has its own accessibility tree (UIAccessibility on iOS, AccessibilityNodeInfo on Android), each screen reader has different gesture conventions, and third-party UI frameworks each introduce their own translation layer. A "correct" label in one framework can be silently dropped by the OS on the other platform.&lt;/p&gt;

&lt;p&gt;There are roughly 1.3 billion people worldwide with a significant disability, per the &lt;a href="https://www.who.int/news-room/fact-sheets/detail/disability-and-health" rel="noopener noreferrer"&gt;World Health Organization&lt;/a&gt;, and most of them own a phone. Touch-first, screen-heavy, notification-driven experiences make almost every category of disability harder: low vision (contrast, dynamic type), blindness (screen reader), motor impairment (small touch targets, gestures), deaf and hard of hearing (video without captions), cognitive (dense UI, poor error recovery).&lt;/p&gt;

&lt;p&gt;Compliance pressure is catching up. The &lt;a href="https://ec.europa.eu/social/main.jsp?catId=1202" rel="noopener noreferrer"&gt;European Accessibility Act&lt;/a&gt; took effect for consumer-facing digital products in June 2025, so any app doing business in the EU now has a legal obligation. WCAG 2.2 is the current baseline. In the US, &lt;a href="https://www.ada.gov/resources/2024-03-08-web-rule/" rel="noopener noreferrer"&gt;ADA Title II&lt;/a&gt; requires state and local government mobile apps to meet WCAG 2.1 AA by April 2026.&lt;/p&gt;

&lt;p&gt;Regulation without automation is a punishment. AI is what makes the automation possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shift 1: On-device assistive AI is doing the work the app didn't
&lt;/h2&gt;

&lt;p&gt;The phone itself has quietly become a better citizen for users with disabilities.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;VoiceOver image descriptions.&lt;/strong&gt; iOS added an on-device model that generates descriptions for photos and unlabeled UI elements. If an app ships a button with no accessibility label, a mistake that used to leave a blind user with "Button, button, button," VoiceOver will now attempt to describe the icon: "Button, likely a play triangle."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TalkBack image descriptions.&lt;/strong&gt; Android's TalkBack uses an on-device model to produce contextual descriptions for images, including images inside third-party apps that never bothered to add &lt;code&gt;contentDescription&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live Captions everywhere.&lt;/strong&gt; Both platforms now produce real-time captions for any audio playing on the device, in any app. A video without captions is no longer a dead end.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Voice Control got smarter.&lt;/strong&gt; It now understands intent, not just label matching. "Tap the little heart" works even when the accessibility label is &lt;code&gt;favoriteButton&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The consequence for developers is subtle but important: &lt;strong&gt;the floor is rising, but the ceiling isn't.&lt;/strong&gt; The OS can make a mediocre app tolerable. It can't turn a badly structured screen into a well-structured one. VoiceOver's guess of "Button, likely a heart" will always lose to a labeled "Add to favorites" button that also announces its state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shift 2: AI-powered testing catches what humans miss
&lt;/h2&gt;

&lt;p&gt;Accessibility auditing used to be a slow, manual, checklist-driven job: expensive to do well, easy to skip.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Static analysis got better.&lt;/strong&gt; axe-core, axe DevTools Mobile, and Google's Accessibility Scanner have added ML on top of their rule engines. They now catch things a pure static analyzer couldn't, such as an image whose label is technically present but semantically meaningless (&lt;code&gt;contentDescription="image1.jpg"&lt;/code&gt;), or a button that looks tappable but has no accessibility role.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Visual regression plus ML.&lt;/strong&gt; A category of tools diffs screenshots against a corpus of known-good accessible screens, flagging suspicious contrast ratios, small tap targets, and low-legibility fonts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LLM-based audits.&lt;/strong&gt; A newer category runs a model over rendered screens (screenshots plus accessibility tree dumps) and returns natural-language findings: "The 'Sign in with Apple' button has no accessibility label; VoiceOver will announce it as 'Button'." This is the most useful class during development, because it produces something a developer can act on.&lt;/p&gt;

&lt;p&gt;None of these replace real users on real devices. But they collapse a first-pass audit from days to minutes, which means the audit actually happens.&lt;/p&gt;

&lt;p&gt;The honest limit: &lt;a href="https://webaim.org/projects/million/#automation" rel="noopener noreferrer"&gt;WebAIM's own analysis&lt;/a&gt; suggests even the best automated tools catch only 30 to 40% of WCAG failures. AI raises that, but not to 100%.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shift 3: Generative AI creates the missing content
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Alt text generation.&lt;/strong&gt; Vision-language models produce reasonable alt text in one call. Meta's platforms auto-generate it for uploaded images, browsers do it in-page, and screen readers do it when the app forgets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Captions and audio descriptions.&lt;/strong&gt; Whisper and its successors made speech-to-text good enough for accessibility-quality captions in most languages. AI-generated audio descriptions of visual scenes are becoming usable for consumer content, though not yet reliable for critical use cases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Simplified-language modes.&lt;/strong&gt; LLMs producing plain-language versions of complex content on the fly is a genuine breakthrough for users with cognitive disabilities and non-native speakers.&lt;/p&gt;

&lt;p&gt;The pattern: content that was never authored, because authoring it was too expensive or simply forgotten, is now generated at read time. That's a real win. It also creates a subtler failure mode. AI-generated descriptions are only &lt;em&gt;usually&lt;/em&gt; correct, and a screen reader confidently reading a subtly wrong description is worse than one that says nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shift 4: AI-assisted development is where accessibility actually gets fixed
&lt;/h2&gt;

&lt;p&gt;This is the shift that matters most, because it puts accessibility in the code instead of on top of it.&lt;/p&gt;

&lt;p&gt;For twenty years accessibility has lost the priority fight against feature velocity. When a team is racing to ship, "add accessibility labels to all the buttons on this screen" is the last-day task that gets cut. When a team isn't racing, the work is boring enough that even senior engineers avoid it.&lt;/p&gt;

&lt;p&gt;AI-assisted development inverts that. When a model generates the UI code in the first place, a proper label isn't extra work, it's part of the same generation. Here's the difference in practice:&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;// What most generators emitted a couple of years ago&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;TouchableOpacity&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;handleFavorite&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;HeartIcon&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;TouchableOpacity&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="c1"&gt;// What accessibility-aware generation emits now&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;TouchableOpacity&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;handleFavorite&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;accessibilityRole&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt;
  &lt;span class="na"&gt;accessibilityLabel&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Add to favorites"&lt;/span&gt;
  &lt;span class="na"&gt;accessibilityState&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;selected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isFavorited&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;accessibilityHint&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Adds this item to your favorites list"&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;HeartIcon&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;TouchableOpacity&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first version is what a screen reader user hits constantly: an unlabeled tap target that announces nothing about what it does or whether it's already active. The second costs the generator four extra props and costs the developer nothing.&lt;/p&gt;

&lt;p&gt;This is where &lt;a href="https://www.rapidnative.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=how-ai-is-making-mobile-accessibility-easier" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt; fits. It generates real React Native and Expo code from a natural language prompt, and the output isn't a webview wrapper, it's native components with actual accessibility props attached. Because the model generating the screen also generates the accessibility layer in the same pass, an image comes out as &lt;code&gt;accessibilityLabel="Sunset over the ocean"&lt;/code&gt; rather than a nameless &lt;code&gt;&amp;lt;Image /&amp;gt;&lt;/code&gt; someone will theoretically come back to label.&lt;/p&gt;

&lt;p&gt;This isn't unique to one tool. A well-prompted Copilot, Cursor, or Claude does the same thing in a hand-written codebase. What generation changes is the &lt;em&gt;default&lt;/em&gt;. The path of least resistance shifts from "no labels, ship it" to "labels included, they came free."&lt;/p&gt;

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

&lt;p&gt;If you take away one thing, take this: AI raises the floor dramatically, but the ceiling still requires humans.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AI can't tell you if your screen reader flow is &lt;em&gt;usable&lt;/em&gt;, only that labels exist.&lt;/strong&gt; A screen with a "Buy" button labeled "Buy" and a price labeled "$29.99" can be technically compliant and still confusing, because the reading order jumps around.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generated alt text is generic.&lt;/strong&gt; "A person" is compliant. "The founder of the company you're about to donate to" is what a sighted user sees. Only a human knows which matters here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cognitive accessibility is barely automatable.&lt;/strong&gt; Overwhelming UIs, confusing flows, error messages written for engineers. A model can flag some of it, but the fix is design work, not a tag.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live captioning makes contextually catastrophic errors.&lt;/strong&gt; The wrong medication name, the wrong price, the wrong date. Plausible-but-wrong is a special failure mode for exactly the users you were trying to help.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Motor and cognitive disability are poorly covered.&lt;/strong&gt; Most tooling targets low-vision and blindness because those map onto structured tests. Touch targets, timing constraints, error recovery, and reading load are much harder to automate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The right framing: &lt;strong&gt;AI is your first line of defense, the intern who catches the obvious stuff at 10x human speed.&lt;/strong&gt; It doesn't replace the specialist. It makes their work higher-leverage.&lt;/p&gt;

&lt;h2&gt;
  
  
  A checklist for AI-generated apps
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Every interactive element has an &lt;code&gt;accessibilityLabel&lt;/code&gt;&lt;/strong&gt;, plus &lt;code&gt;accessibilityHint&lt;/code&gt; and &lt;code&gt;accessibilityRole&lt;/code&gt; where appropriate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Images are labeled or explicitly marked decorative.&lt;/strong&gt; No unlabeled &lt;code&gt;&amp;lt;Image /&amp;gt;&lt;/code&gt; in production.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Text contrast passes WCAG 2.2 AA&lt;/strong&gt; (4.5:1 body, 3:1 large text). Default palettes usually pass; custom color changes need rechecking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic Type and font scaling work.&lt;/strong&gt; Layouts shouldn't break at 200% text size.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Touch targets are at least 44x44 points.&lt;/strong&gt; The single most-violated rule in generated code, because models over-index on clean small icons.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forms have labels, not just placeholders.&lt;/strong&gt; A placeholder disappears the moment the user types.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Videos have captions&lt;/strong&gt;, or use the platform's live-caption support.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test with VoiceOver and TalkBack yourself.&lt;/strong&gt; Fifteen minutes completing your primary flow with a screen reader on will find things no automated audit will.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Points 3 through 5 are the ones most worth manually verifying after any design tweak, whatever generator you're using.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this is heading
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;WCAG 3.0&lt;/strong&gt; moves away from binary pass/fail toward outcome-based scoring, which suits AI evaluation far better. "This screen scores 82% for the low-vision population" is something a model can output. Binary pass/fail isn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;EAA enforcement&lt;/strong&gt; in Europe will start producing case law and fines, which changes the internal politics of accessibility work at product companies. It stops being a "when we get around to it" project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI-native compliance tooling.&lt;/strong&gt; Expect tools that sit alongside the development pipeline rather than auditing finished apps: reviewing PRs, gating deploys, producing an audit trail for compliance evidence.&lt;/p&gt;

&lt;p&gt;The direction of travel is that accessibility becomes a property of &lt;em&gt;how the app was built&lt;/em&gt;, not something painted on afterward.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ship accessibly by default
&lt;/h2&gt;

&lt;p&gt;Mobile apps haven't been bad at accessibility because developers don't care. The work is invisible, the tests are boring, the labels are last-minute, and the priority always loses to the next feature.&lt;/p&gt;

&lt;p&gt;AI doesn't change the priority argument. It changes the cost of doing the right thing. On-device AI covers for oversights, testing AI catches issues before shipping, generative AI fills in missing content, and generative development bakes the props in from the first draft.&lt;/p&gt;

&lt;p&gt;If you're building a mobile app in 2026: use a generator that emits accessible code, verify the parts AI can't check (real screen-reader flows, cognitive load, motor targets), and treat the OS-level assistive AI as a backstop rather than a plan.&lt;/p&gt;

&lt;p&gt;What's the worst accessibility bug you've shipped and caught later? Mine was a modal that VoiceOver read straight through, announcing the entire screen behind it. Drop yours in the comments.&lt;/p&gt;

</description>
      <category>a11y</category>
      <category>mobile</category>
      <category>ai</category>
      <category>reactnative</category>
    </item>
    <item>
      <title>Porting a Lovable App to React Native: The Translation Table Nobody Gives You</title>
      <dc:creator>Hugo Rus</dc:creator>
      <pubDate>Fri, 14 Aug 2026 06:08:28 +0000</pubDate>
      <link>https://dev.to/hugo_rus_630dd942fcf7cc62/porting-a-lovable-app-to-react-native-the-translation-table-nobody-gives-you-17ho</link>
      <guid>https://dev.to/hugo_rus_630dd942fcf7cc62/porting-a-lovable-app-to-react-native-the-translation-table-nobody-gives-you-17ho</guid>
      <description>&lt;p&gt;You built something in Lovable over a weekend. It works. Now you want it on a phone, and you've discovered the thing the marketing pages skip: Lovable builds React web apps, not React Native ones.&lt;/p&gt;

&lt;p&gt;The usual advice is "wrap it in Capacitor." That advice has a failure mode I'll get to. First, the actual work, because if you're going to port this thing you should know what porting means.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lovable emits React DOM: &lt;code&gt;div&lt;/code&gt;, &lt;code&gt;span&lt;/code&gt;, &lt;code&gt;input&lt;/code&gt;, Tailwind classes, shadcn/ui, react-router&lt;/li&gt;
&lt;li&gt;None of those exist in React Native. Not "work differently." Do not exist.&lt;/li&gt;
&lt;li&gt;Your Supabase layer ports over essentially unchanged, which is most of your backend work saved&lt;/li&gt;
&lt;li&gt;Wrapping in a webview risks an App Store rejection under Guideline 4.2&lt;/li&gt;
&lt;li&gt;Budget the port at the UI layer and nowhere else&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Translation Table
&lt;/h2&gt;

&lt;p&gt;This is the part you actually need. Every Lovable-generated component maps to something, or to nothing.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;React DOM (Lovable)&lt;/th&gt;
&lt;th&gt;React Native&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;View&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;&amp;lt;span&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;Text&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;TextInput&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;button onClick&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;Pressable onPress&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;img src&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;&amp;lt;Image source&amp;gt;&lt;/code&gt; or &lt;code&gt;expo-image&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;a href&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;&amp;lt;Link&amp;gt;&lt;/code&gt; from expo-router&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;&amp;lt;ul&amp;gt;&lt;/code&gt; / &lt;code&gt;.map()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;FlatList&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;onChange={e =&amp;gt; e.target.value}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;onChangeText={text =&amp;gt; ...}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;localStorage&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;AsyncStorage or MMKV&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;react-router-dom&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;expo-router&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tailwind classes&lt;/td&gt;
&lt;td&gt;NativeWind, or &lt;code&gt;StyleSheet.create&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;shadcn/ui&lt;/td&gt;
&lt;td&gt;no direct port. gluestack-ui or Tamagui&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two entries there are bigger than they look.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;react-router-dom&lt;/code&gt; to expo-router&lt;/strong&gt; is not a swap, it's a restructure. Web routing is a component tree. Expo routing is file-based with native stack and tab semantics underneath. Do this first, before you touch individual screens, or you'll do the screens twice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;shadcn/ui has no port.&lt;/strong&gt; It's built on Radix, which is built on DOM primitives. Every shadcn component in your Lovable project is a rewrite. This is usually the single largest line item in the whole migration.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Looks Like in Practice
&lt;/h2&gt;

&lt;p&gt;Here's a login form the way Lovable writes it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Lovable output&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;LoginForm&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;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setEmail&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="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;navigate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useNavigate&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="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"flex flex-col gap-4 p-6"&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="nt"&gt;h1&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text-2xl font-bold"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Sign in&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&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="nt"&gt;input&lt;/span&gt;
        &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"border rounded px-3 py-2"&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;onChange&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;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="nf"&gt;setEmail&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="nx"&gt;target&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="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;placeholder&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Email"&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="nt"&gt;button&lt;/span&gt;
        &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"bg-blue-600 text-white rounded py-2 hover:bg-blue-700"&lt;/span&gt;
        &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;navigate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/home&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        Continue
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&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;And the same thing in React Native:&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;// React Native&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;View&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;TextInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Pressable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;StyleSheet&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-native&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useRouter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;expo-router&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;LoginForm&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;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setEmail&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="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;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRouter&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;container&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;heading&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Sign in&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;TextInput&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;input&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;onChangeText&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;setEmail&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;placeholder&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Email"&lt;/span&gt;
        &lt;span class="na"&gt;autoCapitalize&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"none"&lt;/span&gt;
        &lt;span class="na"&gt;keyboardType&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email-address"&lt;/span&gt;
      &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Pressable&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onPress&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/home&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt; &lt;span class="na"&gt;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;buttonText&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Continue&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Pressable&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="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;container&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;flexDirection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;column&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;heading&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;fontSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;fontWeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bold&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;borderWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;borderRadius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;paddingHorizontal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;paddingVertical&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="na"&gt;button&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#2563eb&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;borderRadius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;paddingVertical&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;buttonText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#fff&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;textAlign&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;center&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four things changed that aren't obvious from the table:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The button label needed its own &lt;code&gt;&amp;lt;Text&amp;gt;&lt;/code&gt;.&lt;/strong&gt; You cannot put a bare string inside a &lt;code&gt;Pressable&lt;/code&gt;. Every piece of text on screen lives in a &lt;code&gt;Text&lt;/code&gt; node, always.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;hover:bg-blue-700&lt;/code&gt; vanished.&lt;/strong&gt; There's no cursor. If you want press feedback you write it, usually via &lt;code&gt;Pressable&lt;/code&gt;'s style callback.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;autoCapitalize&lt;/code&gt; and &lt;code&gt;keyboardType&lt;/code&gt; appeared.&lt;/strong&gt; Mobile keyboards are configurable and the defaults are wrong for email. Web never made you think about this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;textAlign: 'center'&lt;/code&gt; moved onto the text, not the container.&lt;/strong&gt; Style inheritance doesn't cascade in React Native. Text styling has to live on the &lt;code&gt;Text&lt;/code&gt; node.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That fourth one accounts for a genuinely stupid share of "why does this look wrong" debugging.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things That Silently Do Nothing
&lt;/h2&gt;

&lt;p&gt;These compile, run, and have no effect. No warning, no error.&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;// all of this is ignored&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fixed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;// only absolute and relative exist&lt;/span&gt;
  &lt;span class="na"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;grid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;// flexbox only&lt;/span&gt;
  &lt;span class="na"&gt;boxShadow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0 2px 4px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// use shadowColor/shadowOffset, or elevation on Android&lt;/span&gt;
  &lt;span class="na"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pointer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;// no cursor&lt;/span&gt;
  &lt;span class="na"&gt;overflow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;scroll&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;// you need ScrollView, this won't do it&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;That last one bites hard. On the web, content longer than the viewport scrolls by default. In React Native, content that overflows a &lt;code&gt;View&lt;/code&gt; is just clipped and gone. If a Lovable screen had a long form, it scrolled for free, and after the port it won't. Wrap it in &lt;code&gt;ScrollView&lt;/code&gt; and remember &lt;code&gt;KeyboardAvoidingView&lt;/code&gt;, because the on-screen keyboard covers about half the display and users can't see the field they're typing into.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Good News: Your Backend Ports Clean
&lt;/h2&gt;

&lt;p&gt;If your Lovable app uses Supabase, and it probably does, that entire layer moves over almost untouched. Same &lt;code&gt;supabase-js&lt;/code&gt;, same queries, same auth calls, same RLS policies.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// works identically in both&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;projects&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user_id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;user&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You do need to configure storage for native:&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="nx"&gt;AsyncStorage&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@react-native-async-storage/async-storage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createClient&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;anonKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AsyncStorage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;autoRefreshToken&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;persistSession&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;detectSessionInUrl&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="c1"&gt;// required on native&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;detectSessionInUrl: false&lt;/code&gt; stops the client from trying to parse a browser URL that doesn't exist. Miss it and you'll chase a null session for an hour.&lt;/p&gt;

&lt;p&gt;So the real scope of the port is: the entire UI layer, all of routing, and none of your data model. That's better than it sounds on day one and worse than it sounds on day three.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Wrapping It Is a Trap
&lt;/h2&gt;

&lt;p&gt;The shortcut everyone reaches for is Capacitor or a webview shell. It genuinely works, in the sense that you get a binary.&lt;/p&gt;

&lt;p&gt;The problem is App Store Review Guideline 4.2, minimum functionality. Apple rejects apps that are essentially a repackaged website with no native capability. There's no bright line, which is the worst kind of rule to build a launch schedule around. Apps with real offline behavior, push notifications, and device integration get through. A wrapped CRUD dashboard frequently doesn't, and you find out after you've built everything else.&lt;/p&gt;

&lt;p&gt;If the app is a genuine web product and you want distribution, a PWA is honest and cheap. If you want an App Store listing that survives review, you want native components underneath.&lt;/p&gt;

&lt;h2&gt;
  
  
  If You'd Rather Not Do the Port by Hand
&lt;/h2&gt;

&lt;p&gt;The port above is mechanical, which is exactly the kind of work worth handing off. &lt;a href="https://www.rapidnative.com/lovable-to-app-store?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=lovable-to-app-store-react-native-port" rel="noopener noreferrer"&gt;RapidNative converts Lovable projects&lt;/a&gt; into React Native and Expo apps and takes them through submission: signing, certificates, screenshots, metadata, privacy labels, data safety forms. You get the source, so this isn't a black box you're stuck with. If either store rejects, they fix and resubmit at no extra cost until it's live, which is the part that actually de-risks the 4.2 problem above. Typical turnaround is one to two weeks.&lt;/p&gt;

&lt;p&gt;Whether you hand it off or grind through it manually, the translation table above is what's actually happening underneath. Worth understanding either way, because you'll be debugging it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Short Version
&lt;/h2&gt;

&lt;p&gt;Lovable is a good tool pointed at the web. React Native is a different rendering target, not a different flavor of the same one. The port is real work concentrated almost entirely in the UI layer, your Supabase code comes along for free, and the wrapper shortcut trades a week of porting for an unbounded risk at review time.&lt;/p&gt;

&lt;p&gt;What tripped you up most on a web-to-native port? I'm collecting the silent-failure list and &lt;code&gt;overflow: scroll&lt;/code&gt; is only number two.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>ai</category>
      <category>webdev</category>
      <category>expo</category>
    </item>
    <item>
      <title>React Native i18n in 2026: A Practical Expo Router Playbook</title>
      <dc:creator>Hugo Rus</dc:creator>
      <pubDate>Mon, 03 Aug 2026 07:22:27 +0000</pubDate>
      <link>https://dev.to/hugo_rus_630dd942fcf7cc62/react-native-i18n-in-2026-a-practical-expo-router-playbook-1eha</link>
      <guid>https://dev.to/hugo_rus_630dd942fcf7cc62/react-native-i18n-in-2026-a-practical-expo-router-playbook-1eha</guid>
      <description>&lt;p&gt;If your React Native app only speaks English, you're leaving most of the world's smartphone users on the table. Here's the modern stack for shipping a multilingual Expo app without three days of glue code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;react-i18next&lt;/strong&gt; for translation management (TypeScript-first, the default choice in the RN ecosystem)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;expo-localization&lt;/strong&gt; for device locale detection (or &lt;code&gt;react-native-localize&lt;/code&gt; for bare RN)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Intl&lt;/strong&gt; for dates, numbers, currencies (built into Hermes)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I18nManager&lt;/strong&gt; for RTL layout flipping&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Setup in 4 Steps
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Install
&lt;/h3&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-localization
npm i i18next react-i18next
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Translation files
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;locales/
  en.json
  es.json
  ar.json
&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"welcome"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Welcome"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cart"&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;"itemCount_one"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{{count}} item"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"itemCount_other"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{{count}} items"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;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;Use the ICU plural suffixes (&lt;code&gt;_one&lt;/code&gt;, &lt;code&gt;_other&lt;/code&gt;, &lt;code&gt;_few&lt;/code&gt;, &lt;code&gt;_many&lt;/code&gt;, &lt;code&gt;_zero&lt;/code&gt;). Arabic has six plural forms. Get this right on day one.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Bootstrap
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// i18n.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;i18n&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;i18next&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;initReactI18next&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-i18next&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Localization&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;expo-localization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;en&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./locales/en.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;es&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./locales/es.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;ar&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./locales/ar.json&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;deviceLocale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Localization&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getLocales&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;languageCode&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;i18n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;initReactI18next&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;compatibilityJSON&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;v4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;en&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;translation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;en&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;es&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;translation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;es&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;ar&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;translation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ar&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;lng&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;deviceLocale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;fallbackLng&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;interpolation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;escapeValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Two production-important bits: &lt;code&gt;compatibilityJSON: 'v4'&lt;/code&gt; for modern plural rules, and &lt;code&gt;escapeValue: false&lt;/code&gt; because RN already escapes JSX.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Wire into Expo Router
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/_layout.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Stack&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;expo-router&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./i18n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;RootLayout&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Stack&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then in any component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useTranslation&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-i18next&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useTranslation&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Text&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cart.itemCount&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;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="p"&gt;})}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Text&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="c1"&gt;// "3 items"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Gotchas Nobody Warns You About
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Never concatenate around &lt;code&gt;t()&lt;/code&gt;&lt;/strong&gt;. Word order changes across languages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Breaks in Japanese, German, Arabic&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Text&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hello&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;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;!&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Text&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;
&lt;span class="c1"&gt;// ✅ Whole sentence is one key&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Text&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;greeting&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;})}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Text&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;RTL doesn't flip everything&lt;/strong&gt;. Flexbox auto-flips, but you'll need to fix:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;left&lt;/code&gt;/&lt;code&gt;right&lt;/code&gt;, use &lt;code&gt;start&lt;/code&gt;/&lt;code&gt;end&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Directional icons, wrap in &lt;code&gt;transform: [{ scaleX: -1 }]&lt;/code&gt; when &lt;code&gt;I18nManager.isRTL&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Hardcoded &lt;code&gt;textAlign&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Hardcoded strings in third-party UI kits&lt;/strong&gt;. Buttons in libraries like RN Elements ship English defaults. Override or pick i18n-aware libs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Push notifications aren't localized by the phone&lt;/strong&gt;. Your backend has to send the localized copy or use OS-level notification categories.&lt;/p&gt;

&lt;h2&gt;
  
  
  Formatting
&lt;/h2&gt;

&lt;p&gt;Never string-concat dates or currencies. Use &lt;code&gt;Intl&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Add &lt;code&gt;@formatjs/intl-datetimeformat&lt;/code&gt; polyfills if you need older Android or less common locales.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing Discipline
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Turn on &lt;code&gt;saveMissing&lt;/code&gt; in dev. react-i18next logs every missing key. Fail CI on any.&lt;/li&gt;
&lt;li&gt;Pseudo-localize with &lt;code&gt;en-XA&lt;/code&gt;. Surfaces hardcoded strings and 30 to 40% longer text for layout stress-tests.&lt;/li&gt;
&lt;li&gt;Snapshot per locale. French, German, and Arabic snapshots catch layout regressions.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Shortcut
&lt;/h2&gt;

&lt;p&gt;If you're starting fresh, &lt;a href="https://www.rapidnative.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=rn-i18n-playbook" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt; generates the whole i18n scaffold, provider, layout wiring, translation files, RTL flags, from a natural-language prompt. You get the same setup you'd write by hand, in about 30 seconds.&lt;/p&gt;

&lt;p&gt;Otherwise: ship it. It's 2 to 3 days of setup and it opens the app to a much larger share of the market.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>i18n</category>
      <category>javascript</category>
    </item>
    <item>
      <title>How to Build a Pet Care App with React Native (2026)</title>
      <dc:creator>Hugo Rus</dc:creator>
      <pubDate>Fri, 31 Jul 2026 10:28:00 +0000</pubDate>
      <link>https://dev.to/hugo_rus_630dd942fcf7cc62/how-to-build-a-pet-care-app-with-react-native-2026-3i7</link>
      <guid>https://dev.to/hugo_rus_630dd942fcf7cc62/how-to-build-a-pet-care-app-with-react-native-2026-3i7</guid>
      <description>&lt;ul&gt;
&lt;li&gt;Pet care is a well-scoped React Native project: bounded features, cross-platform demand, forgiving users.&lt;/li&gt;
&lt;li&gt;MVP is five features: pet profile, health record, appointment tracker, feeding log, push notifications.&lt;/li&gt;
&lt;li&gt;Stack: Expo SDK 52+, TypeScript, Expo Router, Supabase, NativeWind, FlashList.&lt;/li&gt;
&lt;li&gt;Store UTC in the database, render local with &lt;code&gt;date-fns-tz&lt;/code&gt;, or your 8 AM reminder fires at 3 AM after a flight.&lt;/li&gt;
&lt;li&gt;Store &lt;code&gt;next_due_at&lt;/code&gt; per row. Rabies schedules differ by jurisdiction.&lt;/li&gt;
&lt;li&gt;Realistic timeline: 4 to 6 weeks solo if you know React Native, 10 to 12 if you are learning the stack alongside.&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. That is 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 and DST drift is real.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Honest timeline for a solo developer: 4 to 6 weeks for a shippable MVP if you're comfortable with React Native. 10 to 12 weeks if you're learning Expo, TypeScript, and 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 and 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;The manual scaffold above should get you to a working v1 in about a month.&lt;/p&gt;

&lt;p&gt;If you're building something in this space, drop a comment with what you're working on. Curious whether anyone has solved multi-pet, multi-timezone reminders cleanly, because that one still feels unsolved to me.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>mobile</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>The 4 Notification Categories Users Actually Whitelist (Based on 60 App Interviews)</title>
      <dc:creator>Hugo Rus</dc:creator>
      <pubDate>Wed, 29 Jul 2026 05:06:49 +0000</pubDate>
      <link>https://dev.to/hugo_rus_630dd942fcf7cc62/the-4-notification-categories-users-actually-whitelist-based-on-60-app-interviews-3ao4</link>
      <guid>https://dev.to/hugo_rus_630dd942fcf7cc62/the-4-notification-categories-users-actually-whitelist-based-on-60-app-interviews-3ao4</guid>
      <description>&lt;ul&gt;
&lt;li&gt;Users whitelist only &lt;strong&gt;four&lt;/strong&gt; notification categories: someone they know did something involving them, a thing they explicitly set up, a transaction they're expecting, and a time-critical alert from a system they trust.&lt;/li&gt;
&lt;li&gt;Everything else — feature announcements, recommendations, streaks, re-engagement, promos — gets muted within two weeks.&lt;/li&gt;
&lt;li&gt;Users pattern-match to &lt;strong&gt;category&lt;/strong&gt;, not copy. Better push copy optimizes the wrong variable.&lt;/li&gt;
&lt;li&gt;Per-category in-app consent (not one system prompt) moved opt-in from 41% to 73% and dropped 30-day mute rate from 34% to 9% in one test.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Over the last six weeks I interviewed 60 people about their phone's notification settings. I asked each to open Settings → Notifications, scroll through every app, and tell me the story behind why each one was on, off, or set to silent delivery.&lt;/p&gt;

&lt;p&gt;The pattern was sharper than I expected. Users don't allow or block push app-by-app. They do it &lt;strong&gt;category-by-category&lt;/strong&gt; — and they only tolerate four specific categories. Everything else gets muted within two weeks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four categories users whitelist
&lt;/h2&gt;

&lt;p&gt;Across 60 interviews, people named the same four types of notification as ones they actively want:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Someone I know did something involving me.&lt;/strong&gt; A message, a comment, a mention, a friend joining, a family member sharing a photo. Nearly everyone said they never mute these.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. A thing I explicitly set up.&lt;/strong&gt; Alarm, reminder, calendar event, medication, workout time. The rare exceptions had turned these off &lt;em&gt;in the app itself&lt;/em&gt;, not in system settings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. A transaction I'm expecting.&lt;/strong&gt; Delivery updates, ride arrival, payment confirmation, booking status. Widely kept on, though several used silent delivery to soften the interruption.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. A time-critical alert from a system I trust.&lt;/strong&gt; Bank fraud alert, security code, appointment 15 minutes out, flight delay. &lt;em&gt;Trust&lt;/em&gt; was the load-bearing word — several people had disabled these for specific apps that had cried wolf.&lt;/p&gt;

&lt;p&gt;Every other category — feature announcements, personalized recommendations, streak reminders, re-engagement, promos, content updates — got muted, disabled, or in a few cases triggered an uninstall.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why category beats copy
&lt;/h2&gt;

&lt;p&gt;The standard growth advice is "write better push copy." A/B test the emoji, tighten the CTA, personalize with the user's name.&lt;/p&gt;

&lt;p&gt;That optimizes the wrong variable.&lt;/p&gt;

&lt;p&gt;The interviews were unambiguous: users don't parse notification copy before deciding whether to keep push on. They pattern-match to the category. Once your app has sent two notifications from a category they don't want, they mute the whole app — no matter how good the copy was.&lt;/p&gt;

&lt;p&gt;So the design question isn't "how do we write a compelling notification?" It's &lt;strong&gt;"how do we ensure every notification we send falls into one of the four whitelisted categories?"&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The consent conversation to have in-app
&lt;/h2&gt;

&lt;p&gt;The iOS system dialog is a blunt instrument: one yes/no for all future notifications. What users actually want is category-level consent — which iOS supports through notification categories, but almost no app uses well.&lt;/p&gt;

&lt;p&gt;The pattern I now recommend:&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="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Category&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;social&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;reminders&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;transactions&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;security&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;setupNotificationCategories&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;setNotificationCategoryAsync&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&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="na"&gt;identifier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;reply&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;buttonTitle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Reply&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;opensAppToForeground&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="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;setNotificationCategoryAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;reminders&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;setNotificationCategoryAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;transactions&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;setNotificationCategoryAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;security&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;requestConsentForCategory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Category&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;stored&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;getStoredPreferences&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;stored&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;denied&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="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// In-app modal that explains what this category is&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;wantsIt&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;showCategoryConsentModal&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="nf"&gt;labelFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;example&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;exampleFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;frequency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;frequencyFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;savePreference&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;wantsIt&lt;/span&gt; &lt;span class="p"&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;denied&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;wantsIt&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;getPermissionsAsync&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;status&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="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="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 in-app modal does the real work. It shows a concrete example of the notification the user would receive, states roughly how often ("a few times per week," not "occasionally"), and lets them opt in per category rather than for everything at once.&lt;/p&gt;

&lt;p&gt;One app I tested this with saw opt-in rise from &lt;strong&gt;41% to 73%&lt;/strong&gt; by moving from a single system prompt to contextual per-category prompts. More importantly, 30-day mute rate dropped from &lt;strong&gt;34% to 9%&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The signal every PM misses: silent delivery
&lt;/h2&gt;

&lt;p&gt;iOS's "Deliver Quietly" lets users keep receiving your notifications but hide them from the lock screen and Notification Center. It's a soft mute — chosen when someone wants the app's data but not the interruption.&lt;/p&gt;

&lt;p&gt;Most PMs don't track it, because iOS doesn't report it. But you can infer it: if a user's open rate on pushes drops to near-zero while their app opens stay steady, they've likely moved you to Deliver Quietly.&lt;/p&gt;

&lt;p&gt;Segment those users. They're one bad broadcast away from a full mute or uninstall. For them, cut sends by 60–80% and fire only category 1 and 3 notifications (people-related and transactional). I've seen apps recover full-delivery status this way for a meaningful share of the quietly-delivered segment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Redesign around user-declared intent
&lt;/h2&gt;

&lt;p&gt;The biggest shift from these interviews: the notifications users keep are tied to intent &lt;em&gt;they&lt;/em&gt; expressed inside the product. They set the reminder. They ordered the food. They followed the person. They asked the bank to alert on fraud.&lt;/p&gt;

&lt;p&gt;The notifications they mute are tied to intent the &lt;em&gt;product&lt;/em&gt; declared on their behalf. "We think you'd love this new feature." "We noticed you haven't opened us in a week." "Here's a personalized recommendation."&lt;/p&gt;

&lt;p&gt;So the implication is concrete: for every notification your app sends, you should be able to trace it back to a specific in-app action the user took that constitutes consent for &lt;em&gt;this&lt;/em&gt; notification. If you can't, it belongs in the mute pile. If you can, you're building the trust that keeps push on for the lifetime of that user.&lt;/p&gt;

&lt;p&gt;A practical way to enforce this: require a &lt;code&gt;sourceAction&lt;/code&gt; field on every notification in your schema — a machine-checkable link back to the user action that authorized the send. It's an annoying constraint at first. Then it becomes the reason your app is still on the lock screen six months in, when competitors have been muted.&lt;/p&gt;




&lt;p&gt;Run the interviews yourself. Ten users, thirty minutes each. You'll come back with a notification strategy your PM would never sign off on — and your users will actually welcome.&lt;/p&gt;

&lt;p&gt;I write more about building this kind of thing into React Native apps over at &lt;a href="https://www.rapidnative.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=notification-categories" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Which of the four categories does your app mostly send? And have you ever caught yourself sending from the mute pile?&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>ux</category>
      <category>mobile</category>
      <category>expo</category>
    </item>
    <item>
      <title>Introducing tinbase: A Docker-Free, Supabase-Compatible Backend on Real Postgres</title>
      <dc:creator>Hugo Rus</dc:creator>
      <pubDate>Mon, 27 Jul 2026 13:30:19 +0000</pubDate>
      <link>https://dev.to/hugo_rus_630dd942fcf7cc62/introducing-tinbase-a-docker-free-supabase-compatible-backend-on-real-postgres-7pg</link>
      <guid>https://dev.to/hugo_rus_630dd942fcf7cc62/introducing-tinbase-a-docker-free-supabase-compatible-backend-on-real-postgres-7pg</guid>
      <description>&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;tinbase&lt;/strong&gt; is an open-source (MIT) backend that speaks the Supabase API but runs as a &lt;strong&gt;single process on real Postgres 17&lt;/strong&gt; — no Docker, no container orchestration.&lt;/li&gt;
&lt;li&gt;It's &lt;strong&gt;&lt;code&gt;supabase-js&lt;/code&gt; compatible&lt;/strong&gt;, so a lot of existing Supabase client code points at it unchanged.&lt;/li&gt;
&lt;li&gt;Because it drops the container requirement, it runs where the full stack can't: lightweight CI, low-spec machines, and &lt;strong&gt;in the browser&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Built by &lt;a href="https://twitter.com/sanketsahu" rel="noopener noreferrer"&gt;Sanket Sahu&lt;/a&gt;. MIT-licensed and open to contributions.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;If you've run Supabase locally, you know the ritual: install Docker, start the stack, wait for Postgres, GoTrue, PostgREST, Realtime, and Storage to spin up, and watch your laptop fans kick in.&lt;/p&gt;

&lt;p&gt;For a lot of workflows — quick local iteration, CI, teaching, browser sandboxes — that's more machinery than the task needs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;tinbase&lt;/strong&gt; is a different bet: the Supabase API surface, on real Postgres, as one process.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it actually is
&lt;/h2&gt;

&lt;p&gt;tinbase is an open-source backend that provides Supabase-compatible APIs while running as a single process on &lt;strong&gt;PostgreSQL 17&lt;/strong&gt;, with &lt;strong&gt;no Docker requirement&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The design goals in one line each:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Real Postgres, not a shim.&lt;/strong&gt; It runs on Postgres 17, so SQL behavior matches production. No SQLite dialect gaps to trip over when you deploy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;supabase-js&lt;/code&gt; compatible.&lt;/strong&gt; Client code written against Supabase can, in many cases, point at a tinbase instance without changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single process, no Docker.&lt;/strong&gt; One thing to start. No container stack to orchestrate or resource-tune.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runs in the browser.&lt;/strong&gt; Because it sheds the container requirement, tinbase can run in environments a Docker stack simply can't — including in-browser sandboxes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MIT-licensed.&lt;/strong&gt; Fully open source. Inspect it, self-host it, contribute to it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why single-process, Docker-free matters
&lt;/h2&gt;

&lt;p&gt;The architecture isn't the point — what it unlocks is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Faster local loops.&lt;/strong&gt; A single process starts in a fraction of the time an orchestrated multi-container stack takes. The edit-run-test cycle gets shorter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cheaper CI.&lt;/strong&gt; CI runners are memory-constrained and billed by the minute. Spinning up one Postgres-backed process per job is dramatically lighter than booting the full Supabase container set.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Teaching and demos that just run.&lt;/strong&gt; Workshops and tutorials can hand out a real backend without first walking every participant through installing and configuring Docker. That alone removes the most common "it doesn't work on my machine" failure at the start of a session.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Browser and constrained environments.&lt;/strong&gt; In-browser IDEs and low-spec machines that can't host the full container stack can still run a real Supabase-compatible backend.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it compares
&lt;/h2&gt;

&lt;p&gt;If you want a lighter local Supabase, you've probably weighed these:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Real Postgres?&lt;/th&gt;
&lt;th&gt;Docker-free?&lt;/th&gt;
&lt;th&gt;Supabase-compatible?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Full Supabase CLI stack&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQLite-based shim&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Partial&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hand-rolled Postgres + PostgREST&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Depends&lt;/td&gt;
&lt;td&gt;You wire it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;tinbase&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The gap tinbase fills: the realism of Postgres-backed Supabase, without the Docker overhead, and without dropping to a non-Postgres engine that diverges from production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who it's for
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Developers who want a fast local Supabase without the container tax&lt;/li&gt;
&lt;li&gt;Teams trying to cut CI time and cost&lt;/li&gt;
&lt;li&gt;Educators and content authors who need a reproducible backend in a tutorial&lt;/li&gt;
&lt;li&gt;Anyone building in a browser sandbox or on constrained hardware&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;p&gt;tinbase is single-process and Docker-free, so setup is meant to be minimal: install, start the process, and point a &lt;code&gt;supabase-js&lt;/code&gt; client at it.&lt;/p&gt;

&lt;p&gt;The full documentation and source are at &lt;a href="https://www.tinbase.dev/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=tinbase-launch" rel="noopener noreferrer"&gt;tinbase.dev&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;If you've been avoiding local Supabase because of the Docker overhead, this is worth a look — especially for CI and teaching setups.&lt;/p&gt;

&lt;p&gt;Running Supabase locally today? What's your current setup — full CLI stack, a shim, or something hand-rolled? Curious what the pain points have been.&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>postgres</category>
      <category>opensource</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to Prototype a Mobile App in Hours (Wireframe React Native)</title>
      <dc:creator>Hugo Rus</dc:creator>
      <pubDate>Fri, 24 Jul 2026 09:34:12 +0000</pubDate>
      <link>https://dev.to/hugo_rus_630dd942fcf7cc62/how-to-prototype-a-mobile-app-in-hours-wireframe-react-native-2mjc</link>
      <guid>https://dev.to/hugo_rus_630dd942fcf7cc62/how-to-prototype-a-mobile-app-in-hours-wireframe-react-native-2mjc</guid>
      <description>&lt;ul&gt;
&lt;li&gt;The wireframe → clickable prototype → coded prototype pipeline collapses into a single step.&lt;/li&gt;
&lt;li&gt;Four phases: &lt;strong&gt;capture the wireframe&lt;/strong&gt; (20–40 min), &lt;strong&gt;generate the base app&lt;/strong&gt; (2–3 min), &lt;strong&gt;iterate visually&lt;/strong&gt; (1–2 hrs), &lt;strong&gt;ship to real hardware&lt;/strong&gt; (30 sec).&lt;/li&gt;
&lt;li&gt;Traditional first testable build: &lt;strong&gt;2–4 weeks&lt;/strong&gt;. AI-native: &lt;strong&gt;1–3 hours&lt;/strong&gt;. Most of the compression is eliminating role handoffs.&lt;/li&gt;
&lt;li&gt;Best wireframe input isn't a sketch — it's a competitor screenshot, or a precise written paragraph.&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;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, and tap behaviour works: hand sketch, whiteboard photo, Excalidraw, Figma frame, competitor screenshot, or a written paragraph.&lt;/p&gt;

&lt;p&gt;Treat the wireframe as disposable. Its job is to be &lt;em&gt;input to the generator&lt;/em&gt;, 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 plus a one-paragraph description into an AI mobile app builder. You get back a live React Native + Expo app with routing, styled screens, and stubbed data.&lt;/p&gt;

&lt;p&gt;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: 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. &lt;strong&gt;Colours are cheap; navigation is expensive.&lt;/strong&gt;
&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.&lt;/p&gt;

&lt;p&gt;This is where the bugs Figma can't show you actually surface: thumb-tap zones, safe areas, keyboard behaviour, dark mode.&lt;/p&gt;

&lt;h2&gt;
  
  
  Traditional vs. AI-native
&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 by 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 it 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 media produce clean shapes and labels.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Figma frame&lt;/strong&gt; — very good. Layout and hierarchy transfer cleanly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Written PRD paragraph&lt;/strong&gt; — underrated. A precise three-to-four sentence description often beats a bad sketch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Competitor screenshot&lt;/strong&gt; — highest signal. It's already a mobile UI, so layout, typography, and interaction all transfer.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Hour by hour: a food-delivery MVP in 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 plus a 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, roughly 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; fixed in 3 minutes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1:35–2:20&lt;/strong&gt; — Send the QR to three test users, collect feedback in a shared doc.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2:20–3:15&lt;/strong&gt; — Second iteration pass. Address the top ten UX notes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;3:15–3:30&lt;/strong&gt; — Publish a shareable preview URL, drop it in Slack.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;h2&gt;
  
  
  When &lt;em&gt;not&lt;/em&gt; to 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;
&lt;strong&gt;Novel interaction patterns with no precedent.&lt;/strong&gt; Generators pattern-match; no pattern means no output quality.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Regulated industries&lt;/strong&gt; (medical, aviation, financial trading) that need documented decision trails.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pixel-perfect exec-review demos&lt;/strong&gt; 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;p&gt;If you want to run the loop end to end, &lt;a href="https://www.rapidnative.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=wireframe-to-app-prototyping" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt; does the sketch/prompt/screenshot → React Native + Expo → QR-preview pipeline in one editor.&lt;/p&gt;

&lt;p&gt;What's your current wireframe-to-code gap? Curious whether the competitor-screenshot trick works as well for others as it has for me.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>ai</category>
      <category>mobile</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Recruiting 12 Real Testers for Your Google Play Closed Test (Without Buying Them)</title>
      <dc:creator>Hugo Rus</dc:creator>
      <pubDate>Wed, 22 Jul 2026 11:02:33 +0000</pubDate>
      <link>https://dev.to/hugo_rus_630dd942fcf7cc62/recruiting-12-real-testers-for-your-google-play-closed-test-without-buying-them-3ffe</link>
      <guid>https://dev.to/hugo_rus_630dd942fcf7cc62/recruiting-12-real-testers-for-your-google-play-closed-test-without-buying-them-3ffe</guid>
      <description>&lt;ul&gt;
&lt;li&gt;Buying testers fails three ways: &lt;strong&gt;Play's fraud detection flags dormant accounts&lt;/strong&gt;, &lt;strong&gt;bought testers never open the app&lt;/strong&gt; (so engagement doesn't count), and &lt;strong&gt;you're dependent on a third party&lt;/strong&gt; to satisfy a store requirement.&lt;/li&gt;
&lt;li&gt;Three source pools that work: your &lt;strong&gt;existing audience&lt;/strong&gt; (highest quality), &lt;strong&gt;community trades&lt;/strong&gt; on r/androiddev and r/indiedev (fastest), and your &lt;strong&gt;personal network&lt;/strong&gt; (last resort).&lt;/li&gt;
&lt;li&gt;A warm-list invite converts 40–50%. Anonymous forums convert 5–10% — which is why you need multiple pools.&lt;/li&gt;
&lt;li&gt;Play watches for &lt;strong&gt;meaningful engagement&lt;/strong&gt;, not just opt-ins. Four touches over 14 days keeps testers active.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;You need 12 opted-in testers for 14 days before Google Play will let you ship. Buying them looks like the shortcut.&lt;/p&gt;

&lt;p&gt;It usually isn't. Play's ML is good at spotting bought accounts, and it will silently reset your 14-day counter.&lt;/p&gt;

&lt;p&gt;Here's how to recruit 12 real humans in 3–4 days instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why paid services fail
&lt;/h2&gt;

&lt;p&gt;The tester-service business exists, and some of it works. Most of it has three failure modes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Play's fraud detection flags dormant or newly created accounts.&lt;/strong&gt; Your test "has 12 testers" but Play doesn't count them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The testers opt in and never open the app.&lt;/strong&gt; The engagement signal doesn't register.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You're now dependent on a third party to satisfy a store requirement.&lt;/strong&gt; If they turn shady, your launch is stuck.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And even when it works, you've spent $50–200 on 12 accounts that will never use your app again. You had to do the harder work anyway — you just paid extra for the privilege.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three source pools
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Your existing audience (highest quality)
&lt;/h3&gt;

&lt;p&gt;Newsletter subscribers, followers, Discord or Slack members. Even 200 subscribers converts to 10–15 testers with a good ask.&lt;/p&gt;

&lt;p&gt;If you don't have this: start building one on day one of your next project. This requirement is a recurring tax, not a one-off.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Community trades (fastest)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;r/androiddev&lt;/strong&gt; — weekly beta-swap threads&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;r/indiedev&lt;/strong&gt; — weekly "test my app" threads&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Indie Hackers&lt;/strong&gt; — milestones section&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Discord communities&lt;/strong&gt; in your product's category (game dev, productivity, etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The trade is simple: "I'll test your app, you test mine." Both parties get closer to their 12, and communities enforce reciprocity so quality stays high.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Personal network plus amplification (last resort)
&lt;/h3&gt;

&lt;p&gt;Friends, colleagues, family — then their networks via a "know anyone with an Android who'd help?" broadcast. This usually gets you the last three or four you need.&lt;/p&gt;

&lt;h2&gt;
  
  
  The invite template
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Subject:&lt;/strong&gt; Would you spend 5 minutes to help me launch my app?&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Hey [name],&lt;/p&gt;

&lt;p&gt;I'm launching [app name] on Android next month. Google now requires 12 testers for 14 days before I can ship publicly.&lt;/p&gt;

&lt;p&gt;If you have an Android phone and 5 minutes, you'd be helping me a lot by:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Clicking this link: [tester opt-in URL]&lt;/li&gt;
&lt;li&gt;Installing when the app appears in your Play Store&lt;/li&gt;
&lt;li&gt;Opening it a couple of times over the next 2 weeks&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's the whole ask. You'll get free access to the paid version as a thank you.&lt;/p&gt;

&lt;p&gt;[your name]&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This converts &lt;strong&gt;40–50% on a warm list&lt;/strong&gt;. Anonymous forums convert 5–10% — which is exactly why you need more than one pool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping them engaged for 14 days
&lt;/h2&gt;

&lt;p&gt;Play watches for meaningful engagement. If your 12 testers opt in and never open the app, the requirement isn't satisfied.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Day 1:&lt;/strong&gt; Welcome message with "here's what to try" (two or three specific flows).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Day 5:&lt;/strong&gt; A genuine "what's confusing so far?" ask. This gets you feedback &lt;em&gt;and&lt;/em&gt; another session.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Day 10:&lt;/strong&gt; Preview of what's shipping in the next update. Another reason to open.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Day 14:&lt;/strong&gt; Thank you, plus "the app goes to production tomorrow."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Four touches, each with real value. Not spam.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turning testers into first users
&lt;/h2&gt;

&lt;p&gt;Your 12 testers just spent 14 days with your app. That's the highest-quality feedback pool you will ever have. Before the closed test ends:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Send a proper survey.&lt;/strong&gt; Not "rate 1–5." Ask: &lt;em&gt;"What almost stopped you from using this?"&lt;/em&gt; and &lt;em&gt;"What would you tell a friend it does?"&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ask for a testimonial&lt;/strong&gt;, with permission to quote.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ask whether they'd upgrade to paid at launch.&lt;/strong&gt; Their answer is your first real customer signal.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The reframe
&lt;/h2&gt;

&lt;p&gt;You started this hunting 12 accounts to satisfy Google. You finish with 12 engaged real users, genuine feedback, a few testimonials, and possibly your first paying customers.&lt;/p&gt;

&lt;p&gt;The Play requirement is annoying. The discipline it forces is actually a launch playbook.&lt;/p&gt;




&lt;p&gt;If you're stuck at "have the app, need the testers," &lt;a href="https://letsdeploy.it/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=recruiting-12-testers" rel="noopener noreferrer"&gt;letsdeploy.it&lt;/a&gt; handles the release side so you can focus on recruiting. The 12 humans still have to come from you.&lt;/p&gt;

&lt;p&gt;How did you find your 12? I'm curious which pool actually worked — the swap threads or the personal network.&lt;/p&gt;

</description>
      <category>android</category>
      <category>googleplay</category>
      <category>indiedev</category>
      <category>startup</category>
    </item>
    <item>
      <title>7 Things That Break When You Convert a Web App to React Native</title>
      <dc:creator>Hugo Rus</dc:creator>
      <pubDate>Wed, 22 Jul 2026 09:51:38 +0000</pubDate>
      <link>https://dev.to/hugo_rus_630dd942fcf7cc62/7-things-that-break-when-you-convert-a-web-app-to-react-native-4bj2</link>
      <guid>https://dev.to/hugo_rus_630dd942fcf7cc62/7-things-that-break-when-you-convert-a-web-app-to-react-native-4bj2</guid>
      <description>&lt;ul&gt;
&lt;li&gt;There is no &lt;code&gt;div&lt;/code&gt;, no &lt;code&gt;span&lt;/code&gt;, and no cascading CSS. Every layout primitive changes.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;localStorage&lt;/code&gt;, &lt;code&gt;window&lt;/code&gt;, &lt;code&gt;document&lt;/code&gt;, and most browser APIs simply don't exist.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;flexDirection&lt;/code&gt; defaults to &lt;code&gt;column&lt;/code&gt;, not &lt;code&gt;row&lt;/code&gt;. This one silently breaks every layout you wrote.&lt;/li&gt;
&lt;li&gt;Routing, image handling, forms, and scrolling all need native equivalents.&lt;/li&gt;
&lt;li&gt;None of it is hard individually. It's the volume that kills weekend projects.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;AI web app builders got very good very fast. You can describe an app to Lovable, v0, or Bolt and have a working web product the same afternoon.&lt;/p&gt;

&lt;p&gt;Then you try to put it on the App Store, and you discover the thing nobody mentions: &lt;strong&gt;a web app and a native app share a language, not a platform.&lt;/strong&gt; They both run JavaScript. Almost nothing else transfers.&lt;/p&gt;

&lt;p&gt;I've done this conversion enough times to know exactly where it hurts. Here are the seven things that break, in roughly the order you'll hit them.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Every HTML element is gone
&lt;/h2&gt;

&lt;p&gt;There is no &lt;code&gt;div&lt;/code&gt;. No &lt;code&gt;span&lt;/code&gt;. No &lt;code&gt;p&lt;/code&gt;, &lt;code&gt;h1&lt;/code&gt;, &lt;code&gt;button&lt;/code&gt;, &lt;code&gt;ul&lt;/code&gt;, or &lt;code&gt;img&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;React Native has its own primitives, and the mapping isn't one-to-one:&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;// Web&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"card"&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="nt"&gt;h2&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Title&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h2&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="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Body text&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&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="nt"&gt;img&lt;/span&gt; &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;url&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="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="c1"&gt;// React Native&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;card&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;Title&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Body text&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;Image&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important gotcha: &lt;strong&gt;all text must be wrapped in &lt;code&gt;&amp;lt;Text&amp;gt;&lt;/code&gt;.&lt;/strong&gt; A bare string inside a &lt;code&gt;&amp;lt;View&amp;gt;&lt;/code&gt; throws. On web you can drop text anywhere; here it's a hard error, and it's the single most common first crash.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. CSS doesn't cascade — and half of it doesn't exist
&lt;/h2&gt;

&lt;p&gt;React Native uses a JavaScript style object, not a stylesheet. That means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No cascade.&lt;/strong&gt; A style on a parent doesn't inherit down to children. Set &lt;code&gt;fontSize&lt;/code&gt; on a &lt;code&gt;View&lt;/code&gt; and the &lt;code&gt;Text&lt;/code&gt; inside ignores it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No selectors.&lt;/strong&gt; No classes, no descendants, no &lt;code&gt;:hover&lt;/code&gt;, no media queries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No units.&lt;/strong&gt; Numbers are density-independent pixels. &lt;code&gt;padding: 16&lt;/code&gt;, not &lt;code&gt;padding: '16px'&lt;/code&gt;.
&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="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;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;borderRadius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#fff&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="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;Also missing: &lt;code&gt;float&lt;/code&gt;, &lt;code&gt;grid&lt;/code&gt;, &lt;code&gt;position: fixed&lt;/code&gt;, and most of the box model shorthand you're used to. Flexbox is the layout system — essentially the only one.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. &lt;code&gt;flexDirection&lt;/code&gt; defaults to &lt;code&gt;column&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;This is the one that quietly wrecks everything.&lt;/p&gt;

&lt;p&gt;On the web, &lt;code&gt;display: flex&lt;/code&gt; defaults to &lt;code&gt;flex-direction: row&lt;/code&gt;. In React Native, &lt;strong&gt;every &lt;code&gt;View&lt;/code&gt; is already flex, and the default direction is &lt;code&gt;column&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So every horizontal row you built on the web stacks vertically after the port, and you won't get an error — just a layout that looks wrong everywhere at once.&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;// You have to be explicit&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;flexDirection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;row&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;alignItems&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;center&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="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;Budget an afternoon just for this.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Browser APIs don't exist
&lt;/h2&gt;

&lt;p&gt;No &lt;code&gt;window&lt;/code&gt;. No &lt;code&gt;document&lt;/code&gt;. No &lt;code&gt;localStorage&lt;/code&gt;. No &lt;code&gt;fetch&lt;/code&gt; cookies, no DOM measurement, no &lt;code&gt;alert()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The replacements:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Web&lt;/th&gt;
&lt;th&gt;React Native&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;localStorage&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;AsyncStorage&lt;/code&gt; (or &lt;code&gt;expo-secure-store&lt;/code&gt; for anything sensitive)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;document.querySelector&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;refs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;window.location&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Expo Router / React Navigation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;alert()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Alert.alert()&lt;/code&gt; from &lt;code&gt;react-native&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CSS media queries&lt;/td&gt;
&lt;td&gt;&lt;code&gt;useWindowDimensions()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;fetch&lt;/code&gt; does survive, which is a small mercy — your API layer mostly ports as-is.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Routing is a different model
&lt;/h2&gt;

&lt;p&gt;URL-based routing doesn't exist natively. There's no address bar and no history object.&lt;/p&gt;

&lt;p&gt;Expo Router gets you closest to a web mental model — it's file-based, so &lt;code&gt;app/profile/[id].tsx&lt;/code&gt; maps to &lt;code&gt;/profile/42&lt;/code&gt; — but you still have to think in &lt;strong&gt;stacks and tabs&lt;/strong&gt; rather than pages. Back isn't "previous URL," it's "pop the stack."&lt;/p&gt;

&lt;p&gt;Deep links also need real configuration on both platforms (URL schemes, Universal Links on iOS, App Links on Android). That's a genuine chunk of work with no web equivalent.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Scrolling and lists are explicit
&lt;/h2&gt;

&lt;p&gt;On the web, content overflows and the browser gives you a scrollbar for free. In React Native, nothing scrolls unless you say so.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ScrollView&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* short content */&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;ScrollView&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FlatList&lt;/span&gt;
  &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;items&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="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And use &lt;code&gt;FlatList&lt;/code&gt; (or FlashList) for anything long — &lt;code&gt;ScrollView&lt;/code&gt; renders every child at once and will tank your frame rate on a list of 500 items.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Forms and keyboards need real work
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt; becomes &lt;code&gt;&amp;lt;TextInput&amp;gt;&lt;/code&gt;, and it needs configuration the browser handled implicitly: &lt;code&gt;keyboardType&lt;/code&gt;, &lt;code&gt;autoCapitalize&lt;/code&gt;, &lt;code&gt;autoComplete&lt;/code&gt;, &lt;code&gt;secureTextEntry&lt;/code&gt;, &lt;code&gt;returnKeyType&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then there's the keyboard itself. On mobile it physically covers the bottom of the screen, so your submit button disappears behind it unless you wrap the form in &lt;code&gt;KeyboardAvoidingView&lt;/code&gt; with different &lt;code&gt;behavior&lt;/code&gt; values per platform.&lt;/p&gt;

&lt;p&gt;There is no web equivalent to this problem, and it catches everyone once.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this actually adds up to
&lt;/h2&gt;

&lt;p&gt;Individually, none of these are hard. A competent React dev can work through any one of them in an hour.&lt;/p&gt;

&lt;p&gt;The problem is that there are seven of them, and they compound — plus certificates, provisioning profiles, screenshots at five device sizes, a privacy nutrition label, and a reviewer who might say no anyway.&lt;/p&gt;

&lt;p&gt;That's the honest reason so many AI-built web apps never reach a store. The build was a weekend. The port and the submission are a different project entirely.&lt;/p&gt;

&lt;p&gt;If you'd rather not do that project, &lt;a href="https://www.rapidnative.com/lovable-to-app-store?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=web-to-rn-conversion" rel="noopener noreferrer"&gt;RapidNative converts Lovable apps into real React Native + Expo builds and handles the full App Store and Play Store submission&lt;/a&gt; — signing, screenshots, metadata included. You get the source code and keep ownership of it, and if your app uses Supabase, the native build points at the same project with no migration.&lt;/p&gt;




&lt;p&gt;Which of these seven got you first? For me it was &lt;code&gt;flexDirection&lt;/code&gt; — I spent two hours convinced my styles weren't applying at all.&lt;/p&gt;

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