<?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: Dan</title>
    <description>The latest articles on DEV Community by Dan (@pestodrizzle).</description>
    <link>https://dev.to/pestodrizzle</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%2F1258232%2F55299958-a5af-42c5-9e73-34fa1cdfde6c.jpeg</url>
      <title>DEV Community: Dan</title>
      <link>https://dev.to/pestodrizzle</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pestodrizzle"/>
    <language>en</language>
    <item>
      <title>How SWSH turned 300 monthly builds into a release pipeline nobody has to babysit</title>
      <dc:creator>Dan</dc:creator>
      <pubDate>Sat, 05 Sep 2026 15:04:16 +0000</pubDate>
      <link>https://dev.to/expo/how-swsh-turned-300-monthly-builds-into-a-release-pipeline-nobody-has-to-babysit-11bl</link>
      <guid>https://dev.to/expo/how-swsh-turned-300-monthly-builds-into-a-release-pipeline-nobody-has-to-babysit-11bl</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Expo's ecosystem helped us take human error and effort out of the release process, from a local build to a validated release."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;— Nathan Ahn, CTO, SWSH&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You've shipped a mobile app before, so you know the drill: cut the builds, wrestle with signing credentials, submit to two different app stores, then hope nothing regressed on the way out. Do that by hand often enough and one of those steps eventually bites you.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://apps.apple.com/us/app/swsh-shared-photo-albums/id6476418833" rel="noopener noreferrer"&gt;SWSH&lt;/a&gt;, a social photo-sharing app for shared albums and events, ships across iOS, an iOS App Clip, Android, and the web from a single React Native codebase, and its release process barely needs a person in the loop. The team runs &lt;strong&gt;300 builds a month&lt;/strong&gt; on its own hardware, never touches signing credentials directly, drives App Store Connect through its API instead of a browser, and validates every release with a home-built end-to-end suite before it goes out.&lt;/p&gt;

&lt;p&gt;This post breaks down how that pipeline is put together, piece by piece, using &lt;a href="https://expo.dev/" rel="noopener noreferrer"&gt;Expo&lt;/a&gt;, &lt;a href="https://docs.expo.dev/build/introduction" rel="noopener noreferrer"&gt;EAS Build&lt;/a&gt;, &lt;a href="https://docs.expo.dev/submit/introduction" rel="noopener noreferrer"&gt;EAS Submit&lt;/a&gt;, &lt;a href="https://docs.expo.dev/eas-update/introduction" rel="noopener noreferrer"&gt;EAS Update&lt;/a&gt;, and &lt;a href="https://expo.dev/solutions/expo-observe" rel="noopener noreferrer"&gt;EAS Observe&lt;/a&gt;.&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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnban9s6lvlo9nmgjk5h0.jpg" 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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnban9s6lvlo9nmgjk5h0.jpg" alt="SWSH on the app store" width="800" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  One codebase, every target
&lt;/h2&gt;

&lt;p&gt;The web deploys on its own track, so the rest of this is about the three native targets: iOS, the App Clip, and Android. A change starts life as a local build, runs through an end-to-end test suite on iOS simulators and Android emulators, and lands in both app stores with almost no human involvement. Native releases go out about once a week, and the pipeline is what gets them there without anyone standing over it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: every manual step is a place to break something
&lt;/h2&gt;

&lt;p&gt;Cut a build, handle credentials, submit to App Store Connect and Google Play, confirm nothing regressed. Done by hand, each of those steps is a place a release can quietly go wrong, and the more often you ship, the more those mistakes cost.&lt;/p&gt;

&lt;p&gt;SWSH decided to go after the whole chain at once instead of patching individual steps: automate the entire release process so human error has nowhere left to enter. Expo's build and submit tooling gave them that automation out of the box, and a foundation solid enough to build custom pieces on top where they needed more.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Expo works as the base for this
&lt;/h2&gt;

&lt;p&gt;Expo gave SWSH one automated path from source to store: builds, credentials, submission, and over-the-air updates, all under the same toolchain.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://expo.dev/blog/what-is-continuous-native-generation" rel="noopener noreferrer"&gt;Continuous Native Generation&lt;/a&gt; (&lt;code&gt;expo prebuild&lt;/code&gt;) means the team regenerates native iOS and Android projects on demand instead of hand-maintaining Xcode and Gradle files, while still being able to &lt;a href="https://expo.dev/blog/expo-ui-stable-sdk-56" rel="noopener noreferrer"&gt;drop into native code&lt;/a&gt; when a feature needs it. And because EAS runs the &lt;em&gt;same&lt;/em&gt; build tooling whether it's local or in the cloud, SWSH could adopt EAS's orchestration, credential management, and submission logic without giving up control over where the actual compilation happens.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inside the pipeline
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Local builds, run at SWSH's own scale
&lt;/h3&gt;

&lt;p&gt;For most teams, &lt;a href="https://docs.expo.dev/build/introduction" rel="noopener noreferrer"&gt;Expo's cloud builds&lt;/a&gt; are the obvious call. SWSH is the exception, and the reason is volume: full iOS and Android builds on every pull request that touches native code, plus per-environment release builds, totaling roughly 300 a month. At that volume the economics shift, so SWSH moved compilation onto self-hosted macOS and Android runners with &lt;a href="https://docs.expo.dev/build-reference/local-builds/" rel="noopener noreferrer"&gt;local EAS builds&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This is the detail that's easy to overlook about EAS: because &lt;code&gt;eas build&lt;/code&gt; behaves identically whether it runs locally or in Expo's cloud, SWSH kept the orchestration, credential handling, and submission logic exactly as they were, and moved only the compute-heavy compilation step onto hardware it owns.&lt;/p&gt;

&lt;h3&gt;
  
  
  Credentials nobody on the team has to touch
&lt;/h3&gt;

&lt;p&gt;Signing credentials for both App Store Connect and Google Play live in EAS. The only secret SWSH's CI holds is a single Expo access token. Code signing is the part of a mobile release you least want sitting in a CI config or a repo, and for SWSH it just isn't there.&lt;/p&gt;

&lt;h3&gt;
  
  
  Submission across both stores, with a custom layer for iOS
&lt;/h3&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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcr53qwjaoml5c57aba44.jpg" 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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcr53qwjaoml5c57aba44.jpg" alt="Agentic Release pipeline" width="800" height="432"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;EAS Submit pushes each build directly to Google Play and App Store Connect with no custom code needed. On Google Play, that's the entire job. iOS asks for more work around the binary: a new App Store version, metadata and release notes, a configured App Clip experience, and a submission for review.&lt;/p&gt;

&lt;p&gt;So SWSH pairs EAS Submit with an App Store Connect pipeline it built itself. Once EAS Submit uploads the build, that pipeline talks to the App Store Connect API directly: it polls until the build finishes processing, creates the App Store version, fills in metadata and the "What's New" text, configures the App Clip's default experience (card subtitle, card image, all of it), and submits the version for review.&lt;/p&gt;

&lt;p&gt;If you've ever configured an App Clip experience by hand, you know how many screens that involves. SWSH turned all of it into one more automated step.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;SWSH manages App Store metadata through this custom pipeline instead of &lt;a href="https://docs.expo.dev/eas/metadata/" rel="noopener noreferrer"&gt;EAS Metadata&lt;/a&gt;, Expo's declarative metadata solution. Most teams don't need to build something like this and should just reach for EAS Metadata directly.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Validating a release before it ships
&lt;/h3&gt;

&lt;p&gt;Before anything goes out, SWSH runs an in-house end-to-end testing framework called &lt;strong&gt;Meridian&lt;/strong&gt;, built on Appium and WebDriverIO and driven by Vitest. Meridian exercises the flows that actually matter (login, sign-up, upload, deep linking) across iOS, the App Clip, and Android, on simulators and emulators, in CI. When SWSH marks a release as validated, that means the app was actually driven through those flows, not just that it compiled without errors.&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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvvfzn5voxy4vx3hkwmsx.jpg" 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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvvfzn5voxy4vx3hkwmsx.jpg" alt="SWSH validation gate and E2E Testing" width="800" height="466"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Shipping JS-only fixes without a store review
&lt;/h3&gt;

&lt;p&gt;For changes that don't touch native code, &lt;a href="https://docs.expo.dev/eas-update/introduction" rel="noopener noreferrer"&gt;EAS Update&lt;/a&gt; lets SWSH push JavaScript fixes immediately, no store review required.&lt;/p&gt;

&lt;p&gt;The part worth stealing: Meridian validates these too. An EAS Update ships new JavaScript onto a native build that's already installed on users' devices, so SWSH runs the same test suite against the updated bundle paired with the build it's landing on, to confirm the combination works and nothing regressed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Watching for regressions after release
&lt;/h3&gt;

&lt;p&gt;Shipping isn't where the release ends. SWSH uses &lt;a href="https://expo.dev/solutions/expo-observe" rel="noopener noreferrer"&gt;EAS Observe to watch app performance in production&lt;/a&gt;, tracking runtime metrics like first render and time-to-interactive. When a release introduces a startup or rendering regression, it shows up there, and the team pushes a fix over the air instead of waiting for the next native release.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this buys them
&lt;/h2&gt;

&lt;p&gt;Engineers push changes and move on. The pipeline builds, validates, submits, and monitors each release on its own, across roughly 300 builds a month, a native release about once a week, and same-day JavaScript fixes shipped over the air.&lt;/p&gt;

&lt;p&gt;SWSH's setup is a decent argument that shipping often and shipping safely aren't opposites. Get the pipeline right once, and the attention that used to go into release logistics goes back into the app itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to start
&lt;/h2&gt;

&lt;p&gt;If you're eyeing something like this for your own app:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Start with &lt;a href="https://docs.expo.dev/build/introduction" rel="noopener noreferrer"&gt;EAS Build&lt;/a&gt; and &lt;a href="https://docs.expo.dev/submit/introduction" rel="noopener noreferrer"&gt;EAS Submit&lt;/a&gt; in the cloud before you consider local builds. Most teams never need to self-host.&lt;/li&gt;
&lt;li&gt;Add &lt;a href="https://docs.expo.dev/eas-update/introduction" rel="noopener noreferrer"&gt;EAS Update&lt;/a&gt; for JS-only fixes once your native release cadence settles into a rhythm.&lt;/li&gt;
&lt;li&gt;Look at &lt;a href="https://docs.expo.dev/eas/metadata/" rel="noopener noreferrer"&gt;EAS Metadata&lt;/a&gt; before building a custom App Store Connect integration like SWSH's. It covers most teams' needs without the extra maintenance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This post is based on content from the &lt;a href="https://expo.dev/blog/inside-swsh-automated-mobile-release-pipeline-with-expo" rel="noopener noreferrer"&gt;Expo blog&lt;/a&gt;. Follow &lt;a href="https://dev.to/expo"&gt;@expo&lt;/a&gt; for more React Native content.&lt;/p&gt;

</description>
      <category>expo</category>
      <category>mobile</category>
      <category>reactnative</category>
    </item>
    <item>
      <title>Life is too short for pasting images into TextInput to be this hard</title>
      <dc:creator>Dan</dc:creator>
      <pubDate>Fri, 04 Sep 2026 15:26:01 +0000</pubDate>
      <link>https://dev.to/expo/life-is-too-short-for-pasting-images-into-textinput-to-be-this-hard-3c3g</link>
      <guid>https://dev.to/expo/life-is-too-short-for-pasting-images-into-textinput-to-be-this-hard-3c3g</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a guest post from *&lt;/em&gt;&lt;a href="https://arunabhverma.com/" rel="noopener noreferrer"&gt;Arunabh Verma&lt;/a&gt;&lt;strong&gt;, a **&lt;a href="https://reactnative.dev/" rel="noopener noreferrer"&gt;React Native&lt;/a&gt;&lt;/strong&gt; developer and founder of &lt;strong&gt;&lt;a href="https://powstac.com/" rel="noopener noreferrer"&gt;Powstać&lt;/a&gt;&lt;/strong&gt; who spends most of his time building mobile products with React Native, focusing on performance, interaction design, and the small details that make apps feel polished and intuitive.*&lt;/p&gt;

&lt;h1&gt;
  
  
  expo-paste-input: pasting images into TextInput shouldn't be this hard
&lt;/h1&gt;

&lt;p&gt;You copy a screenshot from another app, tap into your chat composer, and paste it. On any native app, that just works. No file picker, no extra taps, no ceremony.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://powstac.com/" rel="noopener noreferrer"&gt;Powstać&lt;/a&gt; was building a chat app, and that one interaction was missing. We had image attachments, file uploads, media sharing, all the stuff you'd expect. But paste-to-attach wasn't there, and once I noticed it was missing, I couldn't unnotice it.&lt;/p&gt;

&lt;p&gt;So I built it. It eventually became &lt;a href="https://github.com/arunabhverma/expo-paste-input" rel="noopener noreferrer"&gt;&lt;code&gt;expo-paste-input&lt;/code&gt;&lt;/a&gt;, a native Expo module that wraps the standard React Native &lt;a href="https://reactnative.dev/docs/textinput" rel="noopener noreferrer"&gt;&lt;code&gt;TextInput&lt;/code&gt;&lt;/a&gt; and adds &lt;code&gt;onPaste&lt;/code&gt; support for images, GIFs, and even iOS stickers, without replacing the input or asking developers to learn a new component.&lt;/p&gt;

&lt;p&gt;Here's how it came together, including the parts that didn't work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The library we started with
&lt;/h2&gt;

&lt;p&gt;At first we used Mattermost's &lt;a href="https://github.com/mattermost/react-native-paste-input" rel="noopener noreferrer"&gt;&lt;code&gt;react-native-paste-input&lt;/code&gt;&lt;/a&gt;, the same library &lt;a href="https://bsky.app/" rel="noopener noreferrer"&gt;Bluesky&lt;/a&gt; was using at the time. It supported image pasting on iOS and Android and solved our problem without us writing any native code. Small feature, but the UX improvement was immediate.&lt;/p&gt;

&lt;p&gt;Then we migrated to React Native's New Architecture, and things started breaking. Not with one clean error, but in pieces: some parts worked, some didn't, and behavior diverged between platforms. We patched it, then patched it again. iOS stayed mostly functional. Android got harder to maintain with every release. At some point I realized we were spending more time working around the library than benefiting from it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I didn't build a custom input
&lt;/h2&gt;

&lt;p&gt;Around the same time, &lt;a href="https://swmansion.com/" rel="noopener noreferrer"&gt;Software Mansion&lt;/a&gt; was working on richer input capabilities with &lt;a href="https://github.com/software-mansion/react-native-enriched-html" rel="noopener noreferrer"&gt;&lt;code&gt;react-native-enriched&lt;/code&gt;&lt;/a&gt;, a genuinely interesting project for rich text and rich input in React Native. I considered adopting it. I also considered writing a fully custom input from scratch.&lt;/p&gt;

&lt;p&gt;Then I made a list of everything a native text input already handles: text selection, cursor management, autofill, accessibility, keyboard behavior, clipboard integration, IME support, input accessories, and a long tail of platform-specific edge cases. That's years of native behavior users already expect. I didn't want to rebuild &lt;code&gt;TextInput&lt;/code&gt;. I wanted paste support.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;react-native-enriched&lt;/code&gt; solves a bigger problem: rich content editing with embedded media. My use case was narrower. When a user pastes an image into a chat app, I don't want that image inserted into the text field itself. I want the file. I want a URI. I want to treat it as an attachment, same as if they'd picked it from the gallery.&lt;/p&gt;

&lt;p&gt;That distinction became the whole design.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap it, don't rebuild it
&lt;/h2&gt;

&lt;p&gt;While researching approaches, I found &lt;a href="https://x.com/fernandorojo" rel="noopener noreferrer"&gt;Fernando Rojo&lt;/a&gt;'s write-up on how &lt;a href="https://v0.app/" rel="noopener noreferrer"&gt;v0&lt;/a&gt; handled paste. The idea: don't rebuild &lt;code&gt;TextInput&lt;/code&gt;, wrap it.&lt;/p&gt;

&lt;p&gt;Instead of a new input component with its own styling and API surface, a wrapper sits around the normal &lt;code&gt;TextInput&lt;/code&gt;, observes native paste events, and hands the app a file instead of inserting content into the field. The app keeps full control of the input. The library only adds paste intelligence.&lt;/p&gt;

&lt;p&gt;That's the API I wanted:&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;// Composer.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;TextInput&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;react-native&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PasteInputWrapper&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-paste-input&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;Composer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PasteInputWrapper&lt;/span&gt;
      &lt;span class="na"&gt;onPaste&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;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;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;images&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;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&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="nx"&gt;uris&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;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;text&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;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&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="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;&lt;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;TextInput&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;"Type a message"&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;PasteInputWrapper&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No custom editor, no prop mirroring, no special styling system, no replacing a component developers already trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  iOS: clipboard access is delicate
&lt;/h2&gt;

&lt;p&gt;I started on iOS, where the first surprise was how careful you have to be with clipboard access. Modern iOS versions can show privacy prompts if an app inspects clipboard contents too early, so checking the pasteboard on every focus event is a bad idea. The wrapper only reads from &lt;code&gt;UIPasteboard&lt;/code&gt; after the user explicitly triggers a paste.&lt;/p&gt;

&lt;p&gt;Once that happens, the native layer inspects the clipboard for text, images, GIFs, WebP, HEIC, and other useful formats. If it finds media, it writes the content to temporary files and sends local file URIs back to JavaScript. The payload stays small on purpose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;PasteEventPayload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;images&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;uris&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="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unsupported&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;That gave me the shape I wanted from the start: a paste event, and the app decides what happens next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Android: more fragmented, more work
&lt;/h2&gt;

&lt;p&gt;Android's clipboard and content system is more spread out. For Android 12 and above, the right path is &lt;code&gt;OnReceiveContentListener&lt;/code&gt;, which lets native views accept rich content like images and media. But that alone wasn't enough, because Android also has insertion menus, selection menus, clipboard managers, and multiple ways a paste can get triggered. To make this reliable, the library also hooks into native paste actions through Android's text editing APIs.&lt;/p&gt;

&lt;p&gt;The behavior needed is simple to state: paste text and it appears as text, paste an image and it becomes an attachment. Getting there took more care. If the clipboard has text, Android should behave normally and the user shouldn't lose standard input behavior. If it has media, the wrapper intercepts it, saves the content to cache storage, and emits file URIs to JavaScript. That's the difference between a broken "can't paste image" moment and a composer that just works.&lt;/p&gt;

&lt;h2&gt;
  
  
  The edge cases that never stop
&lt;/h2&gt;

&lt;p&gt;Once the basics worked, the real work started. GIFs needed to stay GIFs instead of collapsing into static images. Transparent PNGs needed to stay transparent, so the library preserves PNG output when there's an alpha channel and only uses JPEG when appropriate.&lt;/p&gt;

&lt;p&gt;Screenshots were their own surprise. An image copied from the Photos app looks different on the clipboard than a screenshot copied directly from the system screenshot UI. Different payloads, different underlying data types. My original assumptions about "an image is an image" were wrong, so the implementation got smarter about identifying content types instead of assuming every image arrives the same way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stickers, and the issue that changed my thinking
&lt;/h2&gt;

&lt;p&gt;Somebody opened an issue asking about iOS stickers. Even before stickers were supported, they pointed out the library should at least avoid inserting stray characters into the text field when someone pasted one. They were right, and most apps don't handle this well.&lt;/p&gt;

&lt;p&gt;On iOS, stickers aren't always exposed as normal clipboard images. Newer iOS versions can insert them through text attachments and adaptive image glyphs instead of traditional image formats. So the library started watching for &lt;code&gt;NSTextAttachment&lt;/code&gt; and, on iOS 18, &lt;code&gt;NSAdaptiveImageGlyph&lt;/code&gt;. When sticker content shows up, the wrapper extracts the underlying image data, strips the attachment content out of the text field, keeps the cursor position intact, writes the media to temporary storage, and emits a normal image paste event.&lt;/p&gt;

&lt;p&gt;The first version only handled static stickers. Animated sticker support came later. This ended up being my favorite part of the module, because it's the kind of thing users just expect to work, and when it works, nobody notices, which is exactly the point.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does today
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/arunabhverma/expo-paste-input" rel="noopener noreferrer"&gt;&lt;code&gt;expo-paste-input&lt;/code&gt;&lt;/a&gt; now supports text paste, image paste, multiple image paste, GIF paste, transparent images, screenshots, iOS stickers, and animated stickers, all while keeping developers on the standard &lt;code&gt;TextInput&lt;/code&gt; they already use. No custom editor, no custom composer, no special rendering pipeline. Just a wrapper.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;p&gt;This is a young, community-maintained module, not something Expo ships as core. If your app has a heavily customized text editing pipeline (rich text, embedded content, multi-format documents), &lt;code&gt;react-native-enriched&lt;/code&gt; or a similar rich editor is probably a better fit than wrapping paste behavior around a plain &lt;code&gt;TextInput&lt;/code&gt;. And clipboard/content APIs on both platforms keep changing across OS versions, so expect some new edge cases to show up over time, especially around newer iOS sticker formats.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this started as a client problem, not an open source project
&lt;/h2&gt;

&lt;p&gt;This didn't begin as a library. It began as a requirement inside a real client product with real users and real platform constraints. That's what made it worth solving properly instead of shipping a quick patch.&lt;/p&gt;

&lt;p&gt;We solved it for the app first. Once the implementation got more complete, it was clear other React Native developers were probably hitting the same wall. I also reached out to the Bluesky team, since they were using the same Mattermost-based approach we started with, and ended up contributing changes back to that conversation too.&lt;/p&gt;

&lt;p&gt;That's the part of React Native I like most: a small fix in one app turning into something useful for a lot of other developers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to start
&lt;/h2&gt;

&lt;p&gt;Behind a single paste action sits clipboard privacy rules, temporary file handling, GIF and image format detection, Android content APIs, native text editing internals, and platform-specific sticker formats, none of which the user should ever have to think about.&lt;/p&gt;

&lt;p&gt;If you're building a chat app, social feed, notes app, or anything where people share media constantly, try &lt;a href="https://github.com/arunabhverma/expo-paste-input" rel="noopener noreferrer"&gt;&lt;code&gt;expo-paste-input&lt;/code&gt;&lt;/a&gt; in your project. If you hit an edge case I haven't found yet, open an issue on the repo, I'd genuinely like to know about it.&lt;/p&gt;

&lt;p&gt;This post is based on content from the &lt;a href="https://expo.dev/blog/pasting-images-into-textinput-should-not-be-this-hard" rel="noopener noreferrer"&gt;Expo blog&lt;/a&gt;. Follow &lt;a href="https://dev.to/expo"&gt;@expo&lt;/a&gt; for more React Native content.&lt;/p&gt;

</description>
      <category>expo</category>
      <category>javascript</category>
      <category>reactnative</category>
      <category>mobile</category>
    </item>
    <item>
      <title>From Istanbul to #1 in the US App Store: why FERASET bet its AI app studio on Expo</title>
      <dc:creator>Dan</dc:creator>
      <pubDate>Mon, 31 Aug 2026 20:44:40 +0000</pubDate>
      <link>https://dev.to/expo/from-istanbul-to-1-in-the-us-app-store-why-feraset-bet-its-ai-app-studio-on-expo-b3p</link>
      <guid>https://dev.to/expo/from-istanbul-to-1-in-the-us-app-store-why-feraset-bet-its-ai-app-studio-on-expo-b3p</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://expo.dev/blog/from-istanbul-to-1-in-the-us-app-store-why-feraset-bet-its-ai-app-studio-on-expo" rel="noopener noreferrer"&gt;expo.dev/blog&lt;/a&gt; by Said Y. Kaban.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;*This is a guest post from Said Kaban - he is the CTO and Co-Founder of FERASET. *&lt;/p&gt;

&lt;p&gt;…&lt;/p&gt;

&lt;p&gt;In late 2022, most mobile engineers I knew thought Expo was a prototyping tool. Something you'd use for a demo or a hackathon, then switch to the React Native CLI when you wanted to build anything real. Especially in Turkey, where we're based, this was the default take.&lt;/p&gt;

&lt;p&gt;We thought differently. We looked at where Expo was going and saw the same trajectory as Next.js in the React ecosystem. An opinionated, managed layer that doesn't limit you but makes you way faster. So we went all in.&lt;/p&gt;

&lt;p&gt;Today &lt;a href="https://www.feraset.co/" rel="noopener noreferrer"&gt;FERASET&lt;/a&gt; is an AI consumer app studio of ~40 people in Istanbul. We've hit #1 globally in the App Store in multiple countries including the US. Our entire mobile stack runs on Expo. It has since day one.&lt;/p&gt;

&lt;p&gt;Here's how that played out.&lt;/p&gt;

&lt;h2&gt;
  
  
  We started at the right time, with the right bet
&lt;/h2&gt;

&lt;p&gt;FERASET started right when the AI boom hit. ChatGPT had just launched. DALL-E was making headlines. Midjourney was everywhere. But they were all web products. If you wanted to create with AI, you were on a desktop or in a mobile browser. Nobody had built great native mobile experiences for AI yet and we knew that's where the users actually were.&lt;/p&gt;

&lt;p&gt;So speed was the only thing that mattered. The teams that got polished AI apps into the stores first would win. Every week of delay meant losing ground.&lt;/p&gt;

&lt;p&gt;We needed a framework that would let a small team ship to both platforms fast and not waste time on native toolchain issues. Expo gave us that. &lt;a href="https://play.google.com/store/apps/details?id=com.feraset.tune&amp;amp;hl=en" rel="noopener noreferrer"&gt;Music AI,&lt;/a&gt; our first app, went from the first line of code to live in the App Store in 4 weeks. A full MVP, both platforms, submitted and approved. That's the kind of speed Expo made possible and it's what let us move before the market got crowded.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our first app hit #1 globally
&lt;/h2&gt;

&lt;p&gt;Our first app was Music AI, an AI cover generation app. You could create AI covers of songs with different voices, or even train a custom voice of yourself or your loved ones and make them sing. It took off fast.&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnfiwo716o9lusbspmt4j.jpg" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnfiwo716o9lusbspmt4j.jpg" alt="Feraset app" width="800" height="508"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Music AI became the best AI cover app in the world. We surpassed Spotify in the App Store charts and reached the #1 spot globally in multiple countries including the US and major European markets.&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2c6bubjbnhf8rj326hco.jpg" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2c6bubjbnhf8rj326hco.jpg" alt="Hit #1 in the app store" width="799" height="419"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That confirmed two things for us. The mobile AI market was real and massive. And Expo could absolutely handle production at scale. This wasn't a prototyping tool. This was the framework we'd build the whole company on.&lt;/p&gt;

&lt;p&gt;From there, the next step felt obvious. The AI consumer space isn't a single-app market. Different users want different experiences: image generation, video, specific creative tools. One app can't serve all of them well. So instead of trying to stuff everything into Music AI, we decided to run FERASET as a studio. Multiple apps, each focused on a specific use case, all built by the same core team on Expo.&lt;/p&gt;

&lt;h2&gt;
  
  
  A real example of what we build on Expo
&lt;/h2&gt;

&lt;p&gt;The best way to show what building on Expo actually looks like for us is through a specific example: &lt;a href="https://github.com/saidkaban/expo-ai-kit" rel="noopener noreferrer"&gt;&lt;strong&gt;expo-ai-kit&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  expo-ai-kit: on-device AI for Expo apps
&lt;/h3&gt;

&lt;p&gt;All of our apps use language models for things like prompt refinement, content guideline checks and text generation. Tasks that need an LLM but don't need a state-of-the-art model. For a long time, that meant API calls to cloud providers for every single request. It worked, but we were paying for inference on tasks that a small on-device model could handle just fine and user prompts were leaving the device when they didn't need to.&lt;/p&gt;

&lt;p&gt;Apple Foundation Models on iOS and Google ML Kit's Prompt API on Android changed that equation. For the kind of tasks we need, these on-device models are more than good enough. And they're free. No API keys, no per-token costs, no network dependency. The problem was there was no clean way to use them from Expo. So we built expo-ai-kit.&lt;/p&gt;

&lt;p&gt;It's an open-source Expo module that gives you a simple JavaScript API for on-device language models on both platforms. Here's what it looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;isAvailable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sendMessage&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;expo-ai-kit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;available&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;isAvailable&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;available&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sendMessage&lt;/span&gt;&lt;span class="p"&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="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Refine this prompt for image generation: a cat sitting&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;systemPrompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;You are a prompt engineering assistant.&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;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&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;There's also streaming support for progressive text display:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;streamMessage&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;expo-ai-kit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;promise&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;stop&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;streamMessage&lt;/span&gt;&lt;span class="p"&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="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Check this text against content guidelines...&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;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setResponseText&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="nx"&gt;accumulatedText&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;systemPrompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;You are a content moderation assistant.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Just &lt;code&gt;npm install expo-ai-kit&lt;/code&gt;, add the config plugin and you're running on-device inference. The library currently gets tens of thousands of weekly downloads on &lt;a href="https://www.npmjs.com/package/expo-ai-kit" rel="noopener noreferrer"&gt;npm&lt;/a&gt;.&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F54s2wjbu7alp9sgjmaqb.jpg" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F54s2wjbu7alp9sgjmaqb.jpg" alt="expo ai kit" width="800" height="576"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For us, tasks that used to cost money on every request now run for free on the device. Response times for simple tasks dropped because there's no network round trip. And as on-device models keep getting more capable with every new chip generation, the range of tasks we can offload from the cloud will only grow.&lt;/p&gt;

&lt;p&gt;We open-sourced it because we figured other developers would run into the same problem. And building it reinforced something we'd been experiencing all along: expo-ai-kit is real Swift and Kotlin code, talking to platform-level AI frameworks, integrated through &lt;a href="https://docs.expo.dev/modules/overview/" rel="noopener noreferrer"&gt;Expo's module system&lt;/a&gt; and distributed as a config plugin. No ejecting, no bare workflow. You can go deep into native when you need to and still stay in the managed ecosystem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hardest part wasn't the tech
&lt;/h2&gt;

&lt;p&gt;The biggest challenge we faced was the developer ecosystem in Turkey.&lt;/p&gt;

&lt;p&gt;When we started, most mobile engineers here were more comfortable with other frameworks. Expo wasn't really on their radar for production apps and to be fair, that was a reasonable take at the time given how young the ecosystem was. Getting experienced engineers to give it a serious look took some convincing.&lt;/p&gt;

&lt;p&gt;Hiring was tough early on. We couldn't just post a listing for an Expo engineer and get a pile of qualified candidates. We had to find engineers who were open to it, educate them on why Expo was the right choice and prove it through results.&lt;/p&gt;

&lt;p&gt;We spent time building up the React Native and Expo community in Turkey. Sharing what we were learning, making the case that the React ecosystem would win mobile the same way React won the web. We wanted to spearhead that in Turkey rather than wait for the market to catch up.&lt;/p&gt;

&lt;p&gt;It worked. But it took effort that had nothing to do with writing code.&lt;/p&gt;

&lt;h2&gt;
  
  
  What surprised us about Expo
&lt;/h2&gt;

&lt;p&gt;The stability of &lt;a href="https://expo.dev/blog/what-is-continuous-native-generation" rel="noopener noreferrer"&gt;Expo's managed workflow&lt;/a&gt; at scale was the biggest surprise. We're on CNG which helps immensely because we ship so fast and we create different builds constantly: dev, preview, prod. The ease of creating those builds and distributing them to our product teams for testing and submission has been amazingly smooth.&lt;/p&gt;

&lt;p&gt;When you're a small team, managed workflows feel like training wheels. You assume you'll outgrow them. We haven't. The team is bigger, the apps are more complex, the user base is in the millions and we're still fully managed. The benefits scale with your app and your team.&lt;/p&gt;

&lt;p&gt;The upgrade path between SDK versions has also been smoother than expected. Upgrading a single app is one thing. Upgrading multiple apps that share code is a coordination challenge. Expo's upgrade tooling and migration guides make this significantly less painful than it could be.&lt;/p&gt;

&lt;p&gt;And honestly, the pace of innovation from the Expo team keeps impressing us. They've consistently been ahead of the curve. &lt;a href="https://docs.expo.dev/router/introduction/" rel="noopener noreferrer"&gt;File-based routing&lt;/a&gt; was a great example of that. They saw where the developer experience was going and got there early.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next?
&lt;/h2&gt;

&lt;p&gt;Now that the era of agentic coding has officially taken over, Expo is pushing that front too. We're eagerly expecting the official launch of &lt;a href="https://expo.dev/services/agent" rel="noopener noreferrer"&gt;Expo Agent&lt;/a&gt;, which we believe will continue to power and accelerate our client-side development process. The idea of AI-assisted development built directly into the framework we already use is exactly the kind of forward-thinking move that made us bet on Expo in the first place.&lt;/p&gt;

&lt;p&gt;On our side, we're continuing to expand &lt;a href="https://apps.apple.com/us/developer/superapp-labs/id1673699641" rel="noopener noreferrer"&gt;our product portfolio&lt;/a&gt;. The AI consumer space is still growing fast and we see big opportunities in video generation and real-time AI experiences on mobile. All of it will be built on Expo.&lt;/p&gt;

&lt;p&gt;If you're a small team somewhere thinking about building consumer apps, here's what I'd say: don't overthink your stack. Pick Expo, start shipping and focus your energy on the product. The framework will scale with you. Ours did, from a tiny team in Istanbul to apps that hit number one globally. And we're just getting started.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;FERASET is an AI consumer app studio based in Istanbul. Our apps are available on the App Store and Google Play. As is standard practice among AI app studios and mobile gaming studios, our apps are published under different developer accounts for various operational purposes, so you won't find them all under the FERASET name.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>ai</category>
      <category>cng</category>
    </item>
    <item>
      <title>How Hipcamp upgraded Expo SDK versions with Claude Code</title>
      <dc:creator>Dan</dc:creator>
      <pubDate>Wed, 22 Jul 2026 16:59:21 +0000</pubDate>
      <link>https://dev.to/expo/how-hipcamp-upgraded-expo-sdk-versions-with-claude-code-4jb0</link>
      <guid>https://dev.to/expo/how-hipcamp-upgraded-expo-sdk-versions-with-claude-code-4jb0</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://expo.dev/blog/how-hipcamp-upgraded-expo-sdk-versions-with-claude-code" rel="noopener noreferrer"&gt;expo.dev/blog&lt;/a&gt; by Armaiz Adenwala.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is a guest post from Armaiz Adenwala - Armaiz is responsible for the mobile app experience at Hipcamp.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;…&lt;/p&gt;

&lt;p&gt;Upgrading React Native has always been a scary task for any engineer. It requires touching unfamiliar native code, crucial libraries, and resolving breaking changes. Anything could raise an error if not tested thoroughly. Over time tools like &lt;a href="https://react-native-community.github.io/upgrade-helper/" rel="noopener noreferrer"&gt;React Native Upgrade helper&lt;/a&gt; eased the process and frameworks like Expo were built around making this process go smoother.&lt;/p&gt;

&lt;h2&gt;
  
  
  The complexity of upgrades
&lt;/h2&gt;

&lt;p&gt;Unfortunately, not every React Native app is in a perfect state. It can be very difficult for a fast paced company to balance maintaining dependencies and shipping features. As a result, upgrades become a much larger endeavor. At Hipcamp, we recently faced the challenge of upgrading to New Architecture + Expo 54. With New Architecture, we had a few dozen libraries that were outdated and needed to be either upgraded or replaced.&lt;/p&gt;

&lt;p&gt;If you have worked on React Native enough, you would know that simply bumping to the latest version is not enough. There is a large amount of time spent debugging and addressing issues. Additionally, multiple libraries could conflict with each other and you may need to find compatible versions between the two.&lt;/p&gt;

&lt;p&gt;Luckily, this time we had already built &lt;a href="https://www.hipcamp.com/journal/engineering/scout-our-in-house-ai-agent" rel="noopener noreferrer"&gt;Scout&lt;/a&gt;, an AI agent designed to tackle whatever task we throw at it. What would have taken an engineer weeks of tedious dependency research and version resolution, Scout handled across a few focused sessions over several days. While having an internal agent like Scout helps, we will be sharing how you can tackle React Native upgrades with pure Claude Code.&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9o07tblg72xjkgldb28u.jpg" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9o07tblg72xjkgldb28u.jpg" alt="Scout sped up Hipcamp's upgrade" width="800" height="718"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The State of the Hipcamp App pre-upgrade
&lt;/h2&gt;

&lt;p&gt;We haven’t kept up with our app versioning enough outside security updates. Abandoned packages, outdated packages from 4-6 years ago, etc. We had over 100 dependencies with roughly 40 of them needing to be upgraded to support New Arch.&lt;/p&gt;

&lt;p&gt;Given how lean our company is, we didn’t have cycles to juggle upgrades on top of regular product work. It wasn’t a cost issue, it was a time issue for us. We couldn’t justify taking an engineer off an impactful project to work on this major upgrade for a couple months.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to scope an upgrade
&lt;/h2&gt;

&lt;p&gt;Before we dive into how we accomplished this upgrade, we need to discuss the key goals of this process in order to design our prompts.&lt;/p&gt;

&lt;p&gt;We landed on the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Only focus on dependencies that are needed

&lt;ul&gt;
&lt;li&gt;We only upgrade libraries that don’t currently support new arch / 16kb android&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Replace dependencies with an Expo equivalent

&lt;ul&gt;
&lt;li&gt;Expo’s modules are well maintained, reliable, and popular.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Leverage patch-package as a last resort&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The upgrade workflow
&lt;/h3&gt;

&lt;p&gt;We then decided how we wanted this upgrade process to go. We determined we needed to build a multi-phase approach.&lt;/p&gt;

&lt;p&gt;We had to decide how much time/money we were willing to spend on each phase of the work. Ultimately we realized that tokens are far cheaper than having an engineer tirelessly navigate through docs, code, and Github issues. Not to mention, our engineers spent their time saved on developing product features that bring us revenue. So it was a no brainer to not let budget restrict this project.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 1: Build your agent
&lt;/h3&gt;

&lt;p&gt;We highly recommending building an agent like &lt;a href="https://www.hipcamp.com/journal/engineering/scout-our-in-house-ai-agent" rel="noopener noreferrer"&gt;Scout&lt;/a&gt; internally or leverage &lt;a href="https://expo.dev/expo-skills" rel="noopener noreferrer"&gt;Expo’s upgrade skill&lt;/a&gt;. Teach the agent your codebase, design a way to index the architecture of your codebase. We spent a day running our documenter command, /scout-document-architecture, to take notes on every aspect and detail of our app. This prevented Scout from hallucinating or missing anything.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 2: Audit all of your dependencies
&lt;/h3&gt;

&lt;p&gt;We sent our agent on a task to audit all of our dependencies. We recommend using an sqlite db to track everything, you can sync this db to your project management tool, or just resort to using a markdown file.&lt;/p&gt;

&lt;p&gt;We then provided it a prompt. Here is an example of what we gave ours:&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;# Expo SDK 50 → 54 Dependency Audit&lt;/span&gt;

Audit every dependency in the app for Expo 50 → 54 upgrade compatibility.

&lt;span class="gu"&gt;## Context&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Current:**&lt;/span&gt; Expo 50 (RN 0.73, React 18.2)
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Target:**&lt;/span&gt; Expo 54 (RN 0.81, React 19.1)
&lt;span class="p"&gt;-&lt;/span&gt; Expo 52+ enables New Architecture by default
&lt;span class="p"&gt;-&lt;/span&gt; Expo 52+ requires 16kb Android page size support

&lt;span class="gu"&gt;## Research&lt;/span&gt;
For each dependency, you MUST:
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Read GitHub changelogs/releases**&lt;/span&gt; between the Expo 50 and Expo 54 compatible versions
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Search GitHub issues**&lt;/span&gt; for bugs, errors, risks, complaints
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Read setup docs**&lt;/span&gt; and migration guides
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Check for conflicts**&lt;/span&gt; with other dependencies in our stack
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Check if the setup switch is risky or complex**&lt;/span&gt;

Then answer:
&lt;span class="p"&gt;1.&lt;/span&gt; Can this be &lt;span class="gs"&gt;**replaced with an Expo module**&lt;/span&gt;?
&lt;span class="p"&gt;2.&lt;/span&gt; Can this be &lt;span class="gs"&gt;**removed entirely**&lt;/span&gt;?
&lt;span class="p"&gt;3.&lt;/span&gt; Are there &lt;span class="gs"&gt;**known New Arch blockers**&lt;/span&gt; in GitHub issues?
&lt;span class="p"&gt;4.&lt;/span&gt; Are there &lt;span class="gs"&gt;**16kb page size issues**&lt;/span&gt; reported?
&lt;span class="p"&gt;5.&lt;/span&gt; Does this have &lt;span class="gs"&gt;**peer dependency conflicts**&lt;/span&gt; with other deps?
&lt;span class="p"&gt;6.&lt;/span&gt; Is the &lt;span class="gs"&gt;**upgrade path smooth**&lt;/span&gt; or does it require code changes?
&lt;span class="p"&gt;7.&lt;/span&gt; Are there &lt;span class="gs"&gt;**community complaints**&lt;/span&gt; about recent versions?

&lt;span class="gu"&gt;## Subagent Strategy&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**1 subagent**&lt;/span&gt; for small/simple JS-only dependencies
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**5 subagents**&lt;/span&gt; for medium/harder native dependencies
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**10 subagents**&lt;/span&gt; for extreme-complexity dependencies (react-native core, reanimated, maps, firebase, sentry)
&lt;span class="p"&gt;-&lt;/span&gt; Each subagent works on exactly ONE dependency — do not batch

&lt;span class="gu"&gt;## Output&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; One notes file per dependency in &lt;span class="sb"&gt;`./dependencies/{name}.md`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; SQLite DB at &lt;span class="sb"&gt;`./dependencies.db`&lt;/span&gt; with columns: name, expo_50_version, max_version_expo_50, expo_54_version, new_arch_support, sixteenkb_support, status, type (javascript/native), change_recommendation (upgrade/remove/swap), swap_target, complexity (0-10), notes
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Always prefer replacing with an Expo equivalent library if possible**&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Process dependencies one at a time — do not skip ahead

&lt;span class="gu"&gt;## References&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; https://reactnative.directory/
&lt;span class="p"&gt;-&lt;/span&gt; https://docs.expo.dev/workflow/upgrading-expo-sdk-walkthrough/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Phase 3: Review and begin the upgrade!
&lt;/h3&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqq4uselut6nat585j3th.jpg" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqq4uselut6nat585j3th.jpg" alt="Upgrading SDK" width="799" height="321"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then, we manually went through each upgrade item and confirmed it all looked right. We make sure its not removing libraries it shouldn’t, and pick the right alternative library if it recommended any. We highly recommend using &lt;a href="https://docs.expo.dev/workflow/using-libraries/" rel="noopener noreferrer"&gt;Expo’s libraries&lt;/a&gt; if available as they are actively maintained and well documented.&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl4pticapsvsc4y14s81l.jpg" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl4pticapsvsc4y14s81l.jpg" alt="Documenting the process" width="800" height="568"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once complete, we sent off an agent to work on creating prs for every upgrade. For us, the initial run was 3 days of non stop upgrading. We HIGHLY recommend using subagents for every step when dealing with long running tasks. Having each dependency in its own context is critical for a reliable upgrade.&lt;/p&gt;

&lt;p&gt;We gave it the following process:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pick a dependency&lt;/li&gt;
&lt;li&gt;Send a subagent to look at notes from previous step&lt;/li&gt;
&lt;li&gt;Have 1-3 subagents attempt to upgrade to the version specified&lt;/li&gt;
&lt;li&gt;Once complete, send reviewer subagents to review changes&lt;/li&gt;
&lt;li&gt;Run yarn lint, tsc, and react native bundle and address any errors that come up&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.expo.dev/build/introduction/" rel="noopener noreferrer"&gt;Create an EAS build for ios and android&lt;/a&gt;, validate that the build was successful. Address any errors that may come up&lt;/li&gt;
&lt;li&gt;Create a final EAS Build for ios + android&lt;/li&gt;
&lt;li&gt;Create a pr and add builds, manual QA steps, affected screens, and anything else&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When I came back, I had numerous prs to review. It was very quick to go through all of the prs. There were of course a few prs that needed close engineer involvement, but were easy to work on as our agent already shared all of the context, docs, related Github issues, and more in the PR.&lt;/p&gt;

&lt;p&gt;From our experience, we only had a week spent debugging some New Architecture build issues. &lt;strong&gt;Our entire upgrade process went from what was estimated to be 2-3 months to a couple weeks&lt;/strong&gt;. From one quarter to a sprint. Our dependency situation was very concerning, for smaller apps you may notice even less time spent.&lt;/p&gt;

&lt;p&gt;Of course we still had polishing but the bulk of the work was done.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 4: QA, rollout, and monitoring
&lt;/h3&gt;

&lt;p&gt;We wanted a very safe rollout, we required our fellow Hipcamp employees to use the app and call out bugs. We threw a QA party where people from the company joined and QA’d the app together. These are extremely productive.&lt;/p&gt;

&lt;p&gt;We then used the slow rollout feature on iOS and Android. We released iOS first then released Android after we felt confident on iOS.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Monitoring:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It is crucial you have some form of error tracking in place. In addition, it is a good idea to integrate performance profiling into the app. There are many out there, one of which being &lt;a href="https://expo.dev/solutions/expo-observe" rel="noopener noreferrer"&gt;Expo Observe&lt;/a&gt; - we are lucky to be participants in their private preview!&lt;/p&gt;

&lt;p&gt;We also had our agent monitor and fix errors for us as they came through. Whether this was from the QA party or from production, our agent was able to resolve most bugs with minimal engineering involvement.&lt;/p&gt;

&lt;p&gt;To our surprise we saw no major new errors other than a high memory usage issue that became magnified on New Architecture. In fact we saw less errors in total. We attribute this to the fact that engineers can spend time solving the complex work and throughly QA’ing while an agent works through the rest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;AI is changing how engineers work. The hours spent skimming documentation, racing against deadlines, and debugging obscure issues may soon be a thing of the past. This upgrade was proof that AI agents can be a reliable partner on important, challenging projects, and that the real benefit of AI-assisted engineering isn't replacing engineers but freeing them to focus on complex problems instead of toiling through tedious ones.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>upgrade</category>
      <category>claudecode</category>
    </item>
    <item>
      <title>I joined Expo.</title>
      <dc:creator>Dan</dc:creator>
      <pubDate>Tue, 14 Jul 2026 23:43:31 +0000</pubDate>
      <link>https://dev.to/expo/i-joined-expo-3enh</link>
      <guid>https://dev.to/expo/i-joined-expo-3enh</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://expo.dev/blog/seth-webster-joined-expo" rel="noopener noreferrer"&gt;expo.dev/blog&lt;/a&gt; by Seth Webster.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;I recently joined Expo as Chief Developer Evangelist while I continue to serve as Executive Director of the &lt;a href="https://www.linuxfoundation.org/press/linux-foundation-announces-the-formation-of-the-react-foundation" rel="noopener noreferrer"&gt;newly formed&lt;/a&gt; React Foundation. I want to share my excitement about where we are, and why I am so thrilled to be here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I made this move
&lt;/h2&gt;

&lt;p&gt;Most teams do not run out of ideas. They run into drag.&lt;/p&gt;

&lt;p&gt;Over time, more and more effort goes into managing complexity instead of creating value. At some point even the cost of your own &lt;em&gt;success&lt;/em&gt; can be debilitating.&lt;/p&gt;

&lt;p&gt;That is the category of problems Expo has been solving for people &lt;em&gt;for years&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Expo gives developers the fastest path from their brain to a shipped product. Period. It helps teams start quickly, iterate with tight feedback loops, and reach multiple platforms without turning every product decision into infrastructure work.&lt;/p&gt;

&lt;p&gt;That matters because developer velocity is not just about convenience… It is how teams learn faster. It is how companies stay close to their users. It is how more good ideas actually make it into the world.&lt;/p&gt;

&lt;p&gt;This is also how I see React… an incredibly high leverage tool when it comes to my north star: unleashing all of humanity on the totality of the world’s problems. Anything that shortens the distance between a random thought and a solution will, at the very least, catch my eye.&lt;/p&gt;

&lt;h2&gt;
  
  
  Expo &amp;amp; AI
&lt;/h2&gt;

&lt;p&gt;What makes this moment especially compelling to me is the convergence of two shifts.&lt;/p&gt;

&lt;p&gt;AI has become a collaborator in software development and mobile remains one of the primary ways we experience software.&lt;/p&gt;

&lt;p&gt;Those things together matter. When software becomes easier to generate, the bottleneck moves from tooling to shipping. The teams that win will be the ones with the shortest path from intent to working product.&lt;/p&gt;

&lt;p&gt;Expo is incredibly well positioned for that future… I have yet to meet a more developer-obsessed team than the one here at Expo.&lt;/p&gt;

&lt;p&gt;Expo already gives teams rapid feedback loops, a strong foundation in React, and a practical path to building across platforms. Just as important, the team understands something many people miss: speed only matters when it helps people build better products, and better products only happen when people have the time to sweat the small stuff.&lt;/p&gt;

&lt;p&gt;For me, the product Expo is building matters. The people matter just as much.&lt;/p&gt;

&lt;p&gt;In Charlie Cheever, James Ide, and the broader engineering team, I saw something I care deeply about: real technical conviction paired with long-term ecosystem thinking. Then I met Evan Bacon, and it was a transformative meeting for me, one that stayed with me and became part of &lt;em&gt;my&lt;/em&gt; story.&lt;/p&gt;

&lt;p&gt;Moments like that are rare and not easily dismissed.&lt;/p&gt;

&lt;p&gt;The best platform teams do not just solve today’s problems. They create leverage for everyone building on top of them. They think about trust. They think about durability. They think about what developers, and now agents, will need next, not just what they need right now.&lt;/p&gt;

&lt;p&gt;I knew then that these were people I wanted to work with. That this was the kind of work I wanted to be part of.&lt;/p&gt;

&lt;h2&gt;
  
  
  My role at Expo
&lt;/h2&gt;

&lt;p&gt;As Chief Developer Evangelist, my job is to help the Expo ecosystem grow in depth, reach, and impact.&lt;/p&gt;

&lt;p&gt;That means helping teams adopt Expo at scale, strengthening the connection between Expo and the broader React ecosystem, supporting the next generation of AI-assisted development workflows, and growing the global community around Expo and React Native.&lt;/p&gt;

&lt;p&gt;In practical terms, I’ll be spending time with developers, companies, and community leaders to help more ideas become real products.&lt;/p&gt;

&lt;p&gt;I’ve believed for a long time that the most important technology in the world is not software itself.&lt;/p&gt;

&lt;p&gt;It is the people who build it.&lt;/p&gt;

&lt;p&gt;Tools give people leverage. Good tools give them velocity. Great tools give them momentum.&lt;/p&gt;

&lt;p&gt;We knew this when we (React) recommended Expo as the default framework for developing for mobile, but the number of exciting things this team has in store for this next era boggles the mind.&lt;/p&gt;

&lt;p&gt;I couldn’t be more thrilled to be here, and look forward to spending more time with you all.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;P.S. If you want to come work with me/us, &lt;em&gt;[*we're hiring&lt;/em&gt;](&lt;a href="https://expo.dev/careers" rel="noopener noreferrer"&gt;https://expo.dev/careers&lt;/a&gt;)&lt;/em&gt;!*&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>reactfoundation</category>
    </item>
    <item>
      <title>Build an AI QA Agent for Expo Apps with EAS Workflows in minutes today</title>
      <dc:creator>Dan</dc:creator>
      <pubDate>Fri, 03 Jul 2026 16:13:49 +0000</pubDate>
      <link>https://dev.to/expo/build-an-ai-qa-agent-for-expo-apps-with-eas-workflows-in-minutes-today-13nb</link>
      <guid>https://dev.to/expo/build-an-ai-qa-agent-for-expo-apps-with-eas-workflows-in-minutes-today-13nb</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://expo.dev/blog/build-an-ai-qa-agent-for-expo-apps-with-eas-workflows-in-minutes-today" rel="noopener noreferrer"&gt;expo.dev/blog&lt;/a&gt; by Michał Pierzchała.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is a guest post from Michał Pierzchała - Principal Engineer at Callstack's R&amp;amp;D Incubator; created agent-device, React Native Testing Library.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;…&lt;/p&gt;

&lt;p&gt;AI agents are great at producing a lot of code, quickly. More code means more PRs targeting our codebase, and even more demand for a code quality assurance process that works and scales together with agents generating it.&lt;/p&gt;

&lt;p&gt;While AI agents contributing to backend codebases will often do well with integration tests, things are not so bright on the frontend side. When it comes to generating code that produces UI, such as mobile iOS and Android apps, the latest models will often do amazingly well without even checking the results (thank you React for declarative UI that makes this easier). But many times they’ll just miss the mark. Imagine a hardcore React Native developer that can only read code, without access to a mobile device. How certain you can be they’ll nail the job just by looking at the code? Hint: not much. Verification is key.&lt;/p&gt;

&lt;p&gt;This is the gap we want to close for our Expo apps. And in this article, I’ll show you how to do it using existing tools at no extra cost. Let’s dive in!&lt;/p&gt;

&lt;p&gt;With &lt;a href="https://docs.expo.dev/eas/workflows/introduction/" rel="noopener noreferrer"&gt;EAS Workflows&lt;/a&gt;, you can already reuse builds, run custom jobs on Android and iOS, and comment on GitHub pull requests. That turns out to be enough to build a lightweight QA agent today, without introducing a big custom platform.&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9hekhivtucy76k82htqz.jpg" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9hekhivtucy76k82htqz.jpg" alt="QA Agent Workflow" width="800" height="240"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I put together a minimal template here: &lt;a href="https://github.com/callstackincubator/eas-agent-device/" rel="noopener noreferrer"&gt;callstackincubator/eas-agent-device&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The setup is intentionally small:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Expo app with CNG&lt;/li&gt;
&lt;li&gt;EAS Workflows for orchestration&lt;/li&gt;
&lt;li&gt;a tiny Node.js QA agent using AI SDK&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.callstack.com/blog/agent-device-ai-native-mobile-automation-for-ios-android" rel="noopener noreferrer"&gt;&lt;code&gt;agent-device&lt;/code&gt;&lt;/a&gt; for Android and iOS automation&lt;/li&gt;
&lt;li&gt;one GitHub comment with Android and iOS QA results&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important part is not the “AI” label. It’s that you can start with a working baseline in minutes, then expand it as your team needs more coverage.&lt;/p&gt;

&lt;h2&gt;
  
  
  The goal
&lt;/h2&gt;

&lt;p&gt;For every pull request, we want to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;reuse an existing mobile release build with latest JS when possible&lt;/li&gt;
&lt;li&gt;boot an emulator or simulator&lt;/li&gt;
&lt;li&gt;install and launch the app&lt;/li&gt;
&lt;li&gt;let an agent inspect the UI and take screenshots&lt;/li&gt;
&lt;li&gt;post a short QA summary under the PR&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That’s it. Not a giant test framework. Not a replacement for all E2E tests. Just a practical QA loop around mobile UI changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why EAS Workflows fits this really well
&lt;/h2&gt;

&lt;p&gt;The main reason is that EAS Workflows already understands mobile-specific CI.&lt;/p&gt;

&lt;p&gt;What you get easily:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;fingerprint&lt;/code&gt; to detect native changes&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;get-build&lt;/code&gt; to find reusable builds&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;repack&lt;/code&gt; to avoid rebuilding native code when only JS changed&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;linux&lt;/code&gt; and &lt;code&gt;macos&lt;/code&gt; runners with virtualization that run Android and iOS devices&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;github-comment&lt;/code&gt; to send the result back to the PR&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So instead of forcing mobile automation into a generic CI system, you can keep the whole pipeline where the mobile pieces already exist. That makes the setup much easier to reason about.&lt;/p&gt;

&lt;p&gt;I’ll note here we’ll need a &lt;code&gt;linux-medium-nested-virtualization&lt;/code&gt; image for Android job to be able to open Android Emulators, which we’ll need to install from scratch, and &lt;code&gt;macos-medium&lt;/code&gt; image (or larger) for iOS Simulators, which are already available. I must say I was positively surprised with the flexibility of the machines and to what extent I can script them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start from this workflow shape
&lt;/h2&gt;

&lt;p&gt;The core workflow is simple and fits on a single screen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;fingerprint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;fingerprint&lt;/span&gt;

  &lt;span class="na"&gt;android_get_build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;get-build&lt;/span&gt;
    &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;android&lt;/span&gt;
      &lt;span class="na"&gt;profile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;qa-release&lt;/span&gt;

  &lt;span class="na"&gt;android_repack&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;repack&lt;/span&gt;

  &lt;span class="na"&gt;android_build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;build&lt;/span&gt;

  &lt;span class="na"&gt;qa_android&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;linux-medium-nested-virtualization&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;eas/checkout&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;eas/install_node_modules&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;eas/download_build&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;provision_android_emulator&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bash ./scripts/agent-qa/provision-android-emulator.sh&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;run_agent_qa&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bash ./scripts/agent-qa/run-and-export.sh "${{ steps.download_build.outputs.artifact_path }}"&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;AGENT_DEVICE_SESSION&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;qa-android&lt;/span&gt;
          &lt;span class="na"&gt;AGENT_DEVICE_PLATFORM&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;android&lt;/span&gt;

  &lt;span class="na"&gt;qa_comment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github-comment&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For iOS, it’s the same idea, just on a macOS worker with a simulator build. The full working version, with Android and iOS running in parallel, is in the repo: &lt;a href="https://github.com/callstackincubator/eas-agent-device/blob/main/.eas/workflows/agent-qa-mobile.yml" rel="noopener noreferrer"&gt;.eas/workflows/agent-qa-mobile.yml&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The key design choice: split bootstrap from QA
&lt;/h2&gt;

&lt;p&gt;This is the one thing I’d strongly recommend.&lt;/p&gt;

&lt;p&gt;At first glance, you might want the agent to do everything: install the app, open it, navigate, inspect, report. In practice, that makes the system more fragile than it needs to be. Agents can use our tools incorrectly, chose not to read the instructions we asked them to use, or hallucinate flags for CLIs they’re using (been there).&lt;/p&gt;

&lt;p&gt;So the key to make our AI agent work for us reliably, not only at times, is to keep the workflow deterministic as much as it’s possible. And with &lt;code&gt;agent-device&lt;/code&gt; we can script these pretty easily to always provide correct bootstrap parameters when installing and opening the app on a device:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;

&lt;span class="c"&gt;# Phase 1: deterministic bootstrap&lt;/span&gt;
agent-device &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;APP_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;APP_PATH&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
agent-device open &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;APP_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--relaunch&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;agent-device&lt;/code&gt; knows which platform to run thanks to &lt;code&gt;AGENT_DEVICE_PLATFORM&lt;/code&gt; environment variable set in our job.&lt;/p&gt;

&lt;p&gt;The other part of QA process that’s harder to script, can stay agent-driven. The agent will infer acceptance criteria from the PR, inspect the UI in a token-efficient way through accessibility tree, navigate a little, take screenshots, and summarize what happene&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Phase 2: variable agent-driven flow&lt;/span&gt;
npm run agent-qa
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From my own experience building various agents over past month, that split makes the workflow much more reliable. That means the agent never has to guess artifact paths or install commands.&lt;/p&gt;

&lt;h2&gt;
  
  
  The agent can stay very small
&lt;/h2&gt;

&lt;p&gt;Once the app is already running, the agent only needs a few tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;read PR context&lt;/li&gt;
&lt;li&gt;load the &lt;code&gt;agent-device&lt;/code&gt; skill&lt;/li&gt;
&lt;li&gt;run UI actions like &lt;code&gt;snapshot&lt;/code&gt;, &lt;code&gt;press&lt;/code&gt;, &lt;code&gt;screenshot&lt;/code&gt; through &lt;code&gt;agent-device&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;write a final report&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A simplified version looks like 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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ToolLoopAgent&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ai&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ToolLoopAgent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;openai/gpt-5.4-mini&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;instructions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`
    You are a mobile QA agent running inside EAS Workflows.
    Treat the app as a black box.
    Infer acceptance criteria from the PR.
    The app is already installed and launched.
    Use agent-device to inspect the UI, navigate, take screenshots, and write a report.
    If the result is visually plausible but not fully confirmed from structured UI output, use "unsure".
    You must call write_report exactly once.
  `&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;get_pr_context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;load_skill&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;read_skill_file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;agent_device&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;write_report&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s enough to get us started fast and iterate to greatness. I’m using Vercel’s AI SDK as it provides good balance of control vs batteries-included tools. On top of that I can access all the models I like through their AI Gateway service (with no markup, at least yet) or if you don’t want another subscription, you can use your good old &lt;code&gt;OPENAI_API_KEY&lt;/code&gt; and use &lt;code&gt;openai&lt;/code&gt; provider instead. Applies to providers too, not only OpenAI.&lt;/p&gt;

&lt;p&gt;The full agent is here: &lt;a href="https://github.com/callstackincubator/eas-agent-device/blob/main/scripts/agent-qa/index.ts" rel="noopener noreferrer"&gt;scripts/agent-qa/index.ts&lt;/a&gt; - I tried to keep it brief.&lt;/p&gt;

&lt;h2&gt;
  
  
  Report only what’s necessary
&lt;/h2&gt;

&lt;p&gt;Keep the output small and useful. In our template, each platform produces verification status, one of: &lt;code&gt;passed&lt;/code&gt;, &lt;code&gt;failed&lt;/code&gt; ,&lt;code&gt;blocked&lt;/code&gt; ,&lt;code&gt;unsure&lt;/code&gt;. And then a short section with a summary, checks performed, issues found, screenshots, and a full JSON report in a collapsible block (for debugging failed QA verification mostly).&lt;/p&gt;

&lt;p&gt;That last status, &lt;code&gt;unsure&lt;/code&gt;, is important. Mobile UI is not always easy to verify from structured automation output alone. Sometimes accessibility trees help a lot. Sometimes the screenshot is the strongest evidence. If the agent cannot prove the result cleanly, it should say so and attach the image.&lt;/p&gt;

&lt;p&gt;That is much better than pretending it knows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final step: a comment under Pull Request
&lt;/h2&gt;

&lt;p&gt;Like with human verification, a short comment under our work is often time all we need to get a good overview of what agent was able to verify (or not), together with a visual confirmation that we often need so much to truly assess the change is not breaking our app. A single PR comment works really well:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;## Agent QA

| Platform | Status    |
| -------- | --------- |
| Android  | ✅ passed |
| iOS      | 🤔 unsure |

### Android
Short summary...
Screenshots

### iOS
Short summary...
Screenshots
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That puts the result exactly where reviewers already are, with necessary information and visual feedback at hand.&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwpcpcmd34iy0jg31pxag.jpg" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwpcpcmd34iy0jg31pxag.jpg" alt="QA Agent before and after" width="800" height="625"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡There’s no official API to upload screenshots to GitHub comments, so instead in our example we used Vercel Blob as a 3rd party cloud to store images. Replace it with the solution that works for your use case, e.g. AWS S3.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What I’d recommend if you want to try this today
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Connect your GitHub project to EAS Workflows from Expo dashboard&lt;/li&gt;
&lt;li&gt;Start with one platform first, e.g. Android.&lt;/li&gt;
&lt;li&gt;Use CNG and keep native build reuse enabled.&lt;/li&gt;
&lt;li&gt;Keep your QA build profiles separate from production.&lt;/li&gt;
&lt;li&gt;Make bootstrap deterministic.&lt;/li&gt;
&lt;li&gt;Keep the agent black-box only.&lt;/li&gt;
&lt;li&gt;Post one PR comment, not ten different artifacts.&lt;/li&gt;
&lt;li&gt;Add screenshots early. They help a lot.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then, once that works, extend it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;add iOS&lt;/li&gt;
&lt;li&gt;upload screenshots to Blob storage&lt;/li&gt;
&lt;li&gt;add better selectors&lt;/li&gt;
&lt;li&gt;turn successful exploratory checks into more deterministic flows&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;You do not need a huge AI testing platform to get useful mobile QA automation.&lt;/p&gt;

&lt;p&gt;Expo together with EAS Workflows already gives you most of the infrastructure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;build reuse: for fast iteration and repack for up-to-date JS bundle&lt;/li&gt;
&lt;li&gt;mobile CI workers: with scripting and virtualization, so simulators and emulators are available&lt;/li&gt;
&lt;li&gt;workflow orchestration: to put this all together&lt;/li&gt;
&lt;li&gt;GitHub integration: to reduce cognitive load and keep verification inside Pull Requests&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From there, a custom QA agent can stay surprisingly small. And thanks to TypeScript-based AI SDK, you can bend it to all your needs (and it’s pretty flexible).&lt;/p&gt;

&lt;p&gt;That’s why I like this setup: it’s something you can build today, in minutes, starting from a simple template, and grow only when you actually need more.&lt;/p&gt;

&lt;p&gt;Start here: &lt;a href="https://github.com/callstackincubator/eas-agent-device/" rel="noopener noreferrer"&gt;callstackincubator/eas-agent-device&lt;/a&gt;&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>qa</category>
      <category>ai</category>
    </item>
    <item>
      <title>What Expo’s Series B funding means for you</title>
      <dc:creator>Dan</dc:creator>
      <pubDate>Sat, 27 Jun 2026 14:41:56 +0000</pubDate>
      <link>https://dev.to/expo/what-expos-series-b-funding-means-for-you-fm5</link>
      <guid>https://dev.to/expo/what-expos-series-b-funding-means-for-you-fm5</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://expo.dev/blog/what-expo-s-series-b-funding-means-for-you" rel="noopener noreferrer"&gt;expo.dev/blog&lt;/a&gt; by Charlie Cheever.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Expo raised some money. &lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/Xjtq5X-sOew"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;We raised a $45MM Series B from &lt;a href="https://georgian.io/" rel="noopener noreferrer"&gt;Georgian&lt;/a&gt; and some other great partners. We picked them because they have a track record of partnering with and helping other great developer infrastructure companies like Replit and Render and Dagster. And they just get it. &lt;/p&gt;

&lt;p&gt;The first thing they showed us when we initially met them was a bunch of stuff people on their team had built with Expo. It's going to be great to work with them.&lt;/p&gt;

&lt;p&gt;We have been profitable for a while so we didn't need to raise money to keep going. But we're seeing so many things that people want from Expo right now and we want to build them, ASAP, and this round will let us do more. &lt;/p&gt;

&lt;p&gt;There are a million things we can do to make the apps you bring to life with Expo closer to being exactly what you're dreaming. And there is a handful of things we want to build for you to make that process easier and faster. This funding makes it possible for us to hire engineers to try and build everything. &lt;/p&gt;

&lt;p&gt;All the new ways that people are using Expo with AI have also made our todo list way, way longer. We can see a path to letting everyone in the world who has grit and vision make application software (not just the people who are software developers today). &lt;/p&gt;

&lt;p&gt;What a fun and crazy time to be helping people make software. There's so much to do. This round of funding lets us really go after it.&lt;/p&gt;

&lt;p&gt;If you’re passionate about our mission to make app development better and easier please &lt;a href="https://expo.dev/careers" rel="noopener noreferrer"&gt;take a look at our open roles&lt;/a&gt; and reach out if you see something that fits your skills. &lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>ai</category>
    </item>
    <item>
      <title>The real cost of React Native animations: benchmarking every approach</title>
      <dc:creator>Dan</dc:creator>
      <pubDate>Fri, 26 Jun 2026 17:07:15 +0000</pubDate>
      <link>https://dev.to/expo/the-real-cost-of-react-native-animations-benchmarking-every-approach-3bej</link>
      <guid>https://dev.to/expo/the-real-cost-of-react-native-animations-benchmarking-every-approach-3bej</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://expo.dev/blog/the-real-cost-of-react-native-animations-benchmarking-every-approach" rel="noopener noreferrer"&gt;expo.dev/blog&lt;/a&gt; by Janic Duplessis.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is a guest post from &lt;em&gt;[*Janic Duplessis&lt;/em&gt;](&lt;a href="https://www.linkedin.com/in/janic-duplessis-4aa83171/" rel="noopener noreferrer"&gt;https://www.linkedin.com/in/janic-duplessis-4aa83171/&lt;/a&gt;)&lt;/em&gt; - He is the head of consulting at App&amp;amp;Flow and a long-time React Native contributor. *&lt;br&gt;
…&lt;/p&gt;

&lt;p&gt;Picture a login screen with a slowly drifting background, the kind of subtle motion that makes a product feel polished rather than built. Simple enough. We implemented it with Reanimated and shipped it. But every now and then, you could catch a frame drop. Just enough to feel slightly off, the kind of thing that bugs you once you’ve seen it.&lt;/p&gt;

&lt;p&gt;The root cause is that Reanimated runs on the UI thread every frame. When the app does significant work during that frame (a re-render kicks in, a list scrolls, an input updates), the animation budget shrinks, and that slow background translate becomes a slow background stutter.&lt;/p&gt;

&lt;p&gt;At &lt;a href="https://appandflow.com/" rel="noopener noreferrer"&gt;App &amp;amp; Flow&lt;/a&gt;, we build React Native apps and tools for product teams that care about getting the details right. Fluid, native-feeling UIs are a big part of that. So instead of working around the problem, we went looking for a better approach.&lt;/p&gt;

&lt;p&gt;Core Animation on iOS hands animations off to the OS render server and never touches your thread again. Once you give it a &lt;code&gt;CAAnimation&lt;/code&gt;, the system drives it and your app is out of the loop entirely. We wanted that in React Native. That’s how &lt;a href="https://github.com/AppAndFlow/react-native-ease" rel="noopener noreferrer"&gt;react-native-ease&lt;/a&gt; came about, a declarative animation library that drives everything through platform APIs (Core Animation on iOS, ObjectAnimator on Android) with no JS loop, no worklets, and no shadow tree commits per frame.&lt;/p&gt;

&lt;p&gt;But building it raised a question we wanted to answer honestly: &lt;strong&gt;how much does the choice of animation library actually matter?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So we measured it. Across four approaches, two platforms, and both high-end and mid-range devices, we tracked per-frame UI thread overhead. This post shares what we found, and tries to answer the questions that actually matter: how large is the frame penalty? In what kinds of apps does it matter? And what should you prioritize when choosing an animation library?&lt;/p&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 The frame drops on the Reanimated side are simulated. We injected artificial UI thread pressure to reproduce what happens in a busy app. Real-world jank depends on your workload, device, and how much else is happening on the UI thread at the same time.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  The four React Native animation libraries tested
&lt;/h2&gt;

&lt;p&gt;All benchmarks were run in April 2026 with Expo SDK 55, React Native 0.83, Reanimated 4.3.0, and react-native-ease 0.7.0.&lt;/p&gt;

&lt;p&gt;We compared four animation approaches:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ease:&lt;/strong&gt; react-native-ease, using platform APIs directly. Animations are described as props on the JS side and driven natively with no per-frame JS involvement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reanimated (Shared Values):&lt;/strong&gt; the standard worklet-based approach. Values are driven on the UI thread via a C++ worklet runtime, but each frame still updates props through the shadow tree.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reanimated (CSS Animations):&lt;/strong&gt; Reanimated’s newer CSS animation API. Declarative like Ease, but still backed by Reanimated’s animation engine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RN Animated:&lt;/strong&gt; React Native’s built-in &lt;code&gt;Animated&lt;/code&gt; API with &lt;code&gt;useNativeDriver: true&lt;/code&gt;. Values are driven natively, but the implementation varies by platform.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We also tested Reanimated with its static feature flags enabled, specifically &lt;code&gt;ANDROID_SYNCHRONOUSLY_UPDATE_UI_PROPS&lt;/code&gt; and &lt;code&gt;IOS_SYNCHRONOUSLY_UPDATE_UI_PROPS&lt;/code&gt;, which let Reanimated skip the shadow tree commit when only non-layout props (like &lt;code&gt;transform&lt;/code&gt; and &lt;code&gt;opacity&lt;/code&gt;) are updated. A meaningful optimization worth calling out separately.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; RN 0.85 introduced a new Shared Animation Backend that will eventually make the feature flags unnecessary. Reanimated’s integration is in progress but not yet released.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  How the benchmark measures per-frame overhead
&lt;/h2&gt;

&lt;p&gt;We built a benchmark screen into the example app that animates N views simultaneously in a loop (translateX, 2s, linear, repeating). We used a custom Expo native module to measure per-frame overhead:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;iOS:&lt;/strong&gt; We swizzle &lt;code&gt;CADisplayLink&lt;/code&gt;’s factory method to intercept all display link callbacks registered by any framework, then measure wall-clock time per callback aggregated by frame timestamp.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Android:&lt;/strong&gt; We use &lt;code&gt;Window.OnFrameMetricsAvailableListener&lt;/code&gt;, which reports &lt;code&gt;ANIMATION_DURATION&lt;/code&gt;, &lt;code&gt;LAYOUT_MEASURE_DURATION&lt;/code&gt;, and &lt;code&gt;DRAW_DURATION&lt;/code&gt; from the platform’s frame metrics system.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We ran 5-second collection windows per test and multiple configurations to show both worst-case and best-case Reanimated performance.&lt;/p&gt;
&lt;h2&gt;
  
  
  Benchmark results: per-frame UI thread cost on iOS and Android
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Android (Moto G8 Plus)
&lt;/h3&gt;

&lt;p&gt;Android is the most apples-to-apples comparison between libraries. Every approach runs on the UI thread, so what you’re seeing is a direct measure of how much work each animation engine adds per frame. No tricks, no shortcuts.&lt;/p&gt;
&lt;h4&gt;
  
  
  How much does build configuration matter? (50 views, avg ms)
&lt;/h4&gt;

&lt;p&gt;The single biggest variable for Reanimated performance isn’t which animation API you pick. It’s whether you’re testing in a debug or release build.&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz26tt53tvq9yq32ft7jt.png" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz26tt53tvq9yq32ft7jt.png" alt="comparison chart" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;



&lt;p&gt;The red line is the 16.67ms frame budget at 60fps. In debug mode, Reanimated SV and CSS both blow past it at just 50 views, actively dropping frames. The same animation in a release build comes in at 11ms. &lt;strong&gt;Debug builds lie. If you notice animation jank during development, reproduce it in a release build before panicking.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The feature flags add another 11–19% on top by bypassing the shadow tree commit for non-layout props. They can cause visual bugs in some apps so they’re opt-in, but worth testing if you’re seeing overhead.&lt;/p&gt;
&lt;h4&gt;
  
  
  How does overhead scale with view count? (Release, all FF, avg ms)
&lt;/h4&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe4i5cvymon51t3s47t27.png" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe4i5cvymon51t3s47t27.png" alt="How does overhead scale with view count?" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 500 views is a stress test, not a realistic target. If you're animating 500 things at once, the animation library might not be your biggest problem.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;At 10–100 views, all approaches stay under the frame budget on average, though Reanimated and RN Animated are within 5ms of it at 100 views, leaving little headroom for the rest of your frame work. At 500 views, only Ease stays under budget. Reanimated SV hits 36ms, more than twice the frame budget; and this is the &lt;em&gt;optimized&lt;/em&gt; configuration.&lt;/p&gt;
&lt;h3&gt;
  
  
  iOS (iPhone 15 Pro)
&lt;/h3&gt;

&lt;p&gt;iOS is where the architectural difference becomes impossible to ignore. On Android, all libraries share the UI thread, so the comparison is fair. On iOS, Ease gets to cheat (in the best way). Core Animation runs in a separate OS render server process, completely outside your app. Once Ease registers a &lt;code&gt;CAAnimation&lt;/code&gt;, the system takes over and your thread is free to do other work. That’s why Ease shows ~0.01ms across the board: there is genuinely nothing happening on the UI thread per frame. The tradeoff is that Core Animation animations can’t be read or interrupted from JS mid-flight, which is exactly why gesture-driven animations still belong to Reanimated.&lt;/p&gt;
&lt;h4&gt;
  
  
  Display link callback time per frame, ms (release build)
&lt;/h4&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5eovql0hnhibjk9ga88f.png" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5eovql0hnhibjk9ga88f.png" alt="Display link callback time per frame, ms (release build)" width="799" height="367"&gt;&lt;/a&gt;&lt;/p&gt;



&lt;p&gt;The absolute numbers are lower than Android because the measurement captures only UI thread callback time. But the point stands: on iOS, Ease adds no UI thread cost regardless of how many views are animating, while every other approach keeps doing work every frame.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why React Native animation libraries differ in per-frame cost
&lt;/h2&gt;
&lt;h3&gt;
  
  
  The shadow tree tax
&lt;/h3&gt;

&lt;p&gt;Every frame, Reanimated’s worklet computes new values and commits a prop update through the shadow tree. That commit runs Yoga layout, prop diffing, and view mutations. When you’re animating &lt;code&gt;transform&lt;/code&gt; or &lt;code&gt;opacity&lt;/code&gt; (properties with zero effect on layout) every bit of that work is wasted. You’re paying the full price of a layout pass to nudge a blob three pixels to the left. Yoga doesn’t need to know.&lt;/p&gt;

&lt;p&gt;The feature flags (&lt;code&gt;ANDROID/IOS_SYNCHRONOUSLY_UPDATE_UI_PROPS&lt;/code&gt;) short-circuit this by pushing visual prop updates directly to the UI layer, skipping the layout pass entirely. On the Moto G8 Plus at 50 views they cut Reanimated SV from 11.87ms to 10.57ms (-11%) and CSS from 11.20ms to 9.06ms (-19%). They’re opt-in because they can cause visual bugs in some apps, but if you’re chasing overhead they’re the first thing to try.&lt;/p&gt;
&lt;h3&gt;
  
  
  RN Animated
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;RN Animated&lt;/code&gt; with &lt;code&gt;useNativeDriver: true&lt;/code&gt; skips the JS thread per frame too, but drives animations through a separate native animation module that carries bookkeeping overhead per animated node. It holds up fine at low-to-mid view counts, but scales worse than Reanimated CSS as the number of animated views climbs. This is partly because it lacks the shadow tree optimizations the feature flags enable.&lt;/p&gt;
&lt;h2&gt;
  
  
  When your animation library choice matters in production apps
&lt;/h2&gt;

&lt;p&gt;It matters most for long-running or slow animations: skeleton loaders, drifting backgrounds, ambient UI effects. A single dropped frame in a 5-second animation is noticeable, and other work (data fetching, re-renders, user interaction) is almost always happening at the same time. It also matters for anything in a list, where you can easily have hundreds of animated items on screen at once. On low-end devices, small per-frame overhead compounds fast, and your users notice before you do.&lt;/p&gt;

&lt;p&gt;For short one-shot transitions (a button press, a toast, a modal) the overhead is negligible and any library works fine.&lt;/p&gt;

&lt;p&gt;Worth noting: Ease only covers this specific use case. Gesture-driven animations (scroll-linked, drag, swipe) and anything that changes layout properties (width, height, padding) still need Reanimated or RN Animated. Ease is purpose-built for declarative, trigger-based animations on visual properties.&lt;/p&gt;
&lt;h4&gt;
  
  
  React Native 0.85 and the Shared Animation Backend
&lt;/h4&gt;

&lt;p&gt;React Native 0.85 ships an experimental Shared Animation Backend, a unified animation engine built directly into the renderer by Meta and Software Mansion. Once Reanimated’s integration ships, &lt;code&gt;SYNCHRONOUSLY_UPDATE_UI_PROPS&lt;/code&gt; becomes unnecessary because shadow tree bypass will be the default path, and the gap between “default Reanimated” and “optimized Reanimated” effectively goes away.&lt;/p&gt;

&lt;p&gt;The architectural difference remains, though. Ease has no per-frame animation engine at all. Even with a faster backend, Reanimated still computes values and pushes prop updates every frame. That overhead doesn’t disappear; it just gets smaller. We’ll update the benchmarks once the integration ships.&lt;/p&gt;
&lt;h4&gt;
  
  
  Running the React Native animation benchmark yourself
&lt;/h4&gt;

&lt;p&gt;The benchmark is built into the example app. Clone the repo, run &lt;code&gt;yarn example ios&lt;/code&gt; or &lt;code&gt;yarn example android&lt;/code&gt;, and tap &lt;strong&gt;Benchmark&lt;/strong&gt; from the demo screen. Source is in &lt;code&gt;example/src/demos/BenchmarkDemo.tsx&lt;/code&gt; and the native module is in &lt;code&gt;example/modules/frame-metrics/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One note: use release builds. Debug mode inflates Reanimated’s numbers significantly. So if your numbers look alarming, that’s likely why.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;yarn example ios &lt;span class="nt"&gt;--configuration&lt;/span&gt; Release
yarn example android &lt;span class="nt"&gt;--variant&lt;/span&gt; release
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/AppAndFlow/react-native-ease" rel="noopener noreferrer"&gt;*react-native-ease&lt;/a&gt; is built by &lt;a href="https://appandflow.com/" rel="noopener noreferrer"&gt;App &amp;amp; Flow&lt;/a&gt;, a Montreal-based React Native engineering studio recommended by Expo.*&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>animations</category>
      <category>reanimated</category>
    </item>
    <item>
      <title>5 proven strategies to increase adoption of your B2B mobile app</title>
      <dc:creator>Dan</dc:creator>
      <pubDate>Thu, 25 Jun 2026 16:19:04 +0000</pubDate>
      <link>https://dev.to/expo/5-proven-strategies-to-increase-adoption-of-your-b2b-mobile-app-57h7</link>
      <guid>https://dev.to/expo/5-proven-strategies-to-increase-adoption-of-your-b2b-mobile-app-57h7</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://expo.dev/blog/5-proven-strategies-to-increase-adoption-of-your-b2b-mobile-app" rel="noopener noreferrer"&gt;expo.dev/blog&lt;/a&gt; by Dan Kelly.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;B2B mobile apps face a different challenge than consumer apps. You're not competing for attention on social media. You're competing for budget approval, security reviews, and change management bandwidth.&lt;/p&gt;

&lt;p&gt;The decision to adopt your app isn't made by one person scrolling through the App Store at 11pm. It's made by a team evaluating ROI, integration complexity, and whether your app will actually get used after the procurement process ends.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.businessofapps.com/ads/cpi/research/cost-per-install/" rel="noopener noreferrer"&gt;Customer acquisition costs have climbed 60% in five years&lt;/a&gt;, but throwing money at ads won't solve the B2B adoption problem. Enterprise buyers don't install apps because they saw a Facebook ad. They install apps because they trust your business, understand the ROI of your app, and believe their team will actually use it.&lt;/p&gt;

&lt;p&gt;Here's what actually works for driving adoption of B2B mobile apps in 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  Focus on onboarding that delivers the "Aha!" moment fast
&lt;/h2&gt;

&lt;p&gt;Your app has 60 seconds to prove its value. Maybe less.&lt;/p&gt;

&lt;p&gt;Enterprise users are busy. They're evaluating your app between meetings, during a commute, or while waiting for their coffee. If they don't immediately understand what problem you solve and how to solve it, they'll uninstall and tell procurement it "wasn't a good fit."&lt;/p&gt;

&lt;h3&gt;
  
  
  Show the value immediately
&lt;/h3&gt;

&lt;p&gt;Don't start with account creation. Don't ask for 12 fields of company information. Don't require them to "sync data" before they can do anything.&lt;/p&gt;

&lt;p&gt;Show them what your app does first. Let them experience the core value before asking for commitment.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://play.google.com/store/apps/details?id=org.me.mobiexpensifyg&amp;amp;hl" rel="noopener noreferrer"&gt;Expensify&lt;/a&gt; nails this. Take a photo of a receipt. The app instantly extracts the vendor, amount, date, and category. Within 15 seconds of opening the app for the first time, you've seen it work. That's your "Aha!" moment. Everything else (creating an account, connecting to your company's expense system) happens after you've already experienced the value.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Figma's mobile app&lt;/strong&gt; lets you view and comment on designs immediately. No account required for viewing shared links. By the time you're ready to create an account, you've already collaborated on three designs and understand exactly why your team needs this.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Guide users to one meaningful action
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://www.appcues.com/blog/mobile-onboarding-best-practices" rel="noopener noreferrer"&gt;The best mobile onboarding focuses on completing one core action&lt;/a&gt;, not explaining every feature.&lt;/p&gt;

&lt;p&gt;For a project management app, that's creating their first task. For a sales tool, that's logging their first activity. For an analytics app, that's seeing their first dashboard (&lt;em&gt;dashboards = dollars&lt;/em&gt;, after all).&lt;/p&gt;

&lt;p&gt;Everything else can wait. Get them to that moment where they think "oh, this actually helps me" and the rest of onboarding becomes easier.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test with real users before you launch
&lt;/h3&gt;

&lt;p&gt;Don't wait until your app is "perfect" to get feedback. Use TestFlight (iOS) and internal testing tracks (Android) to get your app in front of real users early (&lt;a href="https://launch.expo.dev/" rel="noopener noreferrer"&gt;Expo Launch&lt;/a&gt; is the fastest way to get to TestFlight).&lt;/p&gt;

&lt;p&gt;You can have up to 10,000 external testers on TestFlight. That's more than enough to validate your onboarding, find confusing flows, and identify bugs before they hit the App Store.&lt;/p&gt;

&lt;p&gt;Here's the workflow that works:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Week 1-2 of development&lt;/strong&gt;: Ship to TestFlight with just the core feature&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recruit 20-50 testers&lt;/strong&gt; from your target industry (not friends and family)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch them use it&lt;/strong&gt;: Schedule video calls where they share their screen&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Count completions&lt;/strong&gt;: If fewer than 80% complete the core action in 5 minutes, iterate&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repeat weekly&lt;/strong&gt;: Each TestFlight build should be better than the last&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;More teams should be doing this. TestFlight exists precisely for this purpose, and the feedback you get is worth 100x more than internal testing alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build case studies and ROI calculators into your marketing
&lt;/h2&gt;

&lt;p&gt;B2B buyers need to justify the purchase. They're spending company money, not their own. That means they need evidence this will work.&lt;/p&gt;

&lt;p&gt;Your job is to help your users sell your product to their managers. Give them the nutrients they need to make the business case internally.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create case studies that show real results
&lt;/h3&gt;

&lt;p&gt;"Acme Corp increased productivity by 30%" is better than "our app is great for productivity." But the best case studies go deeper.&lt;/p&gt;

&lt;p&gt;They explain the problem Acme faced, the specific features they used, and the measurable outcome. They include a real person's name and title. They provide enough detail that a prospect can think "that sounds like us."&lt;/p&gt;

&lt;p&gt;Your case studies should answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What problem were they trying to solve?&lt;/li&gt;
&lt;li&gt;Why did they choose your app over alternatives?&lt;/li&gt;
&lt;li&gt;How did they roll it out to their team?&lt;/li&gt;
&lt;li&gt;What specific metrics improved and by how much?&lt;/li&gt;
&lt;li&gt;What would they tell someone else your app?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Let your customer write it.&lt;/strong&gt; The best case studies are written by the customer themselves, with light editing from your team. This creates authenticity that a ghostwritten case study never achieves. Prospects can tell the difference between "&lt;em&gt;here's what the vendor says we said&lt;/em&gt;" and "&lt;em&gt;here's what we actually experienced&lt;/em&gt;."&lt;/p&gt;

&lt;p&gt;Offer to help with structure and editing, but let their voice come through. Awkward phrasing and industry-specific jargon actually make case studies more credible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Build an ROI calculator
&lt;/h3&gt;

&lt;p&gt;Let prospects input their own numbers and see potential value. "If your team of 50 people saves 30 minutes per week using our app, that's $156,000 in annual productivity gains."&lt;/p&gt;

&lt;p&gt;The calculator doesn't need to be sophisticated. It needs to be honest and help them make the business case internally.&lt;/p&gt;

&lt;p&gt;Put it on your website. Link to it from your app store listing. Make it easy to screenshot and include in a procurement document.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use customer quotes everywhere
&lt;/h3&gt;

&lt;p&gt;Real companies using your app is social proof. Use their logos. Quote their teams. Link to their case studies.&lt;/p&gt;

&lt;p&gt;People are tribal. We trust people who are like us more than we trust companies selling to us. When a prospect sees that companies like theirs are already using your app successfully, it reduces perceived risk. The question shifts from "will this work?" to "how do we implement this?"&lt;/p&gt;

&lt;p&gt;This is why "Trusted by 10,000+ businesses" is less powerful than "Trusted by teams at [Logo] [Logo] [Logo]." Seeing specific companies in your industry creates tribal trust that generic numbers never will.&lt;/p&gt;

&lt;h2&gt;
  
  
  Target LinkedIn and industry-specific communities (not just Reddit)
&lt;/h2&gt;

&lt;p&gt;B2B buyers aren't only browsing r/AppHookup looking for enterprise software, but decisions do get influenced on Reddit. They're also on LinkedIn, in industry Slack channels, and on specialized forums for their profession.&lt;/p&gt;

&lt;h3&gt;
  
  
  LinkedIn is where B2B decisions happen
&lt;/h3&gt;

&lt;p&gt;Developers tend to roll their eyes when I talk about Linkedin. I get it. But your target users are on LinkedIn every week. They're reading industry news, following thought leaders, and looking for solutions to their problems.&lt;/p&gt;

&lt;p&gt;Post content that addresses their specific challenges. "5 ways operations managers are using mobile apps to reduce manual data entry" will perform better than "check out our cool app."&lt;/p&gt;

&lt;p&gt;Use LinkedIn's targeting to reach decision makers. You can target by job title, company size, and industry. A well-targeted post to 1,000 operations managers at mid-sized logistics companies will drive more qualified installs than a viral tweet (we see this often at Expo).&lt;/p&gt;

&lt;h3&gt;
  
  
  Find where your users already gather
&lt;/h3&gt;

&lt;p&gt;Every industry has its digital watercoolers. For developers, it's GitHub, Twitter, and Reddit (it used to also include Stack Overflow and Hacknews). For marketers, it's specialized Slack communities and forums. For healthcare professionals, it's HIPAA-compliant messaging platforms and professional associations.&lt;/p&gt;

&lt;p&gt;Figure out where your target users already spend time professionally. Tools like &lt;a href="https://sparktoro.com/" rel="noopener noreferrer"&gt;&lt;strong&gt;SparkToro&lt;/strong&gt;&lt;/a&gt; can help you discover where your audience actually hangs out online, what podcasts they listen to, and what websites they visit. Stop guessing and start using data to find your people.&lt;/p&gt;

&lt;p&gt;Once you know where they are, show up there with genuinely helpful content. Answer questions. Share insights. Demonstrate expertise. Mention your app when it's relevant, but don't make that the primary goal. Build trust first.&lt;/p&gt;

&lt;h3&gt;
  
  
  Write for industry publications
&lt;/h3&gt;

&lt;p&gt;Getting featured in an industry publication carries more weight than any ad you could run.&lt;/p&gt;

&lt;p&gt;Pitch editors with genuinely useful content. "How mobile apps are changing field service management" is interesting. "Download our app" is not.&lt;/p&gt;

&lt;p&gt;The article should educate first and mention your app second. The goal is to position your company as an authority in the space. When readers need a solution later, they'll remember you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Offer pilot programs and soft launches with key accounts
&lt;/h2&gt;

&lt;p&gt;Enterprise sales works differently than consumer sales. You can't expect companies to commit to a full rollout without proof it will work for their team.&lt;/p&gt;

&lt;h3&gt;
  
  
  Start with a pilot program
&lt;/h3&gt;

&lt;p&gt;Offer a free or heavily discounted pilot to 10-20 users at a target company. Give them 30-60 days to test the app with real work.&lt;/p&gt;

&lt;p&gt;The pilot should include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dedicated onboarding support (not just a welcome email)&lt;/li&gt;
&lt;li&gt;Weekly check-ins to address issues and answer questions&lt;/li&gt;
&lt;li&gt;Clear success metrics you're tracking together&lt;/li&gt;
&lt;li&gt;An easy path to expand to the full organization&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most pilots either prove the value quickly (leading to expansion) or reveal issues you need to fix (leading to a better product).&lt;/p&gt;

&lt;h3&gt;
  
  
  Make expansion frictionless
&lt;/h3&gt;

&lt;p&gt;If the pilot succeeds, make it easy to expand. Have pricing ready. Have the contract ready. Have the rollout plan ready.&lt;/p&gt;

&lt;p&gt;The momentum from a successful pilot can disappear quickly if procurement takes 6 weeks to process the expansion. Strike while the team is excited and seeing results.&lt;/p&gt;

&lt;h3&gt;
  
  
  Document everything for the next pilot
&lt;/h3&gt;

&lt;p&gt;What worked in this pilot? What questions did users ask repeatedly? What friction points slowed adoption?&lt;/p&gt;

&lt;p&gt;Use these insights to improve the next pilot. Each one should be smoother and more successful than the last.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optimize for search intent with long-tail professional keywords
&lt;/h2&gt;

&lt;p&gt;Enterprise buyers search differently than consumers. They search for solutions to specific work problems, not generic app categories.&lt;/p&gt;

&lt;h3&gt;
  
  
  Target problem-based search terms
&lt;/h3&gt;

&lt;p&gt;"App for tracking construction site inspections" is how someone actually searches. "Construction app" is too broad.&lt;/p&gt;

&lt;p&gt;"Mobile CRM for pharmaceutical sales reps" is specific. "CRM app" could mean anything.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.dogtownmedia.com/aso-2-0-advanced-app-store-optimization-strategies-for-2025/" rel="noopener noreferrer"&gt;Long-tail keywords have lower competition&lt;/a&gt; and higher conversion because the searcher knows exactly what they need.&lt;/p&gt;

&lt;h3&gt;
  
  
  Write your app store description for search
&lt;/h3&gt;

&lt;p&gt;Your app store description should include the specific problems you solve and the industries you serve.&lt;/p&gt;

&lt;p&gt;"Field service management app for HVAC contractors to schedule jobs, track inventory, and generate invoices" tells Google and the App Store exactly what you do. It also tells the right prospects they're in the right place.&lt;/p&gt;

&lt;h3&gt;
  
  
  Optimize your website for these searches too
&lt;/h3&gt;

&lt;p&gt;When someone searches "mobile app for restaurant inventory management," your website should rank, not just your app store listing.&lt;/p&gt;

&lt;p&gt;Create dedicated landing pages for your target use cases. Include the search terms naturally in the content. Link to your app store listings.&lt;/p&gt;

&lt;p&gt;This expands your discoverability beyond the app stores and gives prospects more information to evaluate your solution.&lt;/p&gt;

&lt;h2&gt;
  
  
  The enterprise reality you're working with
&lt;/h2&gt;

&lt;p&gt;B2B app adoption is slow. It requires multiple touchpoints. It involves multiple decision makers. &lt;a href="https://indieappsanta.com/2025/11/21/10349/" rel="noopener noreferrer"&gt;The median app makes under $50 per month after a year&lt;/a&gt;, and B2B apps follow similar patterns without focused effort.&lt;/p&gt;

&lt;p&gt;But B2B apps have advantages consumer apps don't:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Higher lifetime value per user&lt;/li&gt;
&lt;li&gt;More predictable revenue (contracts and subscriptions)&lt;/li&gt;
&lt;li&gt;Easier to target decision makers&lt;/li&gt;
&lt;li&gt;Word of mouth within industries is powerful&lt;/li&gt;
&lt;li&gt;Successful deployments lead to referrals&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key is focusing on ROI, trust, and demonstrable value. B2B buyers will pay for solutions that work. They just need proof before they commit.&lt;/p&gt;

&lt;p&gt;Start with onboarding because it determines whether your pilot succeeds. Build case studies because they enable prospects to justify the purchase internally. Target the right channels because enterprise buyers aren't where consumer buyers are. Offer pilots because companies need to test before they buy. Optimize for search because that's how problems turn into solutions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it now&lt;/strong&gt;: If you have an existing B2B app, audit your first-time user experience. Open the app as if you've never seen it before. Can you complete one valuable action in under 3 minutes without help? If not, that's your starting point.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>marketingmobileapps</category>
    </item>
    <item>
      <title>From React web to Native in one week</title>
      <dc:creator>Dan</dc:creator>
      <pubDate>Thu, 25 Jun 2026 16:18:10 +0000</pubDate>
      <link>https://dev.to/expo/from-react-web-to-native-in-one-week-3kd9</link>
      <guid>https://dev.to/expo/from-react-web-to-native-in-one-week-3kd9</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://expo.dev/blog/from-react-web-to-native-in-one-week" rel="noopener noreferrer"&gt;expo.dev/blog&lt;/a&gt;&lt;/em&gt;&lt;br&gt;
&lt;em&gt;By Ishika Chakraborty&lt;/em&gt;&lt;/p&gt;



&lt;p&gt;I am a React dev who writes Next.js, Tailwind, component libraries, and design systems every day - a world that feels completely familiar. Native does not, and for years that gap felt enormous.&lt;/p&gt;

&lt;p&gt;Every time I thought about building an iOS app, I pictured Xcode, Swift, provisioning profiles, and two weeks disappearing before I even got to “Hello World.”&lt;/p&gt;

&lt;p&gt;Until I built a real native iOS app running on my iPhone in a week - setup, native debugging, device testing, and the build, end to end.&lt;/p&gt;

&lt;p&gt;Not a tutorial app, not a counter, not another todo list. I built &lt;strong&gt;Sun Buddy&lt;/strong&gt; - a native iOS app with a cute, animated sun mascot, real-time UV tracking from my location, haptic feedback, daily local notifications, persistent progress, and a sleeping nighttime state when the sun is down.&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxhbw25gs9xxo687a9a7d.png" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxhbw25gs9xxo687a9a7d.png" alt="Hero-image" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I built it with &lt;a href="https://expo.dev/" rel="noopener noreferrer"&gt;Expo&lt;/a&gt;, &lt;a href="https://claude.ai/" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt;, and &lt;a href="https://expo.dev/services" rel="noopener noreferrer"&gt;Expo Skills&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Setup at a glance:&lt;/strong&gt; Expo SDK (latest), Claude Code (latest), Expo Skills installed via /plugin marketplace add expo/skills then &lt;code&gt;/plugin install expo&lt;/code&gt;, latest macOS and Xcode, developed in the iOS Simulator and on a physical iPhone, final build via EAS.&lt;/p&gt;

&lt;p&gt;This is my end-to-end journey: what transferred from React, what did not, where Expo made native feel approachable, and where platform reality still showed up.&lt;/p&gt;

&lt;p&gt;The short version: I did not have to become an app developer to build this app. I had to learn the native edges around the React skills I already had.&lt;/p&gt;
&lt;h2&gt;
  
  
  The idea: Sun Buddy
&lt;/h2&gt;

&lt;p&gt;The app needed to be real enough to test native development properly. I did not want to build something that could have just been a web page in disguise - I wanted to touch actual device capabilities: location, haptics, notifications, native animation, local storage, and a real build running on my iPhone.&lt;/p&gt;

&lt;p&gt;So I landed on a sunlight exposure tracker. Sun Buddy is a daily habit app that helps you hit your recommended outdoor sunlight goal - tracked by UV index from your real location.&lt;/p&gt;

&lt;p&gt;The idea was simple: you are supposed to get some outdoor sunlight every day, and most people do not. I wanted a little mascot named Sunny who reacts emotionally to your progress.&lt;/p&gt;

&lt;p&gt;Sunny has a few states:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;drowsy at 0%&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;curious when you have started&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;happy when you are halfway there&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;absolutely radiant when you hit your goal&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;asleep at night when the sun is down&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A little weird. A little cute. The kind of app I would actually keep on my phone.&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%2Fcdn.sanity.io%2Fimages%2F9r24npb8%2Fproduction%2F45ea1e708079496812984e0188dc997f156e9967-4320x1420.png" 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%2Fcdn.sanity.io%2Fimages%2F9r24npb8%2Fproduction%2F45ea1e708079496812984e0188dc997f156e9967-4320x1420.png" alt="mascot-stages" width="800" height="263"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Sun Buddy was deliberately small, which was the point. I wanted a contained app that still forced me through real native surfaces: location permissions, device APIs, haptics, local notifications, persistent state, animation, app config, and an actual device build. &lt;/p&gt;

&lt;p&gt;Those patterns are not specific to a sun mascot; they are the same patterns you hit in fitness apps, habit trackers, field tools, travel apps, internal dashboards, delivery workflows, or anything else that needs to feel at home on a phone.&lt;/p&gt;
&lt;h2&gt;
  
  
  The setup: Expo first, Claude Code second
&lt;/h2&gt;

&lt;p&gt;The most important part of this experience was not that I used AI. It was that I was able to build a native app using the React mental model I already had, and &lt;strong&gt;that is where **&lt;/strong&gt;&lt;a href="https://expo.dev/" rel="noopener noreferrer"&gt;Expo&lt;/a&gt;**** mattered**. &lt;br&gt;
Expo gave me the app structure, routing, device APIs, build path, and the bridge from "I know React" to "this is running on my phone."&lt;/p&gt;

&lt;p&gt;Claude Code gave me the pairing loop, and &lt;a href="https://expo.dev/services" rel="noopener noreferrer"&gt;Expo Skills&lt;/a&gt; made that pairing loop much more useful.&lt;/p&gt;

&lt;p&gt;That distinction matters. Claude Code on its own can help you write code, but native development has a lot of details where generic advice is not enough: permissions, config plugins, &lt;a href="https://docs.expo.dev/build/introduction/" rel="noopener noreferrer"&gt;EAS Build&lt;/a&gt; profiles, App Store constraints, native module support. These are exactly the places where stale or vague guidance wastes time.&lt;/p&gt;

&lt;p&gt;**A concrete example: **early on, Sunny's animations broke as soon as I added Reanimated. Generic AI suggestions sent me chasing version mismatches and bad imports for half an hour. The &lt;code&gt;expo-dev-client&lt;/code&gt; Skill knew immediately to check that the Reanimated Babel plugin was registered in &lt;code&gt;babel.config.js&lt;/code&gt; and that the Metro cache had been cleared. Two lines of config, one &lt;code&gt;--clear&lt;/code&gt; flag, animation working. That is the kind of native-specific context generic advice routinely misses. &lt;/p&gt;

&lt;p&gt;Expo Skills brought Expo-specific context into the workflow.&lt;/p&gt;

&lt;p&gt;I noticed the difference most when I asked native-specific questions. Generic AI help could tell me how to write a component, but that was not the hard part. The harder questions were things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Should this be a normal Expo Go flow or a &lt;a href="https://docs.expo.dev/develop/development-builds/introduction/" rel="noopener noreferrer"&gt;dev client&lt;/a&gt;?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Where does this permission need to be configured?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Is this a runtime library, a config plugin, or both?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;What is the right path to a real iPhone build?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;What changes when I move from simulator testing to TestFlight?&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is where Expo Skills mattered - they gave the pairing loop Expo-specific context instead of generic React Native guesses. I added the Skills inside Claude Code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;// expo-skills
/plugin marketplace add expo/skills
/plugin &lt;span class="nb"&gt;install &lt;/span&gt;expo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expo Skills are structured instruction files that work with Claude Code, Cursor, Codex, and other agents. I leaned on the expo plugin, which bundles three actively-maintained Expo Skills:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;**building-native-ui**&lt;/code&gt; - &lt;a href="https://docs.expo.dev/router/introduction/" rel="noopener noreferrer"&gt;Expo Router&lt;/a&gt; patterns, native UI structure, Apple HIG guidance&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;**expo-deployment**&lt;/code&gt; - EAS Build, TestFlight, App Store, credentials&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;**expo-dev-client**&lt;/code&gt; - &lt;a href="https://docs.expo.dev/develop/development-builds/introduction/" rel="noopener noreferrer"&gt;development builds&lt;/a&gt; and native module support&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That made Claude Code feel less like a generic coding assistant and more like a collaborator that understood the Expo ecosystem.&lt;/p&gt;

&lt;p&gt;**On the same note: **I started in Expo Go to get the React shell rendering on my iPhone within minutes. The moment I added Skia for Sunny and Reanimated for the animations, I moved to a development build, because both rely on native modules Expo Go does not ship with. The Skills made that transition explicit rather than something I had to discover by crashing the app.&lt;/p&gt;

&lt;h2&gt;
  
  
  The surprise: my React skills transferred almost immediately
&lt;/h2&gt;

&lt;p&gt;This was the biggest mental shift. The component code was just React - &lt;code&gt;useState&lt;/code&gt;, &lt;code&gt;useEffect&lt;/code&gt;, custom hooks, JSX, component composition, conditional rendering, fetching data, managing loading and error states. All of it came with me.&lt;/p&gt;

&lt;p&gt;The primitives changed: &lt;code&gt;View&lt;/code&gt; instead of &lt;code&gt;div&lt;/code&gt;, &lt;code&gt;Text&lt;/code&gt; instead of &lt;code&gt;p&lt;/code&gt;, &lt;code&gt;Pressable&lt;/code&gt; instead of &lt;code&gt;button&lt;/code&gt;. But the mental model did not feel foreign at all. I wrote &lt;code&gt;useUVIndex.ts&lt;/code&gt; and &lt;code&gt;useSunTracking.ts&lt;/code&gt; the same way I would write hooks for a web app: fetch data, manage state, return values to the component. The layout model was familiar too - it was flexbox.&lt;/p&gt;

&lt;p&gt;That does not mean everything from web transfers perfectly. But the core React instincts transferred much more than I expected.&lt;/p&gt;

&lt;p&gt;Here is how it felt in practice:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What I knew from web&lt;/th&gt;
&lt;th&gt;How it mapped to native&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;React components&lt;/td&gt;
&lt;td&gt;React Native components&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;useState, useEffect, custom hooks&lt;/td&gt;
&lt;td&gt;Same patterns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Next.js file-based routing&lt;/td&gt;
&lt;td&gt;Expo Router&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flexbox layout&lt;/td&gt;
&lt;td&gt;React Native flexbox&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Browser geolocation&lt;/td&gt;
&lt;td&gt;expo-location&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;localStorage&lt;/td&gt;
&lt;td&gt;AsyncStorage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CSS/Framer-style motion instincts&lt;/td&gt;
&lt;td&gt;Reanimated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deploying a web app&lt;/td&gt;
&lt;td&gt;EAS Build / TestFlight / App Store&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That last row deserves a footnote: EAS simplifies the path, but App Store and TestFlight still involve Apple-specific review, code signing, metadata, and account requirements that have no real web equivalent.&lt;/p&gt;

&lt;p&gt;That table explains why this felt approachable. I was not starting over, I was learning the edges of a new runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that did not transfer automatically
&lt;/h2&gt;

&lt;p&gt;The surrounding layer is where native feels different. Not the component model, not the React part, but the native edges&lt;/p&gt;

&lt;p&gt;Things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;permission prompts&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;device APIs&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;build-time configuration&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;native modules&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;simulator state&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;development builds&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;code signing&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;App Store constraints&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;testing on a real device&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is where I would have taken wrong turns without Expo and Expo Skills.&lt;/p&gt;

&lt;p&gt;For example, location on native is not &lt;code&gt;navigator.geolocation&lt;/code&gt;. With &lt;code&gt;expo-location&lt;/code&gt;, I needed to request foreground permissions first, check the result, handle denial gracefully, and only then get the device position.&lt;/p&gt;

&lt;p&gt;The shape of the logic was familiar, but the platform expectations were different - not hard code, but you do need to know the right sequence.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;requestForegroundPermissionsAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;granted&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Handle denial properly&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;location&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCurrentPositionAsync&lt;/span&gt;&lt;span class="p"&gt;({});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same was true with config. On web, I rarely think about something like app.json. In Expo, that file matters. Some libraries are not just JS imports - they also modify native project configuration. &lt;code&gt;expo-location&lt;/code&gt; needs an iOS usage description string so the system knows what to show in the permission dialog. &lt;code&gt;expo-notifications&lt;/code&gt; registers an icon and notification channel. &lt;code&gt;expo-router&lt;/code&gt; wires up the native navigation stack.&lt;/p&gt;

&lt;p&gt;My app needed plugin entries like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"plugins"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"expo-router"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"expo-location"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"expo-notifications"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"icon"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./assets/notification-icon.png"&lt;/span&gt;&lt;span class="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;This is exactly where Expo-specific context helped. Without it, I probably would have written perfectly reasonable React code and then wasted time debugging native behavior that had nothing to do with my components.&lt;/p&gt;

&lt;h2&gt;
  
  
  The native capabilities I added
&lt;/h2&gt;

&lt;p&gt;Once the app structure was in place, the fun part was adding things that actually feel native.&lt;/p&gt;

&lt;p&gt;I used &lt;a href="https://docs.expo.dev/versions/latest/sdk/location/" rel="noopener noreferrer"&gt;expo-location&lt;/a&gt; to get the phone's GPS coordinates, then passed those to the Open-Meteo API to get the current UV index - no API key, no backend, just the phone's location and a public weather API. That alone made the app feel different from a normal web project. It was not asking the user to type a city; it already knew where the device was.&lt;/p&gt;

&lt;p&gt;Then I added haptics. This was the smallest feature that made the biggest difference. I used &lt;a href="https://docs.expo.dev/versions/latest/sdk/haptics/" rel="noopener noreferrer"&gt;expo-haptics&lt;/a&gt; for two moments:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;light impact feedback when starting a sun session&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;success feedback when hitting 100%&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The code was tiny:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Haptics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;impactAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Haptics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ImpactFeedbackStyle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Light&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Haptics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;notificationAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;Haptics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NotificationFeedbackType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Success&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the effect was immediate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tap. Bounce. Haptic.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Suddenly it did not feel like a React screen pretending to be an app. It felt like something that belonged on the phone.&lt;/p&gt;

&lt;p&gt;Web developers do not usually think about haptics because there is no real web equivalent worth designing around. On native, two lines of code can change how the whole interaction feels.&lt;/p&gt;

&lt;p&gt;I used &lt;a href="https://docs.expo.dev/versions/latest/sdk/notifications/" rel="noopener noreferrer"&gt;expo-notifications&lt;/a&gt; for daily &lt;strong&gt;local&lt;/strong&gt; reminders with copy that felt intentionally a little strange - &lt;em&gt;scheduled on-device, not remote push. (Remote push would require APNs credentials and the paid Apple Developer Program, neither of which the app needed.)&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Sunny is waiting for you outside. Probably.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That felt right for the mascot's personality.&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi1n965irn0z7lxb4khv6.png" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi1n965irn0z7lxb4khv6.png" alt="notifications" width="800" height="643"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Sunny was built with &lt;a href="https://shopify.github.io/react-native-skia/" rel="noopener noreferrer"&gt;@shopify/react-native-skia&lt;/a&gt; and animated with &lt;a href="https://docs.swmansion.com/react-native-reanimated/" rel="noopener noreferrer"&gt;react-native-reanimated&lt;/a&gt; - bouncing, blinking, emotional state transitions. If you have used Framer Motion, the mental model is not alien, and the result was much smoother than I expected. Not "good enough for a demo" smooth but actually smooth.&lt;/p&gt;

&lt;p&gt;For daily progress, I used &lt;a href="https://react-native-async-storage.github.io/async-storage/" rel="noopener noreferrer"&gt;@react-native-async-storage/async-storage&lt;/a&gt;, which felt like async &lt;code&gt;localStorage&lt;/code&gt;. I used &lt;a href="https://docs.expo.dev/versions/latest/sdk/font/" rel="noopener noreferrer"&gt;expo-font&lt;/a&gt; for Nunito and &lt;a href="https://docs.expo.dev/guides/icons/" rel="noopener noreferrer"&gt;@expo/vector-icons&lt;/a&gt; for tab icons. None of this felt like learning mobile development from scratch. Instead, it felt like using React to reach parts of the device that the web usually cannot reach.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where things actually broke
&lt;/h2&gt;

&lt;p&gt;A lot worked faster than I expected, and some things broke.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Skia crash on web
&lt;/h3&gt;

&lt;p&gt;I wanted the app to run on both native and web while developing. The native version of Sunny used Skia, but on web, Skia depends on CanvasKit and WebAssembly. I called &lt;code&gt;Skia.Path.Make()&lt;/code&gt; before CanvasKit was ready, and the web build crashed immediately. I understood the bug once I saw it; what I did not know was the idiomatic React Native fix.&lt;/p&gt;

&lt;p&gt;The fix was platform file splitting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;components/
  SunBuddy.native.tsx   # full Skia implementation
  SunBuddy.web.tsx      # SVG fallback
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Metro automatically resolves &lt;code&gt;.native.tsx&lt;/code&gt; for iOS and &lt;code&gt;.web.tsx&lt;/code&gt; for web.&lt;/p&gt;

&lt;p&gt;That pattern was new to me. &lt;/p&gt;

&lt;p&gt;This convention is worth burning into muscle memory: instead of branching at runtime, Metro picks the right file at bundle time, which means platform-specific code never ships to the wrong target.&lt;/p&gt;

&lt;p&gt;On web, I would probably reach for a runtime check or a lazy load. In React Native, platform-specific files are a first-class convention. My debugging instincts transferred; my knowledge of native patterns did not always. Expo Skills helped fill that gap.&lt;/p&gt;

&lt;h3&gt;
  
  
  The stale Metro bundler
&lt;/h3&gt;

&lt;p&gt;After adding &lt;code&gt;react-native-reanimated&lt;/code&gt;, I opened the simulator and the app crashed. The error was not especially helpful, and I spent about fifteen minutes assuming I had misconfigured something. The actual fix was clearing Metro:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx expo start &lt;span class="nt"&gt;--clear&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The old bundle was still being served without the Reanimated Babel plugin properly registered.&lt;/p&gt;

&lt;p&gt;This is the canonical "try this first" move in Expo and React Native projects whenever you add a library with a Babel plugin and the simulator starts behaving oddly. Worth committing to muscle memory.&lt;/p&gt;

&lt;p&gt;This is not a problem I am used to from web development. Vite usually restarts, refreshes, and gets out of the way. Native development has more state outside your editor.&lt;/p&gt;

&lt;p&gt;I did not love that, but once I hit it and understood it, it became one of those "okay, now I know" moments.&lt;/p&gt;

&lt;h3&gt;
  
  
  The nighttime bug
&lt;/h3&gt;

&lt;p&gt;This was my favorite bug because it was not really a technical one. I was testing Sun Buddy at 9pm, dark outside, and tapped "start sun session." Sunny bounced. Progress increased. The app happily tracked sunlight I was absolutely not getting.&lt;/p&gt;

&lt;p&gt;The funny part is that the app was doing exactly what I had designed - a sunlight tracker that logged sessions and accumulated progress. However, the product logic I missed was obvious only once I used it: a sun tracker should probably check whether the sun is actually up. That was not an AI failure; it was my product thinking. &lt;/p&gt;

&lt;p&gt;&lt;em&gt;Tools execute on your intent, and your job is to have the right intent.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So I added sunrise and sunset calculation based on GPS coordinates. At night, Sunny goes into a sleeping state: half-lidded eyes, a little "zzz," drooped arms, and a disabled button that reads:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Sunny is sleeping. See you at sunrise.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2zkg005s65ecfz5kg3eg.png" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2zkg005s65ecfz5kg3eg.png" alt="sleeping-state" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That one bug made the app much better, and it reminded me that building with AI does not remove the need to actually use what you build.&lt;/p&gt;

&lt;h3&gt;
  
  
  Code signing
&lt;/h3&gt;

&lt;p&gt;The most "native is still native" moment was code signing. On the EAS build, the push notification capability blocked signing on my free Apple Developer account, because remote push is a paid Developer Program feature, which was not really an Expo issue, just Apple being Apple.&lt;/p&gt;

&lt;p&gt;I dropped the push notification capability via the Expo config rather than poking through Xcode. The Expo-native approach is to express the change in app.json and let Continuous Native Generation (CNG) regenerate the native project on the next build -either with a config plugin or expo-build-properties - so the change lives in source instead of in Xcode UI state. Local notifications still worked, because they do not need the push capability and no user-facing behavior changed.&lt;/p&gt;

&lt;p&gt;But I did have to understand what was happening.&lt;/p&gt;

&lt;p&gt;That was a useful reminder: Expo smooths a lot of the native path, but it does not erase platform reality. Apple still has certificates, capabilities, account types, and rules.&lt;/p&gt;

&lt;p&gt;Apple's gating happens at distinct stages, and it is worth knowing the staircase before you start: a free Apple ID lets you run a build on your own device (with seven-day signing limits). TestFlight and App Store distribution require the paid Apple Developer Program. Certain capabilities - push notifications, in-app purchase, sign in with Apple - require the paid program even for development.&lt;/p&gt;

&lt;p&gt;If you want TestFlight or App Store distribution, you need the &lt;a href="https://developer.apple.com/programs/" rel="noopener noreferrer"&gt;Apple Developer Program&lt;/a&gt; and that part is not going away.&lt;/p&gt;

&lt;h2&gt;
  
  
  EAS is the path I recommend first
&lt;/h2&gt;

&lt;p&gt;I tried local iOS setup because I wanted to understand what was happening under the hood, but if I were telling another React web developer how to start, I would point them to &lt;a href="https://expo.dev/services" rel="noopener noreferrer"&gt;EAS&lt;/a&gt; earlier.&lt;/p&gt;

&lt;p&gt;The basic flow is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;eas build:configure
eas build &lt;span class="nt"&gt;--platform&lt;/span&gt; ios
eas submit &lt;span class="nt"&gt;--platform&lt;/span&gt; ios
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact profile depends on what you are building. &lt;code&gt;development&lt;/code&gt; is for the dev client. &lt;code&gt;preview&lt;/code&gt; is for internal distribution to a small group of testers. &lt;code&gt;production&lt;/code&gt; is the App Store / TestFlight target. Profiles live in &lt;code&gt;eas.json&lt;/code&gt; and you control them there.&lt;/p&gt;

&lt;p&gt;One more nuance: &lt;code&gt;eas submit&lt;/code&gt; uploads the binary to App Store Connect and makes it available in TestFlight after processing. It does not release the app to production. You still need App Store Connect metadata, screenshots, the privacy questionnaire, build selection, and App Review.&lt;/p&gt;

&lt;p&gt;That gives you cloud builds, credentials management, and a cleaner path to TestFlight and App Store submission - removing a lot of the local machine pain from the first serious build.&lt;/p&gt;

&lt;p&gt;**Expo Skills **also helped here because deployment advice gets stale quickly. Build profiles, credentials, submission flows, internal distribution, TestFlight - these are exactly the places where current Expo-specific context matters.&lt;/p&gt;

&lt;p&gt;I also tried the local setup: Xcode, CocoaPods, device trust, signing. It took about an hour. Not zero friction, but also not the two-week nightmare I had invented in my head.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The risk of trying is lower than it looks.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You do not have to start by becoming an iOS developer, buying every tool, and committing to an App Store release. The lowest-friction first step is: &lt;code&gt;npx create-expo-app&lt;/code&gt;, open it in the iOS Simulator with Expo Go, and wire up one native capability (location or haptics is a good pick). That is a one-evening loop. Move to a development build only when you reach for a library Expo Go does not ship with - Skia, Reanimated, custom native modules.&lt;/p&gt;

&lt;p&gt;If it does, EAS gives you a path toward real builds and distribution. If it does not, you have spent a weekend learning how your React skills map to another surface area.&lt;/p&gt;

&lt;p&gt;Native setup is real, but it is not a reason to avoid native forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  The moment it clicked
&lt;/h2&gt;

&lt;p&gt;The moment this stopped feeling theoretical was when I ran Sun Buddy on my actual iPhone. Sunny bounced. The Skia progress ring rendered. The UV badge showed a real number pulled from my real location. I tapped the start button and felt the haptic, then tapped it again, then a few more times because the loop felt good.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tap. Animation. Haptic. Progress.&lt;/strong&gt; Not a browser tab, not a responsive web view, not a prototype pretending to be an app, but a real thing on my phone, built with React.&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5tu1lq25i3r4mh8qk93l.png" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5tu1lq25i3r4mh8qk93l.png" alt="phone" width="800" height="801"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It was on my home screen. It knew where I was. It responded to touch in a way the web usually does not.&lt;/p&gt;

&lt;p&gt;That was the moment native stopped feeling like a completely different career path and started feeling like another surface area for the skills I already had.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would tell another React web developer
&lt;/h2&gt;

&lt;p&gt;Your skills transfer. That is the main thing. The component model, the hooks, the debugging instincts - all of it comes with you. You are not starting from zero.&lt;/p&gt;

&lt;p&gt;But native has edges, and you will hit them. A permission sequence. A stale Metro bundle. A config plugin. A signing capability. There will probably be a moment where you think: &lt;em&gt;this is why I do not build native apps.&lt;/em&gt; For me, that moment was usually one &lt;code&gt;--clear&lt;/code&gt; flag or one config entry away from being solved.&lt;/p&gt;

&lt;p&gt;The AI part did not remove the work. I still had to decide what the app should do. I still had to notice that my sun tracker happily tracked sunlight at 9pm, which was ridiculous. The product thinking was mine. The judgment was mine.&lt;/p&gt;

&lt;p&gt;If you write React and you have been avoiding native, do not start by trying to become a mobile expert. Start with one thing the web cannot really give you: location, haptics, camera, notifications, gestures.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;Build something small around that. Run it on a device.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You will know pretty quickly whether it still feels like a separate world, or whether it starts to feel like another surface area for the skills you already have.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Sun Buddy was built with&lt;/em&gt;&amp;nbsp;&lt;a href="https://expo.dev/" rel="noopener noreferrer"&gt;&lt;em&gt;Expo&lt;/em&gt;&lt;/a&gt;&lt;em&gt;￼,&lt;/em&gt;&amp;nbsp;&lt;a href="https://claude.ai/code" rel="noopener noreferrer"&gt;&lt;em&gt;Claude Code&lt;/em&gt;&lt;/a&gt;&lt;em&gt;￼, and&lt;/em&gt;&amp;nbsp;&lt;a href="https://github.com/expo/skills" rel="noopener noreferrer"&gt;&lt;em&gt;Expo Skills&lt;/em&gt;&lt;/a&gt;&lt;em&gt;￼. If you are a React web developer thinking about going native, I would start here:&lt;/em&gt;&amp;nbsp;&lt;a href="https://expo.dev/solutions/expo-for-react-web-devs" rel="noopener noreferrer"&gt;&lt;em&gt;Expo for React web developers&lt;/em&gt;&lt;/a&gt;&lt;em&gt;￼.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>expo</category>
      <category>reactnative</category>
    </item>
    <item>
      <title>What Expo shipped in 2026: New performance tools and AI integrations</title>
      <dc:creator>Dan</dc:creator>
      <pubDate>Tue, 23 Jun 2026 13:50:50 +0000</pubDate>
      <link>https://dev.to/expo/what-expo-shipped-in-2026-new-performance-tools-and-ai-integrations-3i2m</link>
      <guid>https://dev.to/expo/what-expo-shipped-in-2026-new-performance-tools-and-ai-integrations-3i2m</guid>
      <description>&lt;p&gt;App.js 2026 just wrapped up in Kraków with over 500 React Native developers gathering for two days of ecosystem updates. We shared some major announcements, including our new production performance library and a year's worth of platform improvements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why React Native fits the AI development wave
&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/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flclkkn5vlvoqktx0vgog.png" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flclkkn5vlvoqktx0vgog.png" alt="Expo is growing" width="800" height="371"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;AI changed who builds apps and how quickly they ship. React Native and Expo happened to be positioned well for this shift, thanks to decisions made years before anyone thought about prompting models to write code.&lt;/p&gt;

&lt;p&gt;The foundation matters here. Building with JavaScript, TypeScript, and React gives AI models massive training datasets to work from. When a model generates Expo code, it's drawing from years of public React and TypeScript repos, not just a few examples.&lt;/p&gt;

&lt;p&gt;Documentation plays a bigger role than you might expect. Over the past year, docs commits made up the largest portion of changes to the Expo repo. That documentation becomes training data, so better docs directly improve how well models write Expo apps.&lt;/p&gt;

&lt;p&gt;Universal deployment helps too. When someone prompts an app builder ("make me an app that streams my radio station"), the model doesn't need platform-specific knowledge. It can default to Expo, build once, and target everything.&lt;/p&gt;

&lt;p&gt;The community amplifies all of this. When you solve a React Native problem and write about it, both developers and models learn from your solution. This creates a compounding effect across the ecosystem.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we're working on next
&lt;/h2&gt;

&lt;p&gt;Agents need feedback loops. When an agent can run an app, interact with it, and verify it works correctly, it can debug and iterate independently. Tools like &lt;a href="https://argent.swmansion.com/" rel="noopener noreferrer"&gt;Argent&lt;/a&gt; from Software Mansion and &lt;a href="https://www.callstack.com/blog/agent-device-ai-native-mobile-automation-for-ios-android" rel="noopener noreferrer"&gt;Agent Device&lt;/a&gt; from Callstack that let agents screenshot, tap, and inspect running apps enable this workflow.&lt;/p&gt;

&lt;p&gt;Speed remains important. Since writing code got faster for most developers, the bottleneck shifted to builds, distribution, and boot times. Much of our upcoming work targets making these parts as fast as possible.&lt;/p&gt;

&lt;p&gt;We want the default path to produce good results. Tools like &lt;a href="https://docs.expo.dev/versions/latest/sdk/router/" rel="noopener noreferrer"&gt;Expo Router&lt;/a&gt; should give you a polished, native-feeling app without extra configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  A year of major releases
&lt;/h2&gt;

&lt;p&gt;Here's what shipped over the past twelve months:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.expo.dev/guides/building-for-tv/" rel="noopener noreferrer"&gt;TV app support&lt;/a&gt; extends universal apps to living rooms. You can share about 85% of your code between mobile, Apple TV, and Android TV.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.expo.dev/guides/expo-ui-swift-ui/" rel="noopener noreferrer"&gt;Expo UI&lt;/a&gt; reached stability in &lt;a href="https://expo.dev/changelog/sdk-56" rel="noopener noreferrer"&gt;Expo SDK 56&lt;/a&gt; with a default template. It renders real SwiftUI and Jetpack Compose widgets from a single JavaScript API, letting you use OS primitives without writing native modules.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://launch.expo.dev/" rel="noopener noreferrer"&gt;Expo Launch&lt;/a&gt; provides a web-based flow for getting apps to TestFlight, App Store, and web without manually writing config files.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://docs.expo.dev/eas/ai/mcp/" rel="noopener noreferrer"&gt;Expo MCP server&lt;/a&gt; connects agents to your project's SDK version, config, dev server, and cloud services. It's free for all Expo accounts. The simulator integration stands out: multimodal agents can screenshot and interact with running apps, pull React Native DevTools data, and debug build failures using TestFlight and crash reports.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/expo/skills" rel="noopener noreferrer"&gt;Expo skills&lt;/a&gt; contains official skills for building, deploying, and debugging apps. It covers UI development, deployment, SDK upgrades, DOM components, and dev clients. With deep Expo Router integration, agents can generate native-feeling UIs in single attempts.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.expo.dev/versions/latest/sdk/widgets/" rel="noopener noreferrer"&gt;Expo widgets&lt;/a&gt; simplifies building home screen widgets and live activities on iOS without native code.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://expo.dev/blog/expo-router-v56-decoupling-from-react-navigation" rel="noopener noreferrer"&gt;Expo Router v56&lt;/a&gt; adds more native navigation primitives: native toolbars, link transitions, and platform-specific behaviors. The demo showed an accessory sliding into the tab bar on scroll, smooth shared element transitions, and a search bar that collapsed the tab bar while floating above the keyboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Expo SDK 56 focuses on performance
&lt;/h2&gt;

&lt;p&gt;SDK 56 prioritizes speed improvements. &lt;a href="https://docs.expo.dev/guides/prebuilt-expo-modules/" rel="noopener noreferrer"&gt;Pre-compiled binaries&lt;/a&gt; accelerate builds, and Android cold start times dropped &lt;strong&gt;40%&lt;/strong&gt;. Hermes V1 is now default, along with numerous smaller optimizations.&lt;/p&gt;

&lt;p&gt;The new starter app makes a bigger impact than expected. Fresh projects now open with a polished first screen, including clean entrance animations and built-in explore tab animations.&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fprjaz10rlk2wibzq0kz5.png" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fprjaz10rlk2wibzq0kz5.png" alt="starter app" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Starting from something that looks good changes how development feels from day one.&lt;/p&gt;

&lt;p&gt;The &lt;a href="http://expo.dev" rel="noopener noreferrer"&gt;expo.dev&lt;/a&gt; dashboard got a complete redesign with better organization and more data. EAS Workflows added prepackaged jobs: GitHub comment jobs that report builds and updates on PRs, Apple device registration jobs for ad hoc provisioning profiles, and approval jobs that add human checkpoints to workflows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introducing Observe for production performance
&lt;/h2&gt;

&lt;p&gt;The biggest announcement was &lt;a href="https://docs.expo.dev/eas/observe/get-started/" rel="noopener noreferrer"&gt;Observe&lt;/a&gt;, our new open source library for capturing real performance metrics from production apps.&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%2Fcdn.sanity.io%2Fimages%2F9r24npb8%2Fproduction%2F1f09604c35d0e6aef44964537899107ecfd6dc3c-4816x2732.png" 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%2Fcdn.sanity.io%2Fimages%2F9r24npb8%2Fproduction%2F1f09604c35d0e6aef44964537899107ecfd6dc3c-4816x2732.png" alt="observe" width="800" height="454"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Local development doesn't match user reality. You test on good Wi-Fi with a fast device. Users have every kind of device and network imaginable. Performance problems hide in that gap.&lt;/p&gt;

&lt;p&gt;Observe tracks the complete user journey: from tapping your app icon to actually using it. It measures launch time, bundle load time, time to render, and time to interactive. Each measurement includes device metadata, app version, and location. Time-to-interactive metrics also capture device state: battery level, Wi-Fi status, and dropped frame counts. You can attach custom objects to track screen-specific data.&lt;/p&gt;

&lt;p&gt;Per-screen metrics answer the most common developer questions. Mobile apps behave like SPAs, making it hard to identify slow screens. Since every Expo Router route has a URL, Observe can report metrics per page. After rebuilds, you see cold and warm time-to-render and time-to-interactive for each screen.&lt;/p&gt;

&lt;p&gt;Version tracking gets clearer too. React Native apps have native layers and JavaScript bundles, making version identification tricky. Observe shows exactly which version each user is running. In a two-version example (1.0.0 and 1.0.1), you see installs and active users for each version.&lt;/p&gt;

&lt;p&gt;Custom event tracking lets you capture any serializable data.&lt;/p&gt;

&lt;p&gt;Reading the data uses AI assistance. Instead of scanning dashboards for problematic metrics, you can ask questions in plain text and let agents analyze the data. Observe ships with skills for installation and metric interpretation.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Expo Observe is in public beta, free for up to 10,000 monthly active users for at least three months. It will become a paid service after that. We want feedback while preparing for general availability.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;More features are coming in future releases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building is getting more accessible
&lt;/h2&gt;

&lt;p&gt;Seth Webster's closing talk, "It's a great time to be a builder," argued that one person with good judgment and the right tools can now create what used to require ten-person teams.&lt;/p&gt;

&lt;p&gt;Many React developers once saw mobile development as requiring Objective-C or Java knowledge to ship real apps. React Native and Expo removed that barrier. If AI is opening similar doors for more developers, that's worth celebrating.&lt;/p&gt;

&lt;p&gt;Check out &lt;a href="https://www.youtube.com/live/4H8iRPN0Q2M?si=_vy_-aFU8qgMrb_f" rel="noopener noreferrer"&gt;the recorded talks&lt;/a&gt; from Software Mansion. The full two days feature some of the best people working in React Native. Thanks to Software Mansion and all sponsors who made the conference happen.&lt;/p&gt;

&lt;p&gt;Now go build something! Join us in Portland at &lt;a href="https://chainreactconf.com/" rel="noopener noreferrer"&gt;Chain React Conf&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This post is based on content from the &lt;a href="https://expo.dev/blog/expo-highlights-new-products-and-plans-for-the-future" rel="noopener noreferrer"&gt;Expo blog&lt;/a&gt;. Follow &lt;a href="https://dev.to/expo"&gt;@expo&lt;/a&gt; for more React Native content.&lt;/p&gt;

</description>
      <category>expo</category>
      <category>mobile</category>
      <category>reactnative</category>
    </item>
    <item>
      <title>Skip Recompiling 70+ iOS Packages on Every Build</title>
      <dc:creator>Dan</dc:creator>
      <pubDate>Mon, 22 Jun 2026 13:55:58 +0000</pubDate>
      <link>https://dev.to/expo/skip-recompiling-70-ios-packages-on-every-build-27a6</link>
      <guid>https://dev.to/expo/skip-recompiling-70-ios-packages-on-every-build-27a6</guid>
      <description>&lt;p&gt;React Native developers know the pain: every clean iOS build recompiles your entire dependency tree from scratch. React Native core, Expo modules, third-party libraries — everything rebuilds, every time.&lt;/p&gt;

&lt;p&gt;Expo SDK 56 changes this by shipping Expo modules as precompiled XCFrameworks. Your app links these binaries directly instead of rebuilding from source. Clean builds become dramatically faster with zero configuration.&lt;/p&gt;

&lt;p&gt;This also marks the start of a bigger shift: moving the entire ecosystem from CocoaPods to Swift Package Manager, Apple's modern dependency system.&lt;/p&gt;

&lt;h2&gt;
  
  
  What precompiled XCFrameworks do for you
&lt;/h2&gt;

&lt;p&gt;Previously, every iOS build meant compiling these items from source:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;React Native core&lt;/li&gt;
&lt;li&gt;All Expo modules
&lt;/li&gt;
&lt;li&gt;Every third-party library with native code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;SDK 56 distributes many Expo modules as precompiled XCFrameworks through &lt;code&gt;npm&lt;/code&gt;. Your build process links these binaries instead of recompiling source code.&lt;/p&gt;

&lt;p&gt;The results: fewer compilation steps, faster local development, faster EAS Builds, and more predictable build environments.&lt;/p&gt;

&lt;p&gt;No migration needed. This works automatically in existing apps.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;XCFrameworks explained&lt;/strong&gt;: Apple's format for distributing precompiled native libraries. Instead of source code that every app compiles locally, you get binary artifacts already compiled for iOS devices, simulators, and multiple architectures. React Native already uses XCFrameworks internally — SDK 56 extends this to Expo modules.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Two problems this solves
&lt;/h2&gt;

&lt;p&gt;This work addresses major long-term issues in React Native development.&lt;/p&gt;

&lt;h3&gt;
  
  
  CocoaPods is going away
&lt;/h3&gt;

&lt;p&gt;React Native and most RN libraries currently depend on CocoaPods for dependency resolution, native autolinking, project integration, and build orchestration.&lt;/p&gt;

&lt;p&gt;CocoaPods is Ruby-based legacy infrastructure. It becomes read-only in December 2026.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.swift.org/swiftpm/documentation/packagemanagerdocs/" rel="noopener noreferrer"&gt;Swift Package Manager&lt;/a&gt; is Apple's standard tooling for native dependencies, builds, and package distribution. React Native itself has started moving toward SPM support, including distributing parts of RN as precompiled XCFrameworks.&lt;/p&gt;

&lt;p&gt;SDK 56 continues this direction.&lt;/p&gt;

&lt;h3&gt;
  
  
  Native builds are expensive and slow
&lt;/h3&gt;

&lt;p&gt;As apps grow, native build times increase. This hurts especially in CI environments, EAS Build, and large monorepos with many native dependencies.&lt;/p&gt;

&lt;p&gt;Precompiled XCFrameworks move compilation work earlier in the pipeline. Frameworks get compiled once, packaged, then reused across builds. This eliminates repeated native compilation work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this was technically difficult
&lt;/h2&gt;

&lt;p&gt;React Native and Expo were originally built around CocoaPods and source-based compilation. That environment is permissive: headers are globally available, source files can import almost anything, pod targets have loose isolation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;XCFrameworks impose strict rules.&lt;/strong&gt; Every framework must be fully modular, self-contained, and isolated from implementation details outside its module boundary.&lt;/p&gt;

&lt;p&gt;Many assumptions that work in CocoaPods break when building distributable XCFrameworks.&lt;/p&gt;

&lt;p&gt;Rewriting the native architecture from scratch wasn't realistic. We built incremental compatibility layers and new build infrastructure instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  Build time improvements
&lt;/h3&gt;

&lt;p&gt;These numbers come from an Apple M4 Max with 64 GB memory, running clean iOS builds of a stock Expo app while enabling each layer of precompiled XCFrameworks:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Build configuration&lt;/th&gt;
&lt;th&gt;Reduction vs previous&lt;/th&gt;
&lt;th&gt;Reduction vs from-source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Everything from source&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+ React Native core&lt;/td&gt;
&lt;td&gt;~44%&lt;/td&gt;
&lt;td&gt;~44%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+ Expo modules prebuilt&lt;/td&gt;
&lt;td&gt;~10%&lt;/td&gt;
&lt;td&gt;~50%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+ Third-party libraries prebuilt&lt;/td&gt;
&lt;td&gt;~30%&lt;/td&gt;
&lt;td&gt;~65%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Larger projects see benefits based on coverage: React Native and Expo modules are always precompiled, but only widely-used third-party libraries are included. Projects using uncommon native dependencies will compile those from source and see smaller reductions.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Making Expo Modules Core work with SPM
&lt;/h2&gt;

&lt;p&gt;The first step was adapting &lt;code&gt;expo-modules-core&lt;/code&gt;. Nearly every Expo module depends on it, so it sits at the root of the dependency graph. &lt;strong&gt;If it can't build as a modular XCFramework, nothing else can.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This required solving several architectural problems.&lt;/p&gt;

&lt;h3&gt;
  
  
  Removing illegal header exports
&lt;/h3&gt;

&lt;p&gt;Some Expo Modules Core public headers exposed React Native headers directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight objective_c"&gt;&lt;code&gt;&lt;span class="cp"&gt;#import &amp;lt;React/RCTView.h&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works in CocoaPods because all headers are globally available during compilation. Framework interfaces don't allow this.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A framework cannot publicly expose headers from another framework unless those dependencies are themselves modularized correctly.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We solved this by refactoring public interfaces, isolating React Native internals, and restructuring APIs so non-modular dependencies don't leak into exported interfaces.&lt;/p&gt;

&lt;h3&gt;
  
  
  Breaking Swift ↔ Objective-C cycles
&lt;/h3&gt;

&lt;p&gt;Swift Package Manager is much stricter about mixed-language targets than CocoaPods.&lt;/p&gt;

&lt;p&gt;Expo Modules Core had cyclic dependencies where Objective-C referenced Swift types while Swift referenced Objective-C types.&lt;/p&gt;

&lt;p&gt;SPM requires clear dependency direction between targets. We introduced new interface abstractions, separated implementation layers, and refactored internal APIs.&lt;/p&gt;

&lt;p&gt;In some cases, we used Objective-C runtime reflection to dynamically invoke Swift implementations without illegal compile-time dependencies.&lt;/p&gt;

&lt;h3&gt;
  
  
  Separating source trees for SPM
&lt;/h3&gt;

&lt;p&gt;Swift Package Manager is strict about source ownership. The same source file cannot belong to multiple targets in the same package graph.&lt;/p&gt;

&lt;p&gt;Expo's repository structure wasn't designed around this assumption. Rather than permanently reorganizing repositories, we generate temporary isolated source structures during builds using symlinks, generated folders, and build-time source separation.&lt;/p&gt;

&lt;p&gt;This preserves the existing repository layout while satisfying SPM's isolation requirements.&lt;/p&gt;

&lt;h2&gt;
  
  
  React Native and Virtual File System overlays
&lt;/h2&gt;

&lt;p&gt;Another challenge was React Native's header structure.&lt;/p&gt;

&lt;p&gt;React Native's current XCFramework support still depends on the legacy CocoaPods-generated header layout. That layout doesn't naturally exist in Swift Package Manager builds.&lt;/p&gt;

&lt;p&gt;To bridge this gap, we added support for Clang Virtual File System (VFS) overlays inside React Native.&lt;/p&gt;

&lt;p&gt;A VFS overlay lets the compiler "see" a virtual header layout different from the physical filesystem structure.&lt;/p&gt;

&lt;p&gt;This allows us to preserve existing include paths, avoid massive source refactors, and present a modular structure to the compiler without physically reorganizing React Native's source tree.&lt;/p&gt;

&lt;p&gt;This is common when modernizing large legacy native codebases into distributable modular frameworks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Auto-generating Package.swift files
&lt;/h2&gt;

&lt;p&gt;Swift Package Manager uses &lt;code&gt;Package.swift&lt;/code&gt; manifests to define targets, dependencies, platforms, and build settings. Maintaining these manually across Expo packages would become difficult and error-prone.&lt;/p&gt;

&lt;p&gt;We added new tooling to &lt;code&gt;expo-tools&lt;/code&gt; that automatically generates Package.swift manifests, isolated source structures, dependency graphs, and XCFramework packaging steps.&lt;/p&gt;

&lt;p&gt;This infrastructure runs fully in CI and will eventually power large-scale precompiled package distribution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Supporting both CocoaPods and SPM
&lt;/h2&gt;

&lt;p&gt;This transition takes time.&lt;/p&gt;

&lt;p&gt;The React Native ecosystem still depends heavily on CocoaPods, and many libraries aren't yet compatible with Swift Package Manager. SDK 56 focuses on coexistence rather than replacement.&lt;/p&gt;

&lt;p&gt;Expo modules can switch between building from source or consuming precompiled XCFrameworks.&lt;/p&gt;

&lt;p&gt;This lets existing apps continue working while keeping CocoaPods supported as the ecosystem gradually modernizes.&lt;/p&gt;

&lt;p&gt;If needed, precompiled modules can be disabled:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;EXPO_USE_PRECOMPILED_MODULES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Long-term, we expect more of Expo's autolinking, native integration, and build tooling to move into Swift Package Manager itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part of broader modernization
&lt;/h2&gt;

&lt;p&gt;Precompiled XCFrameworks are part of broader modernization in Expo SDK 56, which also introduces &lt;a href="https://expo.dev/blog/native-code-expo-sdk-56" rel="noopener noreferrer"&gt;inline native modules&lt;/a&gt;, continued React Native modernization work, and infrastructure improvements for future native tooling.&lt;/p&gt;

&lt;p&gt;Together, these changes move Expo toward faster native builds, cleaner modular architecture, and deeper integration with Apple's modern development ecosystem.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;Our current focus is stabilizing compatibility, expanding package coverage, validating build performance improvements, and continuing upstream collaboration with React Native.&lt;/p&gt;

&lt;p&gt;This is one of the largest infrastructure migrations we've undertaken on the iOS side of Expo. But it opens up a much more scalable future for React Native development on Apple platforms: faster builds, better tooling, cleaner native boundaries, and eventually a world without CocoaPods.&lt;/p&gt;

&lt;p&gt;SDK 56 is the beginning of that transition.&lt;/p&gt;

&lt;p&gt;This post is based on content from the &lt;a href="https://expo.dev/blog/faster-ios-builds-with-precompiled-xcframeworks" rel="noopener noreferrer"&gt;Expo blog&lt;/a&gt;. Follow &lt;a href="https://dev.to/expo"&gt;@expo&lt;/a&gt; for more React Native content.&lt;/p&gt;

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