<?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: Jules Sarah</title>
    <description>The latest articles on DEV Community by Jules Sarah (@jules_sarah_0718e958f0d24).</description>
    <link>https://dev.to/jules_sarah_0718e958f0d24</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%2F4004010%2Ff34f3a46-3625-4d59-a933-3e49876494b9.png</url>
      <title>DEV Community: Jules Sarah</title>
      <link>https://dev.to/jules_sarah_0718e958f0d24</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jules_sarah_0718e958f0d24"/>
    <language>en</language>
    <item>
      <title>React Native Performance Optimization: The 2026 Playbook</title>
      <dc:creator>Jules Sarah</dc:creator>
      <pubDate>Tue, 11 Aug 2026 09:48:19 +0000</pubDate>
      <link>https://dev.to/jules_sarah_0718e958f0d24/react-native-performance-optimization-the-2026-playbook-515j</link>
      <guid>https://dev.to/jules_sarah_0718e958f0d24/react-native-performance-optimization-the-2026-playbook-515j</guid>
      <description>&lt;ul&gt;
&lt;li&gt;Profile before changing code. Before/after numbers required in every performance PR.&lt;/li&gt;
&lt;li&gt;New Architecture on (default since RN 0.76) is the single biggest win.&lt;/li&gt;
&lt;li&gt;Hermes stays on; the footgun is source maps, not the engine.&lt;/li&gt;
&lt;li&gt;FlashList for any list past ~50 items. Set &lt;code&gt;estimatedItemSize&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Animations off the JS thread: Reanimated 4, Gesture Handler, Skia.&lt;/li&gt;
&lt;li&gt;Ship a performance budget in CI with Flashlight so regressions break the build, not the App Store rating.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most React Native apps do not need more code to feel fast. They need less of the wrong code, in the right places. In 2026 the platform has changed enough (New Architecture is default, Hermes is default, FlashList is stable, Reanimated 4 is stable) that the old advice, memoize everything, throw &lt;code&gt;shouldComponentUpdate&lt;/code&gt; at every list item, dread the bridge, is either automatic or actively wrong.&lt;/p&gt;

&lt;p&gt;This is the current playbook. It is opinionated, ordered by impact, and it assumes one thing above all: &lt;strong&gt;you measured the problem before you changed any code.&lt;/strong&gt; That single discipline is worth more than every trick below combined.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "fast" actually means in React Native
&lt;/h2&gt;

&lt;p&gt;Before you optimize anything, agree with your team on numbers. "It feels slow" is not a spec. In 2026, a shippable React Native app on a mid-tier Android device (think Pixel 6a, Samsung A34) should hit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cold start under 2.0 seconds&lt;/strong&gt; on Android, under 1.2 seconds on an iPhone 13, measured from tap to first interactive frame.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sustained scroll at 58+ fps&lt;/strong&gt; on lists of 500+ items with images.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interaction latency under 100 ms&lt;/strong&gt; from touch to visible state change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JS heap under 180 MB&lt;/strong&gt; during normal use, no monotonic growth over a 10-minute session.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Install size under 30 MB&lt;/strong&gt; for the base binary, App Store thin variant.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you cannot state your app's current numbers against these, that's the first fix. Everything below assumes you know where you stand.&lt;/p&gt;

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

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

&lt;p&gt;The 2026 toolbelt:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Hermes Sampling Profiler&lt;/strong&gt;: the primary tool for finding hot JS functions. It ships with React Native, has near-zero overhead, and produces flame graphs you can open in Chrome DevTools or Perfetto. Start every investigation here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;React DevTools Profiler&lt;/strong&gt;: commit-by-commit view of which components rendered and why. Use it to catch re-render storms; the "Highlight updates when components render" toggle is the quickest way to see them without profiling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flashlight&lt;/strong&gt;: CLI tool for measuring FPS, CPU, memory and JS thread health during scripted test runs. Puts real numbers on regressions in CI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Perfetto (Android)&lt;/strong&gt; and &lt;strong&gt;Instruments (iOS)&lt;/strong&gt;: for anything that crosses the native boundary: cold start, module init, layout, gesture responsiveness.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expo's built-in performance inspector&lt;/strong&gt;: if you're on Expo (you probably should be), the dev menu now includes an FPS meter and JS heap sampler that's good enough for 80% of day-to-day work.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The measurement rule is simple: &lt;strong&gt;produce a before number, produce an after number, and require both in the PR description.&lt;/strong&gt; Every performance change without both numbers gets reverted on principle. This one rule alone will make your app faster than any single optimization on this list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Be on the New Architecture, the single biggest win
&lt;/h2&gt;

&lt;p&gt;React Native 0.76 made the New Architecture the default. Expo SDK 52 followed. If you are still opted out, you are leaving a large share of your cold start and roughly all of your bridge overhead on the table.&lt;/p&gt;

&lt;p&gt;The New Architecture is three pieces working together:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;JSI (JavaScript Interface)&lt;/strong&gt; replaces the old asynchronous JSON bridge with direct, synchronous calls between JS and native code. The bridge is gone. Calls that used to be serialize -&amp;gt; queue -&amp;gt; deserialize -&amp;gt; return -&amp;gt; deserialize are now function pointers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fabric&lt;/strong&gt; is the new renderer. It's concurrent-mode aware, does layout on the UI thread, and eliminates the "commit storm" that used to cause jank during scroll + fetch combinations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TurboModules&lt;/strong&gt; are the successor to native modules. They load lazily (only on first use), which alone can shave 200–400 ms from cold start in apps with many linked modules.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Migration in 2026 is mostly a checkbox: &lt;code&gt;"newArchEnabled": true&lt;/code&gt; in &lt;code&gt;app.json&lt;/code&gt; for Expo, or the equivalent in &lt;code&gt;gradle.properties&lt;/code&gt; and &lt;code&gt;Podfile.properties.json&lt;/code&gt; for bare projects. What breaks are third-party native modules that never got upgraded; check each dependency's issue tracker before you flip the flag. Teams that finished the migration have reported cold starts improving by a third or more, list rendering throughput up by similar margins, lower memory use, and animation frame rates jumping from the high 40s to a steady 58–59 fps. Run your own before/after; the direction is consistent even where the exact numbers vary by app. This is the single largest performance change React Native has ever shipped.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1512941937669-90a1b58e7e9c%3Fw%3D800" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1512941937669-90a1b58e7e9c%3Fw%3D800" alt="Person using a smartphone showing smooth UI" width="800" height="533"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The New Architecture in React Native 0.76+ is where the "React Native feels native" claim finally becomes true. Photo by Rob Hampson on Unsplash&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Hermes is the default, leave it on and understand what it's doing
&lt;/h2&gt;

&lt;p&gt;Hermes is the JavaScript engine on iOS and Android by default in 2026. Its value is not just runtime speed, it's the ahead-of-time bytecode compilation step that happens during your build. Your app ships pre-compiled bytecode instead of raw JS, which is why cold starts on Hermes are typically much faster than on JSC.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Confirm Hermes is on in your release build&lt;/strong&gt;, not just dev. It's easy to miss when migrating from JSC. &lt;code&gt;hermesEnabled=true&lt;/code&gt; in &lt;code&gt;gradle.properties&lt;/code&gt;, &lt;code&gt;:hermes_enabled =&amp;gt; true&lt;/code&gt; in the Podfile, or the equivalent Expo config.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enable bytecode precompilation in CI&lt;/strong&gt;, so the bytecode is baked at build time rather than the first launch on device.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check your source maps.&lt;/strong&gt; Hermes bytecode makes stack traces unreadable in Sentry/Bugsnag without the correct source map upload. This is the one Hermes footgun that consistently bites teams in production.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The engine itself is stable. You do not need to tune it. The wins come from not accidentally disabling it and from wiring source maps correctly.&lt;/p&gt;

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

&lt;p&gt;&lt;code&gt;FlatList&lt;/code&gt; in 2026 is a fine default for short lists. The moment a list has more than ~50 items, images, or variable row heights, replace it with &lt;strong&gt;FlashList&lt;/strong&gt; from Shopify. This is the single highest-leverage React Native optimization that requires you to write actual code.&lt;/p&gt;

&lt;p&gt;FlashList is a drop-in replacement that recycles cells instead of unmounting them, does not allocate a new view for every item, and delivers several times the throughput of &lt;code&gt;FlatList&lt;/code&gt; on data-heavy screens. On a Pixel 6a scrolling 1,000 image-plus-text rows, expect 58–60 fps sustained under FlashList against 30–40 fps with occasional freezes under FlatList.&lt;/p&gt;

&lt;p&gt;Two rules to actually get the speedup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Set &lt;code&gt;estimatedItemSize&lt;/code&gt; accurately.&lt;/strong&gt; Even off by 30% and you still get most of the win; missing entirely and FlashList falls back to slower measurement passes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do not put dynamic per-item work inside &lt;code&gt;renderItem&lt;/code&gt; closures.&lt;/strong&gt; Every re-render creates new closures; memoize row components and their handlers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your list has variable height rows and you cannot estimate size, use &lt;code&gt;estimatedItemSize&lt;/code&gt; with the median height. Do not overthink it.&lt;/p&gt;

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

&lt;p&gt;Re-renders are the most over-optimized problem in React Native. Wrapping every component in &lt;code&gt;React.memo&lt;/code&gt; bloats your bundle and slows down mounting. The rule for 2026: &lt;strong&gt;profile first, memoize where the profiler shows a hot path.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That said, four re-render anti-patterns cause most real performance bugs, and they're worth pattern-matching against:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Inline object props.&lt;/strong&gt; &lt;code&gt;&amp;lt;Component style={{ margin: 8 }} /&amp;gt;&lt;/code&gt; creates a new object on every parent render, which invalidates every downstream &lt;code&gt;React.memo&lt;/code&gt;. Hoist styles into &lt;code&gt;StyleSheet.create&lt;/code&gt; or a constant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inline handler props.&lt;/strong&gt; &lt;code&gt;&amp;lt;Button onPress={() =&amp;gt; doThing(id)} /&amp;gt;&lt;/code&gt; creates a new function every render. Use &lt;code&gt;useCallback&lt;/code&gt;, or better, move the handler inside a memoized child that owns the id.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context provider sprawl.&lt;/strong&gt; Every value change in a large context re-renders every consumer. Split contexts by update frequency: one for user identity (rarely changes), one for theme (rarely changes), one for live data (changes often). Do not put them all in a single context.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Selector reference issues.&lt;/strong&gt; Redux/Zustand selectors that return new object references on every store update will re-render every consumer even if the values are identical. Use shallow equality checks or memoized selectors.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;code&gt;why-did-you-render&lt;/code&gt; package is still the fastest way to find these in dev; it logs to console every time a memoized component re-renders unnecessarily.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Move animations off the JS thread
&lt;/h2&gt;

&lt;p&gt;An animation running on the JS thread will drop frames the moment JS is busy, which is always, because JS is where your business logic runs. In 2026 there is no reason to run animations on the JS thread at all.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reanimated 4&lt;/strong&gt; for anything driven by state, gesture, or timing. Its worklets run on the UI thread as native functions: a 60fps spring animation runs at 60fps even while the JS thread is parsing a 500KB JSON response.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;React Native Gesture Handler&lt;/strong&gt; for anything the user drags, swipes, or pinches. The old &lt;code&gt;PanResponder&lt;/code&gt; API is JS-thread-bound and will jank; Gesture Handler runs on the UI thread.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skia&lt;/strong&gt; for anything painterly: charts, custom drawing, complex transitions. It bypasses the React view tree entirely and renders directly to a GPU canvas.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rule: if you are writing &lt;code&gt;Animated.Value&lt;/code&gt; with &lt;code&gt;useNativeDriver: false&lt;/code&gt;, stop. Convert it to Reanimated or figure out why &lt;code&gt;useNativeDriver: true&lt;/code&gt; isn't an option (usually it's an animatable prop that Reanimated supports and old Animated doesn't).&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Cold start, the number your users actually notice
&lt;/h2&gt;

&lt;p&gt;Cold start is the sum of native init + JS bundle load + JS bundle execute + first render. The wins, in order of impact:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;New Architecture on&lt;/strong&gt; (Step 2). Biggest single win.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hermes bytecode precompiled&lt;/strong&gt; (Step 3). Second biggest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bundle size reduction.&lt;/strong&gt; Every 100 KB of JS costs real parse+execute time on a mid-tier Android. Run &lt;code&gt;npx react-native-bundle-visualizer&lt;/code&gt; and delete what surprises you. &lt;code&gt;moment.js&lt;/code&gt;, &lt;code&gt;lodash&lt;/code&gt; (full), and 5 different date-picker libraries you forgot you had are the usual suspects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lazy-load routes.&lt;/strong&gt; Everything under &lt;code&gt;React.lazy&lt;/code&gt; + &lt;code&gt;Suspense&lt;/code&gt; at the route boundary. Your login screen does not need to parse your dashboard's 400KB of code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Native module audit.&lt;/strong&gt; Every linked native module runs its init code at startup. Remove modules you no longer use. Even removed JS imports leave native code linked until you unlink.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Font loading.&lt;/strong&gt; Preload only the weights you use on the initial screen. &lt;code&gt;Font.loadAsync&lt;/code&gt; for 12 weights blocks first paint for hundreds of ms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Splash screen strategy.&lt;/strong&gt; Keep the native splash visible until the first interactive screen is ready to render; don't cross-fade too early or you get a flash of empty content.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Instrument this with &lt;code&gt;AppRegistry.setWrapperComponentProvider&lt;/code&gt; plus a manual mark at the first meaningful paint, log it to your analytics, and watch the median (not the mean; the mean lies).&lt;/p&gt;

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

&lt;p&gt;Memory leaks in React Native rarely crash the app. They just make it slower, and slower, and eventually the OS kills it in the background and users think "the app forgot me." The three sources that account for almost every leak in the wild:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Uncleaned event listeners.&lt;/strong&gt; Every &lt;code&gt;addEventListener&lt;/code&gt;, every &lt;code&gt;subscribe&lt;/code&gt;, every &lt;code&gt;AppState&lt;/code&gt; handler needs its cleanup in the &lt;code&gt;useEffect&lt;/code&gt; return. The exhaustive-deps ESLint rule will not catch these; you have to review them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image caching without bounds.&lt;/strong&gt; &lt;code&gt;react-native-fast-image&lt;/code&gt; caches aggressively. Set a &lt;code&gt;maxMemoryPolicy&lt;/code&gt; or your app will happily hold hundreds of MB of thumbnails. Same story for any list that renders image-heavy rows: use &lt;code&gt;removeClippedSubviews&lt;/code&gt; and let FlashList recycle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Uncleared timers and intervals.&lt;/strong&gt; &lt;code&gt;setInterval&lt;/code&gt; inside a component that mounts and unmounts on navigation is the classic leak.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Take a heap snapshot after 10 minutes of use, then again after 30, and compare. If the delta isn't roughly flat, you have a leak. The tool for this on iOS is Instruments' Allocations; on Android it's Android Studio's Memory Profiler.&lt;/p&gt;

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

&lt;p&gt;Users don't distinguish between "the app is slow" and "the network is slow." Your job is to hide the network from them.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;TanStack Query&lt;/strong&gt; for every fetch that has a cache key. Deduplication, background refetch, stale-while-revalidate: you get it for free. The single library that most improved perceived React Native performance in the last two years.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HTTP/2 or HTTP/3&lt;/strong&gt; on your API. If you're on HTTP/1.1 you're paying a full round trip per request. Cloudflare, Fastly, AWS ALB all default to HTTP/2 now.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image CDN with responsive sizing.&lt;/strong&gt; Serve 400×400 to the phone, not 4000×4000. Cloudinary, imgix, and Cloudflare Images all do this automatically from URL parameters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parallelize independent requests.&lt;/strong&gt; &lt;code&gt;Promise.all&lt;/code&gt; for anything that doesn't depend on the previous result.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optimistic updates.&lt;/strong&gt; For any user action that will succeed 99% of the time, update the UI immediately and roll back on error. Users feel every millisecond you make them wait for a spinner.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Everything above is worthless if a well-meaning teammate ships a regression next Tuesday. The fix is a performance budget in CI, and the tool for it in 2026 is &lt;strong&gt;Flashlight&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Flashlight runs a scripted E2E test on a real (or emulated) Android device, records CPU/FPS/memory across the run, and fails the build if any metric exceeds the budget you set. Point it at your five most important user flows (cold start, login, main list scroll, detail view, checkout) and set the budgets 10–20% above your current numbers. Now every PR that regresses performance breaks CI, and the regression conversation happens in code review instead of in the App Store reviews six weeks later.&lt;/p&gt;

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

&lt;p&gt;Optimizing without measurement is how apps end up with 400 KB of memoization wrapping and no measurable difference. Three reasons to hold back:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The profiler doesn't show a hotspot.&lt;/strong&gt; Then you're not fixing anything, you're just writing code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The user cannot perceive the change.&lt;/strong&gt; Nobody notices a 12 ms → 8 ms improvement on a screen that's already at 60 fps. They notice 30 fps → 60 fps. Chase the visible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The optimization removes a real feature.&lt;/strong&gt; Slower with the feature &amp;gt; faster without it, unless the feature is optional.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Modern AI mobile app builders like &lt;a href="https://www.rapidnative.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=react-native-performance-optimization-guide-2026" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt; have quietly closed a large part of this gap. When you describe an app in plain words and generate a real React Native + Expo project, the output already has the New Architecture enabled, Hermes on, FlashList for meaningful lists, Reanimated wired up, and TanStack Query for data, because those are the current defaults, and the model was trained on the current stack.&lt;/p&gt;

&lt;p&gt;What AI generation does not do (yet, reliably) is your app's specific measurement work. It cannot know that your product screen renders 400 image cards and needs a memoized row component, or that your context provider has grown three orders of magnitude too much state. The playbook above is what you bring to a generated app: you get the correct baseline for free, and then you profile, measure, and tune what matters for your users.&lt;/p&gt;

&lt;p&gt;If you're building a new React Native app in 2026, the fastest path is: generate the correct baseline, ship the first version, measure real user performance, and apply this playbook to what your profiler actually flags. Skip the weeks of boilerplate; spend that time on the parts of your app only you understand.&lt;/p&gt;

&lt;p&gt;What's the optimization that actually moved your numbers, and what did you measure it with? Drop it in the comments; the profiler-verified wins are the ones worth collecting.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>performance</category>
      <category>expo</category>
      <category>mobile</category>
    </item>
    <item>
      <title>The App Store Privacy Declaration Checklist Nobody Gives You</title>
      <dc:creator>Jules Sarah</dc:creator>
      <pubDate>Fri, 07 Aug 2026 07:36:45 +0000</pubDate>
      <link>https://dev.to/jules_sarah_0718e958f0d24/the-app-store-privacy-declaration-checklist-nobody-gives-you-1cd0</link>
      <guid>https://dev.to/jules_sarah_0718e958f0d24/the-app-store-privacy-declaration-checklist-nobody-gives-you-1cd0</guid>
      <description>&lt;ul&gt;
&lt;li&gt;Google Play's data safety form and Apple's &lt;code&gt;PrivacyInfo.xcprivacy&lt;/code&gt; are the same idea in two formats, and both reject apps whose declarations don't match reality.&lt;/li&gt;
&lt;li&gt;Audit every SDK before touching either form: what it collects, linked or unlinked, and the purpose.&lt;/li&gt;
&lt;li&gt;The six categories that catch first-timers: crash reporting, IP addresses, purchase history, diagnostics, ad SDKs, account creation.&lt;/li&gt;
&lt;li&gt;Apple's manifest has been enforced since May 2024. Missing entries surface as App Store Connect warnings, then rejections.&lt;/li&gt;
&lt;li&gt;Automate the mapping: keep an SDK inventory file and regenerate the declaration on every release.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every mobile app submission has a wall you don't see coming. Not the review queue, not screenshot rejections: the privacy declaration. Google Play's data safety section and Apple's privacy manifest are the same idea in two formats, and both reject an app whose declaration doesn't match reality.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters
&lt;/h2&gt;

&lt;p&gt;An incorrect privacy declaration is not a technical bug, it is a policy violation. Google Play rejects the update, and repeated violations put the developer account at risk. Apple's privacy manifest (&lt;code&gt;PrivacyInfo.xcprivacy&lt;/code&gt;, enforced since May 2024) surfaces missing entries as App Store Connect warnings, then rejections.&lt;/p&gt;

&lt;p&gt;Most indie developers fill these forms from memory five minutes before submitting, then spend the next week resubmitting because they forgot Sentry collects device identifiers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The full inventory audit
&lt;/h2&gt;

&lt;p&gt;Before touching either form, list every third-party SDK in the app. For each, note three things: what it collects, whether that data is linked to the user (account ID or device ID) or unlinked, and whether it serves analytics, personalization, advertising, or app functionality.&lt;/p&gt;

&lt;p&gt;Common indie-app SDKs and what they collect:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;SDK&lt;/th&gt;
&lt;th&gt;Collects&lt;/th&gt;
&lt;th&gt;Linked?&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Firebase Analytics&lt;/td&gt;
&lt;td&gt;Device ID, session data, user actions&lt;/td&gt;
&lt;td&gt;Linked (Firebase user ID)&lt;/td&gt;
&lt;td&gt;Analytics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sentry&lt;/td&gt;
&lt;td&gt;Device model, OS version, stack traces&lt;/td&gt;
&lt;td&gt;Unlinked unless user context attached&lt;/td&gt;
&lt;td&gt;Diagnostics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RevenueCat&lt;/td&gt;
&lt;td&gt;Purchase history, subscription state&lt;/td&gt;
&lt;td&gt;Linked (RevenueCat user ID)&lt;/td&gt;
&lt;td&gt;App functionality&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mixpanel / PostHog&lt;/td&gt;
&lt;td&gt;User actions, custom events&lt;/td&gt;
&lt;td&gt;Linked (your user ID)&lt;/td&gt;
&lt;td&gt;Analytics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AdMob / AppLovin&lt;/td&gt;
&lt;td&gt;Device ID, ad interaction&lt;/td&gt;
&lt;td&gt;Linked&lt;/td&gt;
&lt;td&gt;Advertising&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This audit output is what you reference against every question on both platforms' forms.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 6 categories that trip up first-time submitters
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Crash reporting.&lt;/strong&gt; Counts as collection even if you never look at it. Sentry, Crashlytics, Bugsnag all collect declarable types.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IP addresses.&lt;/strong&gt; Every network request logs an IP somewhere. If your backend logs IPs (it does), you collect device identifiers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Purchase history.&lt;/strong&gt; StoreKit or Play Billing plus RevenueCat means financial info, even though card data never touches your servers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Diagnostic data.&lt;/strong&gt; Performance metrics, ANR reports, custom logs. All collection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ad SDK data.&lt;/strong&gt; Any monetization SDK means collecting for advertising. The most common source of rejections.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Account creation.&lt;/strong&gt; Email, password hash, phone: always linked, always declared.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Google Play vs App Store
&lt;/h2&gt;

&lt;p&gt;Same intent, different formats.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Google Play&lt;/th&gt;
&lt;th&gt;App Store&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Format&lt;/td&gt;
&lt;td&gt;Question-based Console form&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;PrivacyInfo.xcprivacy&lt;/code&gt; plist in the bundle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Structure&lt;/td&gt;
&lt;td&gt;~40 questions across 14 data categories&lt;/td&gt;
&lt;td&gt;Typed entries per data type plus required-reason API codes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Filled&lt;/td&gt;
&lt;td&gt;In the Play Console UI&lt;/td&gt;
&lt;td&gt;In code, merged from your app and every SDK's own manifest&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Xcode 15+ merges your manifest with every SDK's bundled manifest into one privacy report, which feeds the App Store label. Fill the two platforms out separately; there is no shared source unless you build one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Copy-paste starting point
&lt;/h2&gt;

&lt;p&gt;For a typical indie app (analytics + crash reporting + payments):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;You use&lt;/th&gt;
&lt;th&gt;Play form answer&lt;/th&gt;
&lt;th&gt;Apple manifest data type&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Firebase Analytics&lt;/td&gt;
&lt;td&gt;Collects: Device IDs, App interactions. Purpose: Analytics&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;NSPrivacyCollectedDataTypeDeviceID&lt;/code&gt;, &lt;code&gt;NSPrivacyCollectedDataTypeProductInteraction&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sentry&lt;/td&gt;
&lt;td&gt;Collects: Crash logs, Diagnostics&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;NSPrivacyCollectedDataTypeCrashData&lt;/code&gt;, &lt;code&gt;NSPrivacyCollectedDataTypePerformanceData&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RevenueCat&lt;/td&gt;
&lt;td&gt;Collects: Purchase history&lt;/td&gt;
&lt;td&gt;&lt;code&gt;NSPrivacyCollectedDataTypePurchaseHistory&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A starting point, not an answer key: verify each SDK's current documentation at submission time, since collection profiles change between versions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automate this, don't guess
&lt;/h2&gt;

&lt;p&gt;Maintain a spreadsheet or JSON file with your SDK inventory and each one's collection profile, and regenerate the declaration on every release instead of recalling it. CI tooling such as &lt;a href="https://www.letsdeploy.it/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=app-store-privacy-declaration-checklist" rel="noopener noreferrer"&gt;LetsDeploy&lt;/a&gt; can diff your dependency list against the last declared inventory and fail the build when a new SDK appears undeclared. Fifteen minutes building this system saves days of rejection cycles per year.&lt;/p&gt;

&lt;p&gt;What's the SDK that surprised you most on a privacy form? Mine was learning that crash logs count. Drop yours in the comments.&lt;/p&gt;

</description>
      <category>ios</category>
      <category>android</category>
      <category>mobile</category>
      <category>privacy</category>
    </item>
    <item>
      <title>The Onboarding Chapter Missing From Most SaaS Template Reviews</title>
      <dc:creator>Jules Sarah</dc:creator>
      <pubDate>Thu, 06 Aug 2026 09:37:42 +0000</pubDate>
      <link>https://dev.to/jules_sarah_0718e958f0d24/the-onboarding-chapter-missing-from-most-saas-template-reviews-5311</link>
      <guid>https://dev.to/jules_sarah_0718e958f0d24/the-onboarding-chapter-missing-from-most-saas-template-reviews-5311</guid>
      <description>&lt;h1&gt;
  
  
  The Onboarding Chapter Missing From Most SaaS Template Reviews
&lt;/h1&gt;

&lt;p&gt;Most SaaS template reviews follow the same format.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which framework does it use?&lt;/li&gt;
&lt;li&gt;Does it support authentication?&lt;/li&gt;
&lt;li&gt;Which database does it ship with?&lt;/li&gt;
&lt;li&gt;Is the codebase clean?&lt;/li&gt;
&lt;li&gt;Is it worth the price?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those questions matter.&lt;/p&gt;

&lt;p&gt;But they rarely cover the part that determines whether your users actually stick around:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The onboarding experience.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A modern tech stack gets people through signup.&lt;/p&gt;

&lt;p&gt;A thoughtful onboarding flow gets them to their first success.&lt;/p&gt;




&lt;h2&gt;
  
  
  The tech stack tells you nothing about retention
&lt;/h2&gt;

&lt;p&gt;A template with the latest version of Next.js, a polished UI, and perfect TypeScript can still lose users before they ever experience the product's value.&lt;/p&gt;

&lt;p&gt;Meanwhile, a template running a slightly older stack—but with a carefully designed onboarding experience—can create happier users from day one.&lt;/p&gt;

&lt;p&gt;The framework determines what's possible.&lt;/p&gt;

&lt;p&gt;The onboarding determines whether users ever get there.&lt;/p&gt;




&lt;h2&gt;
  
  
  What a good onboarding flow contains
&lt;/h2&gt;

&lt;p&gt;After reviewing dozens of SaaS templates, the best ones usually include most of the following.&lt;/p&gt;

&lt;h3&gt;
  
  
  Users land somewhere useful
&lt;/h3&gt;

&lt;p&gt;Nobody likes opening a blank application.&lt;/p&gt;

&lt;p&gt;If it's a project management app, preload a sample project.&lt;/p&gt;

&lt;p&gt;If it's an analytics product, show demo charts.&lt;/p&gt;

&lt;p&gt;If it's a CRM, include sample contacts.&lt;/p&gt;

&lt;p&gt;An empty product feels unfinished.&lt;/p&gt;

&lt;p&gt;A populated product teaches by example.&lt;/p&gt;




&lt;h3&gt;
  
  
  A short first-run experience
&lt;/h3&gt;

&lt;p&gt;Not a ten-minute walkthrough.&lt;/p&gt;

&lt;p&gt;Not a feature tour.&lt;/p&gt;

&lt;p&gt;Just three to five focused steps that help users complete the product's primary workflow.&lt;/p&gt;

&lt;p&gt;The goal isn't education.&lt;/p&gt;

&lt;p&gt;The goal is momentum.&lt;/p&gt;




&lt;h3&gt;
  
  
  Activation events are already wired
&lt;/h3&gt;

&lt;p&gt;The best templates don't just include analytics.&lt;/p&gt;

&lt;p&gt;They already track the moments that matter.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;trackEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;signup_completed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;trackEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;workspace_created&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;trackEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;first_invite_sent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;trackEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;integration_connected&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;trackEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;subscription_started&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can always replace the analytics provider later.&lt;/p&gt;

&lt;p&gt;What's valuable is that somebody already identified the events worth measuring.&lt;/p&gt;




&lt;h3&gt;
  
  
  Empty states teach
&lt;/h3&gt;

&lt;p&gt;Good empty states answer the question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"What should I do next?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Bad empty states say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;No projects yet.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Good empty states say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You don't have any projects yet. Start by creating one for your team's current goal.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's product design—not placeholder text.&lt;/p&gt;




&lt;h3&gt;
  
  
  Trial status is obvious
&lt;/h3&gt;

&lt;p&gt;If users are on a 14-day trial, they should always know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How many days remain&lt;/li&gt;
&lt;li&gt;What happens when it ends&lt;/li&gt;
&lt;li&gt;What features they're currently using&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No surprises.&lt;/p&gt;




&lt;h3&gt;
  
  
  The first week is already planned
&lt;/h3&gt;

&lt;p&gt;Great templates include more than a welcome email.&lt;/p&gt;

&lt;p&gt;They ship with an onboarding sequence like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Day 0 — Welcome&lt;/li&gt;
&lt;li&gt;Day 2 — First success tips&lt;/li&gt;
&lt;li&gt;Day 5 — Feature discovery&lt;/li&gt;
&lt;li&gt;Day 10 — Trial reminder&lt;/li&gt;
&lt;li&gt;Trial ended — Next steps&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That saves teams days of writing and experimentation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Ship activation events, not just dashboards
&lt;/h2&gt;

&lt;p&gt;Every SaaS template includes an admin dashboard.&lt;/p&gt;

&lt;p&gt;Very few include meaningful analytics instrumentation.&lt;/p&gt;

&lt;p&gt;The templates worth adopting already track moments like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Signup completed&lt;/li&gt;
&lt;li&gt;First workspace created&lt;/li&gt;
&lt;li&gt;First invite sent&lt;/li&gt;
&lt;li&gt;First integration connected&lt;/li&gt;
&lt;li&gt;First subscription started&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The code itself isn't complicated.&lt;/p&gt;

&lt;p&gt;What's valuable is the product thinking behind it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Empty states are a UX exercise
&lt;/h2&gt;

&lt;p&gt;Every empty screen is an opportunity to help users.&lt;/p&gt;

&lt;p&gt;Compare these two messages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Version A&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;No items found.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Version B&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You don't have any projects yet. Most teams begin by creating a project around their current quarterly goal.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;One creates confusion.&lt;/p&gt;

&lt;p&gt;The other creates momentum.&lt;/p&gt;

&lt;p&gt;When reviewing a SaaS template, browse every screen with zero data.&lt;/p&gt;

&lt;p&gt;If every page says "No items yet," you'll spend weeks fixing onboarding gaps after launch.&lt;/p&gt;




&lt;h2&gt;
  
  
  How to spot templates that took onboarding seriously
&lt;/h2&gt;

&lt;p&gt;Look for these signals:&lt;/p&gt;

&lt;p&gt;✅ Demo ships with realistic sample data&lt;/p&gt;

&lt;p&gt;✅ First-run onboarding is built into the demo&lt;/p&gt;

&lt;p&gt;✅ Activation events are already instrumented&lt;/p&gt;

&lt;p&gt;✅ Empty states teach users what to do next&lt;/p&gt;

&lt;p&gt;✅ Subscription and trial status are visible&lt;/p&gt;

&lt;p&gt;✅ Transactional onboarding emails are included&lt;/p&gt;

&lt;p&gt;Templates that check every box are surprisingly rare.&lt;/p&gt;

&lt;p&gt;When you find one, you're paying for product thinking—not just code.&lt;/p&gt;




&lt;h2&gt;
  
  
  Applighter's take
&lt;/h2&gt;

&lt;p&gt;At &lt;a href="https://applighter.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=onboarding_template_reviews" rel="noopener noreferrer"&gt;Applighter&lt;/a&gt;, we've learned that onboarding deserves the same attention as architecture.&lt;/p&gt;

&lt;p&gt;That's why our templates include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sample workspaces from day one&lt;/li&gt;
&lt;li&gt;Activation events wired into the product&lt;/li&gt;
&lt;li&gt;Empty states that guide instead of confuse&lt;/li&gt;
&lt;li&gt;A complete first-week onboarding email sequence&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Whether you choose Applighter or another template, evaluate onboarding with the same rigor you evaluate the tech stack.&lt;/p&gt;

&lt;p&gt;Because users don't retain because your framework is modern.&lt;/p&gt;

&lt;p&gt;They retain because they know what to do next.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;The SaaS template ecosystem spends a lot of time comparing frameworks.&lt;/p&gt;

&lt;p&gt;Users don't.&lt;/p&gt;

&lt;p&gt;They care about one thing:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I succeed in the first five minutes?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The best templates answer that question before the user ever has to ask it.&lt;/p&gt;

&lt;p&gt;That's why onboarding deserves to be part of every SaaS template review.&lt;/p&gt;

</description>
      <category>saas</category>
      <category>ux</category>
      <category>onboarding</category>
      <category>product</category>
    </item>
    <item>
      <title>Ship a React Native app in days, not months</title>
      <dc:creator>Jules Sarah</dc:creator>
      <pubDate>Fri, 31 Jul 2026 10:38:06 +0000</pubDate>
      <link>https://dev.to/jules_sarah_0718e958f0d24/ship-a-react-native-app-in-days-not-months-3hfa</link>
      <guid>https://dev.to/jules_sarah_0718e958f0d24/ship-a-react-native-app-in-days-not-months-3hfa</guid>
      <description>&lt;p&gt;If you've ever started a React Native project on Monday and looked up on Friday to realize you've written zero product code, only auth, DB migrations, and EAS config, this post is for you.&lt;/p&gt;

&lt;p&gt;Applighter customers ship in days, not months. Not because they're faster developers, but because the patterns below are already in the repo when they clone it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup tax
&lt;/h2&gt;

&lt;p&gt;Every React Native project pays it:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Days&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Auth (email + OAuth)&lt;/td&gt;
&lt;td&gt;3–5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DB schema + RLS&lt;/td&gt;
&lt;td&gt;2–4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stripe + entitlements&lt;/td&gt;
&lt;td&gt;3–5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI streaming&lt;/td&gt;
&lt;td&gt;4–7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EAS build/submit&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Design system&lt;/td&gt;
&lt;td&gt;2–3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Push&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~4 weeks&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Before line one of your product code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 1: one theming primitive
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/apps/lib/theme.ts&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;applyProductTheme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;brandHex&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;hsl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hexToHsl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;brandHex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;// propagates to background, foreground, primary, secondary,&lt;/span&gt;
  &lt;span class="c1"&gt;// muted, accent, destructive — all HSL-derived&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;buildPalette&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hsl&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;One color in, full palette out. Rebrand an app in 20 minutes, not a weekend of grep-and-replace.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 2: hybrid data provider
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/apps/lib/data/provider.ts&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;getProductData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&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;fetchFromSupabase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slug&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;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;staticFallback&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;// ships in the bundle&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cloned repo runs on the simulator without env vars. First-run latency: 60 seconds, not 90 minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 3: RLS pre-baked
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- supabase/migrations/20260112085737_create_transactions_table.sql&lt;/span&gt;
&lt;span class="k"&gt;alter&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;transactions&lt;/span&gt; &lt;span class="n"&gt;enable&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt; &lt;span class="k"&gt;level&lt;/span&gt; &lt;span class="k"&gt;security&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"users read own transactions"&lt;/span&gt;
  &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;transactions&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt;
  &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every table ships with the policy. You audit, you don't design.&lt;/p&gt;

&lt;p&gt;Reference: &lt;a href="https://supabase.com/docs/guides/database/postgres/row-level-security" rel="noopener noreferrer"&gt;Supabase RLS docs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 4: Stripe → grant → download
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/api/webhook/stripe/route.ts&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;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;checkout.session.completed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;isBundle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;targets&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;resolveGrantTargets&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;productId&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;grantProducts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;licenseType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;stripe_purchase&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;targets&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;sendReceiptEmail&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;downloadUrl&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;Bundles expand into individual grants. Guest checkouts validated by &lt;code&gt;session_id&lt;/code&gt;. Single/Multiple/Enterprise license types. It's the code you'd write on day 4 of a Stripe integration, except it's already there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 5: EAS ready
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;eas.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"development"&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;"developmentClient"&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;"distribution"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"internal"&lt;/span&gt;&lt;span class="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;"preview"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"distribution"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"internal"&lt;/span&gt;&lt;span class="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;"production"&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;"autoIncrement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"submit"&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;"production"&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;&lt;code&gt;/ship&lt;/code&gt; slash command → &lt;code&gt;eas build --profile production &amp;amp;&amp;amp; eas submit&lt;/code&gt;. See &lt;a href="https://docs.expo.dev/build/introduction/" rel="noopener noreferrer"&gt;Expo EAS docs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 6: agent-legible codebase
&lt;/h2&gt;

&lt;p&gt;Every template ships with a &lt;code&gt;.claude/&lt;/code&gt; directory of slash commands. &lt;code&gt;/add-screen&lt;/code&gt;, &lt;code&gt;/swap-backend&lt;/code&gt;, &lt;code&gt;/audit-security&lt;/code&gt;. They work because the codebase is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Small files, obvious responsibilities&lt;/li&gt;
&lt;li&gt;NativeWind classes (Tailwind semantics)&lt;/li&gt;
&lt;li&gt;Typed data providers&lt;/li&gt;
&lt;li&gt;Colocated components with their tests&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An agent (Claude Code, Cursor) can extend it without wandering. That's a real speed multiplier in 2026.&lt;/p&gt;

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;From scratch&lt;/th&gt;
&lt;th&gt;Free boilerplate&lt;/th&gt;
&lt;th&gt;Applighter&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Time to TestFlight&lt;/td&gt;
&lt;td&gt;4–6 weeks&lt;/td&gt;
&lt;td&gt;2–3 weeks&lt;/td&gt;
&lt;td&gt;3–5 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backend&lt;/td&gt;
&lt;td&gt;You build&lt;/td&gt;
&lt;td&gt;Not included&lt;/td&gt;
&lt;td&gt;Supabase, wired&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Payments&lt;/td&gt;
&lt;td&gt;You build&lt;/td&gt;
&lt;td&gt;Not included&lt;/td&gt;
&lt;td&gt;Stripe + grants&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RLS&lt;/td&gt;
&lt;td&gt;You design&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;Pre-baked&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Updates&lt;/td&gt;
&lt;td&gt;You maintain&lt;/td&gt;
&lt;td&gt;Repo-dependent&lt;/td&gt;
&lt;td&gt;Lifetime&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost&lt;/td&gt;
&lt;td&gt;~$8–20k dev time&lt;/td&gt;
&lt;td&gt;Free + hidden time&lt;/td&gt;
&lt;td&gt;$79 one-time&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What ships in days vs. weeks
&lt;/h2&gt;

&lt;p&gt;Bimodal distribution across ~500 customers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;3–5 days&lt;/strong&gt;: rebrand-and-launch on an existing template (weather, fitness, e-learning)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2–4 weeks&lt;/strong&gt;: novel AI product built on template infrastructure (voice notes, PDF chat, calorie tracker)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both compress the ground-up equivalent by ~4x.&lt;/p&gt;

&lt;h2&gt;
  
  
  When not to use a template
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Building something the data model doesn't stretch to fit (games, real-time collab)&lt;/li&gt;
&lt;li&gt;Learning React Native for its own sake&lt;/li&gt;
&lt;li&gt;Need a stack we don't ship (Firebase, Convex, bare RN)&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;strong&gt;Do I need Supabase experience?&lt;/strong&gt; No. Templates run with the DB seeded.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I skip the theming layer?&lt;/strong&gt; Yes. Default colors ship out of the box.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if a new Expo SDK breaks it?&lt;/strong&gt; Updates are pushed to your dashboard. Pull and diff.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Refunds?&lt;/strong&gt; 7-day money-back. Policy is on the site.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try one
&lt;/h2&gt;

&lt;p&gt;Browse the &lt;a href="https://www.applighter.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=why-applighter-customers-ship-in-days-not-months" rel="noopener noreferrer"&gt;full template catalog&lt;/a&gt;. $79 one-time, full source, lifetime updates. Ship this week.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>supabase</category>
      <category>mobile</category>
    </item>
    <item>
      <title>The React Native template launch that made us $87</title>
      <dc:creator>Jules Sarah</dc:creator>
      <pubDate>Wed, 29 Jul 2026 04:34:56 +0000</pubDate>
      <link>https://dev.to/jules_sarah_0718e958f0d24/the-react-native-template-launch-that-made-us-87-kdb</link>
      <guid>https://dev.to/jules_sarah_0718e958f0d24/the-react-native-template-launch-that-made-us-87-kdb</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; This is the honest developer-story version of an internal postmortem. If you want a shorter, more meme-friendly version, we have one on Medium. If you want a first, code-heavy version, it's on Dev.to. This one is the long form.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Where we started
&lt;/h2&gt;

&lt;p&gt;Two founders. 180 combined hours of work. One React Native template we were quietly proud of. Twelve screens, Expo SDK, TypeScript, NativeWind, tab-based navigation, auth screens that looked like Instagram's. We priced it at $29 and launched on a Tuesday morning.&lt;/p&gt;

&lt;p&gt;By Friday evening we had:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;5 sales&lt;/li&gt;
&lt;li&gt;4 refund requests&lt;/li&gt;
&lt;li&gt;1 chargeback in flight&lt;/li&gt;
&lt;li&gt;$87 in the bank&lt;/li&gt;
&lt;li&gt;1 email from a customer named "Dev" (probably not his real name) that started with "Hey guys, I have to be honest with you..."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That email is where this post starts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The email that changed the company
&lt;/h2&gt;

&lt;p&gt;The paraphrased version:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I thought this was going to save me a week. It saved me maybe an evening. The auth doesn't connect to anything, the API returns hardcoded data, and I still have to build the entire backend. Why did I pay $29 for what's basically a Figma export in TypeScript?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Every sentence was honest. We had shipped a UI kit. Our landing page used the phrase "full-stack" four times. In the repo, "full-stack" meant this:&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;// src/services/mockApi.ts&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;HARDCODED_NOTES&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Note&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&lt;/span&gt;&lt;span class="dl"&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Meeting notes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="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="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2&lt;/span&gt;&lt;span class="dl"&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Idea&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="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="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;getNotes&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Note&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;await&lt;/span&gt; &lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// for that "loading" feel&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;HARDCODED_NOTES&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 auth screens looked professional and did nothing. The Supabase env vars were placeholders. We had, to be blunt, defrauded the word "full-stack."&lt;/p&gt;

&lt;h2&gt;
  
  
  The audit
&lt;/h2&gt;

&lt;p&gt;Before rebuilding, we forced ourselves to write down every layer where v1 fell short of the promise on the landing page.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Landing page promise&lt;/th&gt;
&lt;th&gt;v1 reality&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;"Postgres schema, migrations, seed data"&lt;/td&gt;
&lt;td&gt;Zero SQL files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth&lt;/td&gt;
&lt;td&gt;"Supabase Auth with email + OAuth"&lt;/td&gt;
&lt;td&gt;UI screens returning fake sessions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage&lt;/td&gt;
&lt;td&gt;"File uploads with signed URLs"&lt;/td&gt;
&lt;td&gt;localStorage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI&lt;/td&gt;
&lt;td&gt;"OpenAI + Anthropic integration"&lt;/td&gt;
&lt;td&gt;Not present&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RLS&lt;/td&gt;
&lt;td&gt;"Row-level security policies"&lt;/td&gt;
&lt;td&gt;Not present&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Docs&lt;/td&gt;
&lt;td&gt;"Complete setup guide"&lt;/td&gt;
&lt;td&gt;800-word README&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Demo&lt;/td&gt;
&lt;td&gt;"See it in action"&lt;/td&gt;
&lt;td&gt;Six screenshots&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The audit was demoralizing. It was also the roadmap.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rebuild, layer by layer
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Layer 1: real Postgres schema
&lt;/h3&gt;

&lt;p&gt;Every current template ships with a &lt;code&gt;supabase/migrations/&lt;/code&gt; folder containing the actual tables the app needs, not a hand-wavy ER diagram. Buyers can run &lt;code&gt;supabase db push&lt;/code&gt; and have a working schema before they've written a single line of code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- supabase/migrations/00_notes.sql&lt;/span&gt;
&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;notes&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;primary&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="k"&gt;references&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;delete&lt;/span&gt; &lt;span class="k"&gt;cascade&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;alter&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;notes&lt;/span&gt; &lt;span class="n"&gt;enable&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt; &lt;span class="k"&gt;level&lt;/span&gt; &lt;span class="k"&gt;security&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"Users read their own notes"&lt;/span&gt;
  &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;notes&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt;
  &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Layer 2: auth that actually authenticates
&lt;/h3&gt;

&lt;p&gt;Email, Apple, and Google via Supabase Auth, with session hooks that persist across app restarts and refresh silently. The auth screens no longer lie.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 3: storage with signed URLs and RLS
&lt;/h3&gt;

&lt;p&gt;For templates like &lt;a href="https://www.applighter.com/apps/chat-with-pdf?utm_source=hashnode&amp;amp;utm_medium=blog&amp;amp;utm_campaign=applighter-template-postmortem" rel="noopener noreferrer"&gt;Chat with PDF&lt;/a&gt;, file uploads go straight to Supabase Storage with a signed URL flow and per-user RLS. No customer support ticket has ever said "the file upload doesn't work."&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 4: real AI service layer
&lt;/h3&gt;

&lt;p&gt;An Edge Function proxies OpenAI, Anthropic, or ElevenLabs calls using the buyer's own API keys. It's modular: swap GPT-4o for Claude by changing a config constant. No vendor lock-in.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 5: RLS policies from day one
&lt;/h3&gt;

&lt;p&gt;Every template ships with per-table policies and two seed users so buyers can immediately verify that user A cannot see user B's data. This is table-stakes for production apps, and yet somehow rare in the template market.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 6: 12,000-word docs, plus video
&lt;/h3&gt;

&lt;p&gt;The single highest-leverage rebuild task. Written as copy-pasteable step lists with expected output. If a step is ambiguous, it gets rewritten.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 7: agent-readiness
&lt;/h3&gt;

&lt;p&gt;The one most competitors still ignore. Every template ships with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A root &lt;code&gt;CLAUDE.md&lt;/code&gt; describing the codebase in agent-friendly terms&lt;/li&gt;
&lt;li&gt;Slash commands and skills for the common workflows (&lt;code&gt;/add-screen&lt;/code&gt;, &lt;code&gt;/add-supabase-table&lt;/code&gt;, &lt;code&gt;/swap-ai-provider&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Real TypeScript types across the board (no &lt;code&gt;any&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;A file layout an agent can grep in one pass&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's tested against Claude Code, Codex, Cursor, OpenCode, and Windsurf, because in 2026 the buyer's &lt;em&gt;first&lt;/em&gt; interaction with your codebase is via an agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing: from $29 to $79
&lt;/h2&gt;

&lt;p&gt;Counterintuitive lesson: raising the price improved every downstream metric.&lt;/p&gt;

&lt;p&gt;At $29, we attracted price-shopping buyers whose expectations were "this must also do my dishes." Refund rate was around 40%.&lt;/p&gt;

&lt;p&gt;At $79, we attracted senior indies who value their own hour at $100+ and are buying back time. Refund rate dropped to around 4%.&lt;/p&gt;

&lt;p&gt;The right customer for a React Native template is not price-sensitive. They are time-sensitive. Price signals seriousness.&lt;/p&gt;

&lt;h2&gt;
  
  
  The demo video, in detail
&lt;/h2&gt;

&lt;p&gt;40 seconds. Silent. One take. This is the shot list:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Terminal shows &lt;code&gt;git clone&lt;/code&gt;, then &lt;code&gt;npx expo start&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;iOS Simulator boots&lt;/li&gt;
&lt;li&gt;Sign up as a new user&lt;/li&gt;
&lt;li&gt;Do the hero action (record voice note, upload PDF, whatever the app does)&lt;/li&gt;
&lt;li&gt;Cut to the Supabase table view showing the row that just landed&lt;/li&gt;
&lt;li&gt;Fade out on the app logo&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Nothing else. No music. No voiceover. The point is to remove doubt, not to entertain.&lt;/p&gt;

&lt;h2&gt;
  
  
  v1 vs v2 vs building from scratch
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;v1 ($29)&lt;/th&gt;
&lt;th&gt;v2 ($79)&lt;/th&gt;
&lt;th&gt;From scratch&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Setup time&lt;/td&gt;
&lt;td&gt;~2 hours&lt;/td&gt;
&lt;td&gt;~30 minutes&lt;/td&gt;
&lt;td&gt;3–6 weeks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backend&lt;/td&gt;
&lt;td&gt;Mocked JSON&lt;/td&gt;
&lt;td&gt;Real Supabase&lt;/td&gt;
&lt;td&gt;You build it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RLS&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Per-table + seed users&lt;/td&gt;
&lt;td&gt;You write it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Demo&lt;/td&gt;
&lt;td&gt;Screenshots&lt;/td&gt;
&lt;td&gt;40-sec video&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agent-ready&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Skills + slash commands&lt;/td&gt;
&lt;td&gt;You configure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Docs&lt;/td&gt;
&lt;td&gt;800 words&lt;/td&gt;
&lt;td&gt;~12,000 words + video&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Refund rate&lt;/td&gt;
&lt;td&gt;~40%&lt;/td&gt;
&lt;td&gt;~4%&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For comparison, competitor templates like &lt;a href="https://ignitecookbook.com/" rel="nofollow noopener noreferrer"&gt;Ignite Cookbook&lt;/a&gt; ship strong scaffolding but leave the backend and AI as reader exercises. In 2026 that's the wrong tradeoff.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cost of getting it wrong
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;180 hours on v1 that no one wanted&lt;/li&gt;
&lt;li&gt;$600 in ads that returned nothing&lt;/li&gt;
&lt;li&gt;200 hours on the rebuild&lt;/li&gt;
&lt;li&gt;~$40k in real engineering opportunity cost&lt;/li&gt;
&lt;li&gt;Six months of momentum&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The postmortem was expensive. This blog post is us trying to make it slightly cheaper for the next founder considering a React Native template.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Docs first.&lt;/strong&gt; If they're hard to write, the product is wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;$79 minimum.&lt;/strong&gt; Cheaper attracts refund-prone buyers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real backend day one.&lt;/strong&gt; Mock nothing a reasonable buyer expects to be real.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One demo video per template.&lt;/strong&gt; 40 seconds. Silent. Happy path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ship for agents.&lt;/strong&gt; &lt;code&gt;CLAUDE.md&lt;/code&gt;, slash commands, real types.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refund policy above the buy button.&lt;/strong&gt; In big text.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The gap between "nice UI in a week" and "template a senior indie pays $79 for" is roughly three months of unglamorous infrastructure. Skip any of it and the launch dies quietly.&lt;/p&gt;

&lt;p&gt;You can see where all of this ended up at &lt;a href="https://www.applighter.com/apps?utm_source=hashnode&amp;amp;utm_medium=blog&amp;amp;utm_campaign=applighter-template-postmortem" rel="noopener noreferrer"&gt;Applighter&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>supabase</category>
    </item>
    <item>
      <title>Cutting React Native template support 70% with docs</title>
      <dc:creator>Jules Sarah</dc:creator>
      <pubDate>Fri, 24 Jul 2026 09:52:54 +0000</pubDate>
      <link>https://dev.to/jules_sarah_0718e958f0d24/cutting-react-native-template-support-70-with-docs-71i</link>
      <guid>https://dev.to/jules_sarah_0718e958f0d24/cutting-react-native-template-support-70-with-docs-71i</guid>
      <description>&lt;p&gt;We sell React Native templates. In Q1 2026 we rewrote our docs. Support tickets dropped 70%. This is the exact process, the exact structure, and the numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The audit (do this first)
&lt;/h2&gt;

&lt;p&gt;Before rewriting a single page, tag every support ticket for six weeks. Six columns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csvs"&gt;&lt;code&gt;&lt;span class="k"&gt;date&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="k"&gt;customer&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="k"&gt;category&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="k"&gt;root&lt;/span&gt;&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="k"&gt;cause&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="k"&gt;resolution&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="k"&gt;docs&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Categories we ended up with: &lt;code&gt;install&lt;/code&gt;, &lt;code&gt;configure&lt;/code&gt;, &lt;code&gt;extend&lt;/code&gt;, &lt;code&gt;ship&lt;/code&gt;, &lt;code&gt;billing&lt;/code&gt;, &lt;code&gt;other&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Root causes: &lt;code&gt;docs_missing&lt;/code&gt;, &lt;code&gt;docs_wrong&lt;/code&gt;, &lt;code&gt;docs_unfindable&lt;/code&gt;, &lt;code&gt;product_bug&lt;/code&gt;, &lt;code&gt;user_misunderstanding&lt;/code&gt;, &lt;code&gt;out_of_scope&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The audit output for us:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;% of tickets&lt;/th&gt;
&lt;th&gt;Docs-fixable?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;install&lt;/td&gt;
&lt;td&gt;24%&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;configure&lt;/td&gt;
&lt;td&gt;21%&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;extend&lt;/td&gt;
&lt;td&gt;18%&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ship&lt;/td&gt;
&lt;td&gt;5%&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;billing&lt;/td&gt;
&lt;td&gt;10%&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;other&lt;/td&gt;
&lt;td&gt;22%&lt;/td&gt;
&lt;td&gt;mostly no&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;68% of tickets were docs-fixable. That number is the whole rest of the article.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four-mode structure
&lt;/h2&gt;

&lt;p&gt;Restructure your doc site around the four stages a new customer hits, in order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docs/
├── getting-started/     # install
│   ├── installation.mdx
│   ├── environment.mdx
│   ├── quick-start.mdx
│   └── folder-structure.mdx
├── core-concepts/       # configure
│   ├── supabase.mdx
│   ├── expo-integration.mdx
│   └── ui-components.mdx
├── extend/              # extend
│   ├── adding-a-screen.mdx
│   ├── auth-guards.mdx
│   └── swapping-ai-provider.mdx
└── ship/                # ship
    ├── eas-build.mdx
    ├── app-store.mdx
    └── ota-updates.mdx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Order matters. Doc-site ordering is not aesthetic — it's a state machine. Each page assumes the customer completed the previous one.&lt;/p&gt;

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

&lt;p&gt;Print these. Stick them to a monitor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. First paragraph states the outcome, not the topic.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Bad:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Authentication&lt;/span&gt;

This page covers authentication in the template.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Good:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Adding auth to a new screen&lt;/span&gt;

By the end of this page, tapping a locked screen will redirect
signed-out users to &lt;span class="sb"&gt;`/sign-in`&lt;/span&gt; and preserve the deep link they
originally opened.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Every code block is copy-paste runnable.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Bad:&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;// ... existing imports&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;Screen&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;user&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSupabase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Good:&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;useSupabase&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/lib/supabase&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Redirect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;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;ProtectedScreen&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;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;loading&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSupabase&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;loading&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Redirect&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/sign-in"&lt;/span&gt; &lt;span class="p"&gt;/&amp;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;YourScreenContents&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Every page links to the next page.&lt;/strong&gt; Never let the customer bounce to Google.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. If you answer the same email twice, the docs get updated that day.&lt;/strong&gt; Same day. This is the only rule with real leverage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Ship a &lt;code&gt;claude.md&lt;/code&gt; at the project root.&lt;/strong&gt; If your customer opens the template in Claude Code, Cursor, or any AI-native editor, the AI reads this file first. Ours looks roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Applighter — &amp;lt;template name&amp;gt;&lt;/span&gt;

&lt;span class="gu"&gt;## Stack&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Expo 54, React Native 0.76
&lt;span class="p"&gt;-&lt;/span&gt; Supabase (auth, Postgres, storage, edge functions)
&lt;span class="p"&gt;-&lt;/span&gt; NativeWind 4, React Native Reusables (primitives)
&lt;span class="p"&gt;-&lt;/span&gt; TanStack Query 5

&lt;span class="gu"&gt;## Key files&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`app/(auth)/*`&lt;/span&gt; — auth flows
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`app/(app)/*`&lt;/span&gt; — signed-in surface
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`lib/supabase.ts`&lt;/span&gt; — client + typed helpers
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`supabase/migrations/*`&lt;/span&gt; — schema (RLS policies live here)

&lt;span class="gu"&gt;## Conventions&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; All server access via TanStack Query hooks in &lt;span class="sb"&gt;`hooks/`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Never call supabase-js directly from a component
&lt;span class="p"&gt;-&lt;/span&gt; Add new tables with a migration, not the dashboard

&lt;span class="gu"&gt;## Env vars&lt;/span&gt;
See &lt;span class="sb"&gt;`.env.example`&lt;/span&gt;.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The AI now writes correct code the first time. The customer never emails you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Operational loop
&lt;/h2&gt;

&lt;p&gt;Once the docs exist, keep them alive:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Log every ticket in a shared sheet (six columns above).&lt;/li&gt;
&lt;li&gt;30-minute triage every Friday. Flag anything asked twice.&lt;/li&gt;
&lt;li&gt;Doc gap → paragraph inline or new page scaffolded same day.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's it. No LLM classification. No analytics vendor. The marginal cost of writing one paragraph is much lower than the marginal cost of answering the same email eight times.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Support tickets: &lt;strong&gt;−70%&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Docs-caused refunds: &lt;strong&gt;−85%&lt;/strong&gt; (from ~4% to ~0.6% of purchases)&lt;/li&gt;
&lt;li&gt;Time-to-first-customized-screen: &lt;strong&gt;2–4 hours → 25–45 min&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Five-star reviews mentioning "docs": &lt;strong&gt;2 → 34&lt;/strong&gt; across two quarters&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What we didn't do
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No chatbot.&lt;/strong&gt; Deflection metrics are gameable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No paywalled docs.&lt;/strong&gt; Public and indexable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No community forum.&lt;/strong&gt; Half-populated forums are worse than none.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No codebase restructure to match docs.&lt;/strong&gt; Different audiences, different shapes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reference docs I used
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://diataxis.fr/" rel="noopener noreferrer"&gt;Diátaxis framework&lt;/a&gt; — the closest published version of what we independently arrived at&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.expo.dev/" rel="noopener noreferrer"&gt;Expo docs&lt;/a&gt; — the bar for RN documentation&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://supabase.com/docs" rel="noopener noreferrer"&gt;Supabase docs&lt;/a&gt; — same&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We build production-ready React Native templates at &lt;a href="https://www.applighter.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=docs-support-load" rel="noopener noreferrer"&gt;Applighter&lt;/a&gt; — the docs described above ship with every one of them.&lt;/p&gt;




&lt;p&gt;If you've run this audit on your own support inbox, what was your docs-fixable percentage? I suspect 68% isn't unusual.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>supabase</category>
      <category>applighter</category>
    </item>
    <item>
      <title>The Complete Guide to Offline Storage in React Native (2026)</title>
      <dc:creator>Jules Sarah</dc:creator>
      <pubDate>Wed, 22 Jul 2026 11:25:11 +0000</pubDate>
      <link>https://dev.to/jules_sarah_0718e958f0d24/the-complete-guide-to-offline-storage-in-react-native-2026-1ea7</link>
      <guid>https://dev.to/jules_sarah_0718e958f0d24/the-complete-guide-to-offline-storage-in-react-native-2026-1ea7</guid>
      <description>&lt;ul&gt;
&lt;li&gt;Offline storage is &lt;strong&gt;five categories&lt;/strong&gt;, not one library: key-value, secure, structured, file system, and server-state cache.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MMKV is the new default&lt;/strong&gt; over AsyncStorage — roughly 30× faster with synchronous reads.&lt;/li&gt;
&lt;li&gt;The offline-first pattern has four parts: &lt;strong&gt;local-first reads&lt;/strong&gt;, &lt;strong&gt;optimistic writes&lt;/strong&gt;, a &lt;strong&gt;durable mutation queue&lt;/strong&gt;, and a &lt;strong&gt;sync engine&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Conflict resolution: last-write-wins is fine for most consumer apps. CRDTs are overkill unless you're building a collaborative editor.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Most React Native apps treat the network as an always-on dependency, and it shows the moment a request stalls.&lt;/p&gt;

&lt;p&gt;Offline storage isn't one library — it's a stack: key-value stores for preferences, secure enclaves for tokens, structured DBs for domain data, and query caches for server state.&lt;/p&gt;

&lt;p&gt;This post walks through the whole stack and the offline-first pattern that ties it together.&lt;/p&gt;

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

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

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

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

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

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

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Local-first reads.&lt;/strong&gt; The UI always reads from the local store. The network populates the store; the store renders the UI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optimistic writes.&lt;/strong&gt; The UI updates immediately, before the server acknowledges.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mutation queue.&lt;/strong&gt; A durable list (SQLite or MMKV) of pending server-side changes, each with a client ID.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sync engine.&lt;/strong&gt; A background worker drains the queue with exponential backoff and reconciles conflicts.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;markMessageAsRead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;readAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="na"&gt;pendingSync&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;mutationQueue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enqueue&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;message.markRead&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;readAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;syncEngine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wake&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conflict resolution
&lt;/h2&gt;

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

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

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

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

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

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

&lt;p&gt;Offline-first isn't a defensive posture — it's a performance strategy. Reading from local storage beats a round-trip every time, and a good sync engine hides the network from your users entirely.&lt;/p&gt;

&lt;p&gt;Pick each layer deliberately and this becomes a solved problem rather than a recurring one.&lt;/p&gt;




&lt;p&gt;I built &lt;a href="https://www.rapidnative.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=offline-storage-guide" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt;, an AI mobile app builder that generates production-ready React Native code you can extend with any of these libraries.&lt;/p&gt;

&lt;p&gt;Anyone running MMKV + TanStack Query in production? Curious how the persistence layer has held up for you at scale.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>mobile</category>
      <category>javascript</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Rejected Under Guideline 4.2? Here's the Honest Decision Tree</title>
      <dc:creator>Jules Sarah</dc:creator>
      <pubDate>Wed, 22 Jul 2026 10:22:04 +0000</pubDate>
      <link>https://dev.to/jules_sarah_0718e958f0d24/rejected-under-guideline-42-heres-the-honest-decision-tree-43ad</link>
      <guid>https://dev.to/jules_sarah_0718e958f0d24/rejected-under-guideline-42-heres-the-honest-decision-tree-43ad</guid>
      <description>&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Appeal&lt;/strong&gt; only if the reviewer made a factual error (missed features behind a login). Otherwise you burn 1–3 weeks in Resolution Center limbo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bolt on native features&lt;/strong&gt; (APNs push, Face ID, native tabs, offline) improves your odds — but it's still a WebView underneath, so reviewers vary and each retry costs a cycle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rebuild in React Native&lt;/strong&gt; is the only path that removes the 4.2 category entirely. Real &lt;code&gt;UIView&lt;/code&gt;, not DOM in a browser shell.&lt;/li&gt;
&lt;li&gt;The real question isn't "how do I pass review" — it's "how many review cycles can I afford to lose finding out?"&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;You shipped it. You waited. Then the email arrived:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Guideline 4.2 — Design — Minimum Functionality&lt;/strong&gt;&lt;br&gt;
We found that the experience your app provides is not sufficiently different from a web browsing experience, as it would be if displayed in Safari.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here's the thing nobody tells you up front: &lt;strong&gt;most advice about 4.2 is written by people selling WebView wrappers.&lt;/strong&gt; Their answer is always "add push notifications and resubmit," because their business depends on that working.&lt;/p&gt;

&lt;p&gt;Sometimes it does. Often it doesn't. This is the honest version.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Apple is actually saying
&lt;/h2&gt;

&lt;p&gt;The full guideline text:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Your app should include features, content, and UI that elevate it beyond a repackaged website. If your app is not particularly useful, unique, or "app-like," it doesn't belong on the App Store.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The operative word is &lt;strong&gt;"elevate."&lt;/strong&gt; Apple isn't banning web technology — plenty of major apps use WebViews internally. What they're rejecting is the &lt;em&gt;ratio&lt;/em&gt;: an app whose primary value is showing you a website you could have visited in Safari.&lt;/p&gt;

&lt;p&gt;That tells you what reviewers actually measure. Not "does this contain a WebView," but "would a user be worse off just using the browser?"&lt;/p&gt;

&lt;h2&gt;
  
  
  Your three options
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/.%2Fguideline-4-2-decision-tree.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/.%2Fguideline-4-2-decision-tree.svg" alt="Decision tree showing three responses to an App Store Guideline 4.2 rejection: appeal, bolt on native features, or rebuild natively" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Diagram released under CC0 — reuse it freely.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Path 1: Appeal
&lt;/h3&gt;

&lt;p&gt;Reply in Resolution Center arguing the reviewer missed something.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Works when it's true.&lt;/strong&gt; If your app genuinely has native features the reviewer didn't find — behind a login, or three taps deep — a targeted reply with screenshots pointing at specific capabilities does get decisions reversed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fails badly when it isn't.&lt;/strong&gt; The Apple Developer Forums are full of people two weeks into an appeal with no response, watching a launch window close. One developer's app was tied to an event launching the following week, with the appeal pending a fortnight and no update.&lt;/p&gt;

&lt;p&gt;Appeal if the reviewer made a factual error. Don't appeal to argue taste.&lt;/p&gt;

&lt;h3&gt;
  
  
  Path 2: Bolt on native features
&lt;/h3&gt;

&lt;p&gt;The standard vendor advice: add push notifications, Face ID, offline caching, native tab navigation. Resubmit.&lt;/p&gt;

&lt;p&gt;This is real advice and it does work — reviewers respond to APNs push especially, because it's hard evidence of native platform integration. Face ID is the other easy win.&lt;/p&gt;

&lt;p&gt;But be clear about what you're doing: &lt;strong&gt;you're improving your odds on a dice roll, not removing the dice.&lt;/strong&gt; The app is still a WebView underneath. Reviewers vary. The same binary that passes with one reviewer gets bounced by another, and each attempt costs a full review cycle.&lt;/p&gt;

&lt;p&gt;Pattern worth watching: if you've bolted on features twice and been rejected twice, a third attempt is usually not the highest-value use of your week.&lt;/p&gt;

&lt;h3&gt;
  
  
  Path 3: Rebuild the UI natively
&lt;/h3&gt;

&lt;p&gt;Move the interface to React Native (or Flutter). These compile to actual native UI components — real &lt;code&gt;UIView&lt;/code&gt; and &lt;code&gt;UIButton&lt;/code&gt; instances — rather than rendering DOM inside a browser shell.&lt;/p&gt;

&lt;p&gt;This is the one option that &lt;strong&gt;removes the rejection category entirely.&lt;/strong&gt; You're no longer in the "repackaged website" bucket, so 4.2 stops being a thing that can happen to you.&lt;/p&gt;

&lt;p&gt;Honest cost: a genuine rewrite of your UI layer. Business logic, API calls, Supabase queries — those largely survive. The components don't.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question worth asking
&lt;/h2&gt;

&lt;p&gt;The reframe that actually helps:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The question isn't "how do I pass review?" It's "how many review cycles can I afford to lose finding out?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Each rejection-and-resubmit loop is roughly a week. Pre-launch with no deadline? Gambling on Path 2 is cheap and reasonable. Launch date, investors, a client waiting? The expected cost of two more failed cycles usually exceeds the cost of doing the rebuild once.&lt;/p&gt;

&lt;p&gt;It's a business calculation more than a technical one.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you're coming from Lovable, v0, or Bolt
&lt;/h2&gt;

&lt;p&gt;A lot of 4.2 rejections trace back to the same origin story.&lt;/p&gt;

&lt;p&gt;You built a web app with an AI builder. It's good. Then you tried to ship it and discovered these tools generate &lt;strong&gt;React web apps&lt;/strong&gt; — they don't produce native binaries. The App Store takes compiled &lt;code&gt;.ipa&lt;/code&gt; files; Google Play takes &lt;code&gt;.aab&lt;/code&gt;. A React web app compiles to neither.&lt;/p&gt;

&lt;p&gt;So people reach for a wrapper, and land here.&lt;/p&gt;

&lt;p&gt;If that's your path, Path 2 is a particularly weak bet — your app &lt;em&gt;is&lt;/em&gt; the website, structurally, and there's not much to elevate. Path 3 is where you were always going to end up.&lt;/p&gt;

&lt;p&gt;The rebuild is more mechanical than it sounds: your Supabase backend connects to a React Native app without migration, and your API layer ports mostly unchanged. It's the component tree that needs redoing.&lt;/p&gt;

&lt;p&gt;If you'd rather not do that project yourself, &lt;a href="https://www.rapidnative.com/lovable-to-app-store?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=guideline-4-2-rejection" 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, and metadata included, with source code and full ownership.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist, if you're taking Path 2 anyway
&lt;/h2&gt;

&lt;p&gt;Sometimes bolting on is the right call — tight deadline, genuinely content-heavy product. Prioritise in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;APNs push notifications.&lt;/strong&gt; Strongest single signal to a reviewer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Face ID / Touch ID.&lt;/strong&gt; Native API usage, easy to implement, easy to see.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Native tab bar and navigation.&lt;/strong&gt; Not a hamburger menu rendered in HTML.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real offline handling.&lt;/strong&gt; If it shows "No Internet Connection" like a browser, that's a rejection risk on its own.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rewrite your App Store description.&lt;/strong&gt; If it says "access our website on your phone," you're asking for the rejection.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Three of these is roughly the threshold where reviewers start passing wrapper apps.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Guideline 4.2 isn't Apple being arbitrary. It's a floor on what an app &lt;em&gt;is&lt;/em&gt;, and wrappers sit right on that line by design.&lt;/p&gt;

&lt;p&gt;Appeal when the reviewer was factually wrong. Bolt on features when you have time to gamble. Rebuild when the launch actually matters.&lt;/p&gt;

&lt;p&gt;Whichever you pick — pick it deliberately, rather than defaulting into another resubmit because it feels like the smaller decision. It usually isn't.&lt;/p&gt;




&lt;p&gt;Been through a 4.2 rejection? Which path did you take, and did it work? The data on what actually passes is mostly anecdotal — more anecdotes would help everyone.&lt;/p&gt;

</description>
      <category>ios</category>
      <category>appstore</category>
      <category>reactnative</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Nine Screens of Pain: The UX of Submitting to the App Store</title>
      <dc:creator>Jules Sarah</dc:creator>
      <pubDate>Fri, 17 Jul 2026 11:51:01 +0000</pubDate>
      <link>https://dev.to/jules_sarah_0718e958f0d24/nine-screens-of-pain-the-ux-of-submitting-to-the-app-store-2f06</link>
      <guid>https://dev.to/jules_sarah_0718e958f0d24/nine-screens-of-pain-the-ux-of-submitting-to-the-app-store-2f06</guid>
      <description>&lt;p&gt;If you've ever submitted your first app, what follows will feel familiar. If you haven't, bookmark this — you're going to want to reference it around week two of the process.&lt;/p&gt;

&lt;h2&gt;
  
  
  The starting state: a fresh App Store Connect account
&lt;/h2&gt;

&lt;p&gt;You've paid the $99. You've verified the DUNS number. You've waited three business days for Apple to enroll you.&lt;/p&gt;

&lt;p&gt;Now you're staring at App Store Connect, trying to figure out how to actually submit the app you've built. The dashboard shows icons for iTunes Connect (deprecated, but still visible), Certificates, TestFlight, Users and Access, Contracts. None of them say "Submit an App." That option lives under "My Apps" — which is one of the icons, but isn't labeled as such.&lt;/p&gt;

&lt;p&gt;You have spent fifteen minutes and made zero progress.&lt;/p&gt;

&lt;h2&gt;
  
  
  Screen-by-screen through the actual flow
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Screen 1: Bundle ID registration.&lt;/strong&gt; In the developer portal — a different site than App Store Connect. Get this wrong and you can't rename it later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Screen 2: Certificate + provisioning profile.&lt;/strong&gt; Two of each, development and distribution. Apple's UI walks you through it but assumes you already know what a certificate &lt;em&gt;does&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Screen 3: Xcode signing configuration.&lt;/strong&gt; Six dropdowns, three of which look identical. If any one is wrong, the build fails with "no provisioning profile matches," and you go Google the exact error string.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Screen 4: Archive + upload.&lt;/strong&gt; Ten minutes of "processing," then usually success. Sometimes an email thirty minutes later: "Your build has one or more issues." No indication of which.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Screen 5: App Information.&lt;/strong&gt; Category, subcategory, content rights, age rating. That last one is a twenty-question survey.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Screen 6: Pricing and Availability.&lt;/strong&gt; Choose the countries. Pick the tier. Explain whether the app is free but has in-app purchases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Screen 7: App Privacy.&lt;/strong&gt; Answer questions about data collection. Every "yes" cascades into four to six follow-up questions. Miss one and your submission is rejected.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Screen 8: The version you're submitting.&lt;/strong&gt; Description, keywords (100 characters), promotional text, support URL, marketing URL, screenshots (six iPhone sizes minimum), preview video.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Screen 9: Review information.&lt;/strong&gt; Contact info, demo account credentials, review notes. Then, finally, the "Submit for Review" button.&lt;/p&gt;

&lt;p&gt;And that's not counting the TestFlight side, which is another five screens.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 3am "invalid binary" moment
&lt;/h2&gt;

&lt;p&gt;Every indie dev has this moment. You've finished all nine screens. You upload the build. Twenty minutes later, an email arrives:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The uploaded build has invalid binary architecture.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Or:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The provisioning profile does not include the Push Notifications entitlement.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Google says: rebuild with different Xcode settings. You do. Same error. You Google more. It turns out you also had to add a capability in the developer portal. You do that. Build. Upload. Wait twenty minutes. Different error.&lt;/p&gt;

&lt;p&gt;This is where most people give up on their first-month timeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this UX pattern persists
&lt;/h2&gt;

&lt;p&gt;Apple has zero pressure to simplify submission. Every indie who quits has ten replacements. And Apple's real customer isn't the indie dev — it's the enterprise studio with a team dedicated to this workflow.&lt;/p&gt;

&lt;p&gt;That isn't going to change from within Apple. But it means the leverage for indie tooling is enormous — whoever solves this UX gap earns loyalty from a badly underserved segment.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a good submission tool looks like
&lt;/h2&gt;

&lt;p&gt;A good submission tool would:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Ask the App Privacy questions in plain English, once.&lt;/strong&gt; Then fill in App Store Connect automatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-check your build against the top 20 rejection reasons before upload.&lt;/strong&gt; Fail loudly if any are hit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Show real-time status.&lt;/strong&gt; "In review, 47h in queue, median for your category is 26h" — not "Waiting for Review" for five days.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handle rejections as tickets.&lt;/strong&gt; The rejection reason parsed into a diff you can apply, with a resubmit button.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never let you reach a 3am invalid-binary moment.&lt;/strong&gt; Every failure mode should surface &lt;em&gt;before&lt;/em&gt; upload, not twenty minutes after.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's the product I want to use as an indie dev — and it's part of why we built &lt;a href="https://www.rapidnative.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=app-store-submission-ux" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt; to handle the generate-to-submit path so the nine screens stop being your problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;The App Store submission flow is a UX artifact of Apple's incentives, not a law of nature. Until it improves, treat it like the gauntlet it is: budget a full week, expect one rejection, and surface every failure mode you can before you hit upload.&lt;/p&gt;

&lt;p&gt;Which App Store rejection got you at 3am — invalid binary, missing entitlement, or a privacy label you filled out wrong? Drop it in the comments.&lt;/p&gt;

</description>
      <category>ios</category>
      <category>appstore</category>
      <category>ux</category>
      <category>mobile</category>
    </item>
    <item>
      <title>Auth Is Where User Trust Is Decided — What Indie Founders Miss About Expo + Supabase Flows</title>
      <dc:creator>Jules Sarah</dc:creator>
      <pubDate>Wed, 15 Jul 2026 13:07:33 +0000</pubDate>
      <link>https://dev.to/jules_sarah_0718e958f0d24/auth-is-where-user-trust-is-decided-what-indie-founders-miss-about-expo-supabase-flows-2e9f</link>
      <guid>https://dev.to/jules_sarah_0718e958f0d24/auth-is-where-user-trust-is-decided-what-indie-founders-miss-about-expo-supabase-flows-2e9f</guid>
      <description>&lt;ul&gt;
&lt;li&gt;There's a ~30-second window, usually mid-auth, where a new user decides to lean in or swipe away.&lt;/li&gt;
&lt;li&gt;Four moments break trust: a &lt;strong&gt;magic link that opens Safari instead of the app&lt;/strong&gt;, an &lt;strong&gt;OAuth flow stuck in limbo&lt;/strong&gt;, &lt;strong&gt;error messages that read like stack traces&lt;/strong&gt;, and &lt;strong&gt;sessions silently lost on app resume&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Most fixes don't need a designer — copy error-message tone from apps you love, test the magic-link flow on a real device with the app closed, and add a "check your email" confirmation.&lt;/li&gt;
&lt;li&gt;The single biggest lever: seamless session refresh on resume. Wiring it took install-to-week-1 retention from 32% to 41%.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;I've watched a lot of first-time app installs. There's a moment about 30 seconds in where users either lean forward and start actually using the app, or lean back and swipe it away. That moment is almost always mid-auth-flow — and it's mostly decided by whether the auth flow &lt;em&gt;feels&lt;/em&gt; trustworthy.&lt;/p&gt;

&lt;p&gt;This isn't a marketing insight. It's a UX one. Founders overwhelmingly underweight it, especially indies who see auth as "plumbing." Here's what actually happens in that 30 seconds, and how a good Expo + Supabase setup helps.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 30-second trust window
&lt;/h2&gt;

&lt;p&gt;The user opens your app fresh. They see a splash screen (fine), a welcome screen (fine), and then the sign-up screen. From here they make four judgments in rapid succession:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Do I understand what I'm being asked?&lt;/strong&gt; Email + password? Magic link? Social? All three?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do I trust this with my email?&lt;/strong&gt; Is the design polished? Does it look like a real product?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does anything obviously break?&lt;/strong&gt; Autocorrect mangling my email, the keyboard covering the CTA, weird focus behavior?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do I get a signal that it worked?&lt;/strong&gt; After I hit sign-up, do I see confirmation, or does the app just… sit there?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Break any of these and you lose the user before they see a single product feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four moments that break trust
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Moment 1: The magic link opens in Safari, not the app.&lt;/strong&gt; The user taps the email link, iOS opens Safari, they see a "redirecting…" page, then get bounced back to the app — but the auth state didn't sync. They tap the link again. Same result. They quit. This happens when Universal Links aren't wired properly, or &lt;code&gt;expo-linking&lt;/code&gt; isn't handling the auth callback URL.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Moment 2: The OAuth flow leaves them in limbo.&lt;/strong&gt; The user taps "sign in with Google," gets the browser sheet, taps back — and the app is stuck on a loading spinner. Flows that don't handle the OAuth-cancelled state leave the user stranded.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Moment 3: The error message reads like a stack trace.&lt;/strong&gt; "Invalid credentials" or "Auth session missing!" terrifies non-technical users. "That password doesn't look right. Reset it?" converts them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Moment 4: Session lost after app resume.&lt;/strong&gt; The user signs in, backgrounds the app, comes back an hour later — signed out. &lt;code&gt;supabase-js&lt;/code&gt;'s default token refresh doesn't always fire on iOS resume. Wiring &lt;code&gt;AppState&lt;/code&gt; to &lt;code&gt;supabase.auth.refreshSession()&lt;/code&gt; explicitly solves this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixes without a designer
&lt;/h2&gt;

&lt;p&gt;Even as a solo founder with no design help:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Copy the error-message tone from a well-loved app you use (Linear, Notion, Superhuman).&lt;/li&gt;
&lt;li&gt;Test the magic-link flow yourself on a real device, with the app fully closed, before you launch.&lt;/li&gt;
&lt;li&gt;Add an "account created — check your email" confirmation screen. Users need the feedback.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Run a 20-minute usability test
&lt;/h2&gt;

&lt;p&gt;Grab someone who's never seen your app. Watch them sign up. Don't help.&lt;/p&gt;

&lt;p&gt;Note every place they hesitate for more than two seconds. Those are your trust breakpoints. Fix the top three before you launch.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one feature that changed my install-to-DAU rate
&lt;/h2&gt;

&lt;p&gt;All of the above matters, but the single thing that most moved my numbers: &lt;strong&gt;seamless session refresh on app resume&lt;/strong&gt;, so users never got signed out unexpectedly.&lt;/p&gt;

&lt;p&gt;My install-to-week-1 retention went from 32% to 41%. That's a large lift, and it came from one wired hook.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;Auth is a UX surface, not a plumbing task. If your Expo + Supabase foundation ships wired session refresh, correct deep-link handling, and a proper OAuth-cancel state, you're buying trust — not just code.&lt;/p&gt;

&lt;p&gt;If you'd rather start from a foundation with these already solved, I keep a set of Expo + Supabase templates wired for exactly this at &lt;a href="https://www.applighter.com/apps?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=auth-trust-window" rel="noopener noreferrer"&gt;Applighter&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Which of the four trust-breakers has cost you the most users? Drop it in the comments.&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>expo</category>
      <category>ux</category>
      <category>indiehackers</category>
    </item>
    <item>
      <title>40 React Native Devs on What They Wish Their Boilerplate Fixed</title>
      <dc:creator>Jules Sarah</dc:creator>
      <pubDate>Wed, 08 Jul 2026 14:36:42 +0000</pubDate>
      <link>https://dev.to/jules_sarah_0718e958f0d24/40-react-native-devs-on-what-they-wish-their-boilerplate-fixed-boe</link>
      <guid>https://dev.to/jules_sarah_0718e958f0d24/40-react-native-devs-on-what-they-wish-their-boilerplate-fixed-boe</guid>
      <description>&lt;ul&gt;
&lt;li&gt;I asked ~40 indie React Native devs one question: &lt;strong&gt;"What do you wish your boilerplate did differently?"&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Three wishes dominated: &lt;strong&gt;less opinionated navigation (28/40)&lt;/strong&gt;, &lt;strong&gt;TypeScript without wrapper-type overhead (24/40)&lt;/strong&gt;, and &lt;strong&gt;real dark-mode wiring, not just a CSS toggle (19/40)&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nobody asked for more features.&lt;/strong&gt; Not one. Every wish was subtractive.&lt;/li&gt;
&lt;li&gt;The next wave of boilerplates will win by shipping &lt;strong&gt;less&lt;/strong&gt;, cleaner, with better exit points.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Over the past two months I've been talking to indie React Native developers about the boilerplates they use. The question that got the most useful answers wasn't "which boilerplate do you use?" — it was "what do you wish your boilerplate did differently?"&lt;/p&gt;

&lt;p&gt;I've now heard back from about 40 devs across a handful of communities (r/reactnative, the Expo Discord, some Twitter DMs). The pattern is clear enough to be worth writing down.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the survey ran and who answered
&lt;/h2&gt;

&lt;p&gt;Informal, DM-based, ~10 minutes each. Sample skews toward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Solo devs and 2-3 person teams (35 of 40)&lt;/li&gt;
&lt;li&gt;Shipping paid apps, not free (~28 of 40)&lt;/li&gt;
&lt;li&gt;Using either an off-the-shelf boilerplate or one they'd built themselves (~half and half)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the sample is biased toward serious builders — people whose CI/CD choices matter to their revenue. That's the right audience for a survey about tooling ergonomics.&lt;/p&gt;

&lt;h2&gt;
  
  
  The top three wishes, in order
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Less opinionated navigation (28 of 40 mentioned this, often first)
&lt;/h3&gt;

&lt;p&gt;Most boilerplates ship a navigation shape — tab bar with a stack per tab, a modal presentation pattern, a drawer for settings — and it's the choice devs said they most often had to unwind. The frustration wasn't the specific shape; it was that the boilerplate had committed to one before knowing the product.&lt;/p&gt;

&lt;p&gt;What they wanted: a navigation layer they could rip and replace in an afternoon. What they got: a navigation layer that other screens assumed, so ripping it meant refactoring every screen.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. TypeScript throughout, without wrapper types getting in the way (24 of 40)
&lt;/h3&gt;

&lt;p&gt;Everyone wants TS. What they don't want is a boilerplate whose author defined 40 custom wrapper types on top of standard SDK types (Supabase, Stripe, Expo APIs) because they wanted stricter typing.&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;// ❌ What devs are complaining about&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AppUser&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;@/lib/types/user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AppSession&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;@/lib/types/session&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;// You have to learn the boilerplate's vocabulary&lt;/span&gt;
&lt;span class="c1"&gt;// before you can find what you need in the SDK docs.&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ What they want instead&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Session&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="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="c1"&gt;// The SDK's own types. No translation layer.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The wish is TS-native, TS-first, but using the SDK's own types wherever possible. Wrap only when the SDK types are genuinely painful.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Real dark-mode wiring, not just a CSS toggle (19 of 40)
&lt;/h3&gt;

&lt;p&gt;This one surprised me. Many boilerplates advertise "dark mode included" and what they mean is a theme toggle that swaps a color palette. What devs actually want:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;System-color-scheme detection that respects OS-level user preference&lt;/li&gt;
&lt;li&gt;Persistence of user override across app restarts&lt;/li&gt;
&lt;li&gt;Correct behavior for platform-specific dark-mode surfaces (iOS blur, Android edge-to-edge)&lt;/li&gt;
&lt;li&gt;Status bar + navigation bar following the theme&lt;/li&gt;
&lt;li&gt;Charts, images, and third-party components that don't fight the theme&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;"Dark mode included" should mean all of the above. Mostly it means a toggle.&lt;/p&gt;

&lt;h2&gt;
  
  
  The features nobody asked for
&lt;/h2&gt;

&lt;p&gt;Equally interesting: &lt;strong&gt;nobody asked for more features.&lt;/strong&gt; Not one respondent said "I wish my boilerplate had more built-in screens" or "I wish it included analytics + i18n + AI chat + [something]." Every wish was subtractive: less friction, less coupling, less opinion.&lt;/p&gt;

&lt;p&gt;The implication is unusual for a product category: feature-heavy boilerplates are competing on the wrong axis. The market wants fewer, better, cleaner-seamed layers.&lt;/p&gt;

&lt;h2&gt;
  
  
  What that tells us about product/market fit for starter kits
&lt;/h2&gt;

&lt;p&gt;Boilerplates are a maturing category. The first wave sold on feature counts because that was legible. The current audience has been burned by feature counts enough times that they now audit for extensibility. The next wave of boilerplates will win by having a smaller surface area and cleaner exit points.&lt;/p&gt;

&lt;p&gt;That's a hard product story to tell. "We include less than the competition" doesn't sell landing pages. But it's what devs 6 months into a project actually want.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion: subtractive design wins in tooling
&lt;/h2&gt;

&lt;p&gt;The finding I keep coming back to: developers judge tools by how well they get out of the way, not by how much they do. Boilerplates are no different.&lt;/p&gt;

&lt;p&gt;If you're building a starter kit, the next feature to add is probably a layer to remove. If you're picking one, run the 60-minute strip-down test. Either way, subtractive design is the axis of quality.&lt;/p&gt;

&lt;p&gt;Feature counts are dead. Long live ergonomics.&lt;/p&gt;




&lt;p&gt;What did you wish your React Native boilerplate did differently? Drop it in the comments — I'm collecting responses for a follow-up survey and yours might be the pattern that makes the shortlist next time.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>boilerplate</category>
      <category>indiedev</category>
      <category>expo</category>
    </item>
    <item>
      <title>Stop Hand-Coding React Native Forms. Generate Them Instead.</title>
      <dc:creator>Jules Sarah</dc:creator>
      <pubDate>Wed, 01 Jul 2026 12:20:58 +0000</pubDate>
      <link>https://dev.to/jules_sarah_0718e958f0d24/stop-hand-coding-react-native-forms-generate-them-instead-b31</link>
      <guid>https://dev.to/jules_sarah_0718e958f0d24/stop-hand-coding-react-native-forms-generate-them-instead-b31</guid>
      <description>&lt;ul&gt;
&lt;li&gt;Every mobile app is mostly forms, and every form comes with the same boilerplate tax: library choice, schema, keyboard config, &lt;code&gt;KeyboardAvoidingView&lt;/code&gt;, validation, error/loading/disabled states, cross-platform testing.&lt;/li&gt;
&lt;li&gt;AI generation produces all of that from a plain-English prompt — including the props you always forget (&lt;code&gt;autoCapitalize="none"&lt;/code&gt;, &lt;code&gt;textContentType&lt;/code&gt;, &lt;code&gt;returnKeyType&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Where it still needs you: domain-specific validation, custom UX choices, and swapping the placeholder fetch for your real endpoint.&lt;/li&gt;
&lt;li&gt;The real win is iteration speed: tap a field, describe the change, only that field updates.&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  Stop Hand-Coding React Native Forms. Generate Them Instead.
&lt;/h1&gt;

&lt;p&gt;Ask any React Native dev what part of the job they enjoy least, and forms come up fast. This post walks through generating forms with AI instead — what you get out of the box, what still needs your hand, and where the shortcuts hurt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Forms Are the Worst
&lt;/h2&gt;

&lt;p&gt;Every mobile app is mostly forms with some chrome around them. Signup, checkout, profile edit, "leave a review," "add a task" — all forms. And every form comes with the same tax:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pick a form library (react-hook-form? Formik? roll your own?)&lt;/li&gt;
&lt;li&gt;Wire up a schema library (Zod, Yup)&lt;/li&gt;
&lt;li&gt;Configure the keyboard for each field (&lt;code&gt;keyboardType&lt;/code&gt;, &lt;code&gt;autoCapitalize&lt;/code&gt;, &lt;code&gt;autoComplete&lt;/code&gt;, &lt;code&gt;textContentType&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Fight &lt;code&gt;KeyboardAvoidingView&lt;/code&gt; until the submit button stops hiding under the keyboard&lt;/li&gt;
&lt;li&gt;Handle blur validation, submit validation, error rendering, disabled states, loading states&lt;/li&gt;
&lt;li&gt;Test on both iOS and Android&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Multiply by 15 forms and there goes your sprint.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "AI-Generated Forms" Actually Produces
&lt;/h2&gt;

&lt;p&gt;I've been building with &lt;a href="https://www.rapidnative.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=ai-powered-forms-data-entry" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt; — an AI mobile app builder that takes plain English and outputs React Native + Expo — and forms are the use case where AI generation feels closest to a superpower. Here's what a prompt like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A signup screen with email, password, confirm password, and a role picker (founder, designer, developer). Validate all fields on blur. Auto-advance to the next field on return.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;...actually generates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setFormData&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="na"&gt;email&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="na"&gt;password&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="na"&gt;confirmPassword&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="na"&gt;role&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="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;errors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setErrors&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;emailRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRef&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;passwordRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRef&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;confirmRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRef&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;validateField&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;field&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// per-field validation&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;KeyboardAvoidingView&lt;/span&gt;
    &lt;span class="na"&gt;behavior&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;Platform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OS&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ios&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;padding&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;height&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="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;ScrollView&lt;/span&gt; &lt;span class="na"&gt;keyboardShouldPersistTaps&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"handled"&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;ref&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;emailRef&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;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="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;autoComplete&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;textContentType&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"emailAddress"&lt;/span&gt;
        &lt;span class="na"&gt;returnKeyType&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"next"&lt;/span&gt;
        &lt;span class="na"&gt;onSubmitEditing&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;passwordRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;focus&lt;/span&gt;&lt;span class="p"&gt;()&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;formData&lt;/span&gt;&lt;span class="p"&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="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&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;setFormData&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;onBlur&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;validateField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;formData&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="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&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;error&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* password + confirm + picker follow */&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;KeyboardAvoidingView&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every prop I usually forget on the first try — &lt;code&gt;autoCapitalize="none"&lt;/code&gt;, &lt;code&gt;textContentType="emailAddress"&lt;/code&gt;, &lt;code&gt;returnKeyType="next"&lt;/code&gt;, the &lt;code&gt;KeyboardAvoidingView&lt;/code&gt; wrapper — is there by default. Not because I remembered to ask, but because the generator knows what an email field needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Actually Hard About Mobile Forms
&lt;/h2&gt;

&lt;p&gt;The interesting details, the ones AI generation handles well:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Keyboard type per field.&lt;/strong&gt; Email → &lt;code&gt;email-address&lt;/code&gt;, phone → &lt;code&gt;phone-pad&lt;/code&gt;, card number → &lt;code&gt;number-pad&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Autofill hints.&lt;/strong&gt; &lt;code&gt;textContentType="emailAddress"&lt;/code&gt; on iOS, &lt;code&gt;autoComplete="email"&lt;/code&gt; on RN. Both required for OS autofill to fire.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Focus chain.&lt;/strong&gt; &lt;code&gt;returnKeyType="next"&lt;/code&gt; + &lt;code&gt;onSubmitEditing&lt;/code&gt; + refs to the next input.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Password fields.&lt;/strong&gt; &lt;code&gt;secureTextEntry&lt;/code&gt; + &lt;code&gt;textContentType="password"&lt;/code&gt; (or &lt;code&gt;newPassword&lt;/code&gt; on signup) so iOS offers to save/generate a password.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keyboard avoidance.&lt;/strong&gt; &lt;code&gt;KeyboardAvoidingView&lt;/code&gt; with different &lt;code&gt;behavior&lt;/code&gt; per platform, wrapped around a &lt;code&gt;ScrollView&lt;/code&gt; with &lt;code&gt;keyboardShouldPersistTaps="handled"&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These are the kind of details that get skipped when a human is tired. They're the kind of details that never get skipped by a generator, because they're pattern-heavy and well-documented.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where AI Generation Still Needs You
&lt;/h2&gt;

&lt;p&gt;Being honest: not everything is a slam dunk.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Very domain-specific validation&lt;/strong&gt; (tax IDs, national ID formats, complex phone number rules) — you'll want to review or specify these explicitly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom UX flows&lt;/strong&gt; where the "correct" pattern is a design choice, not a technical one, still benefit from a human eye.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integration with your existing backend&lt;/strong&gt; — the generator produces a placeholder fetch you replace with your actual endpoint. Not automatic magic, but a two-line edit.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Iteration Wins
&lt;/h2&gt;

&lt;p&gt;The real unlock isn't the first-generation speed — it's how fast you can iterate. Tap a field in the preview, say "make this a dropdown with 4 options," and only that field changes. Say "the submit button should be disabled until the form is valid," and it wires it up.&lt;/p&gt;

&lt;p&gt;Compare that to: open the file, find the &lt;code&gt;TextInput&lt;/code&gt;, replace with &lt;code&gt;Picker&lt;/code&gt;, update state binding, add options array, restyle, save, reload the preview. Forms live-edited with a chat interface are unreasonably fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;Give it a shot: &lt;a href="https://www.rapidnative.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=ai-powered-forms-data-entry" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt;. Free to start. Type in a form description and see what falls out.&lt;/p&gt;

&lt;p&gt;What's the form screen that's eaten the most of your time? Drop a comment with what you're building.&lt;/p&gt;

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