<?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 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>
    <item>
      <title>Native UI Components from One Import: Expo UI is Production-Ready</title>
      <dc:creator>Dan</dc:creator>
      <pubDate>Fri, 19 Jun 2026 13:41:05 +0000</pubDate>
      <link>https://dev.to/expo/native-ui-components-from-one-import-expo-ui-is-production-ready-n0k</link>
      <guid>https://dev.to/expo/native-ui-components-from-one-import-expo-ui-is-production-ready-n0k</guid>
      <description>&lt;p&gt;With SDK 56, &lt;code&gt;@expo/ui&lt;/code&gt; gives you real SwiftUI and Jetpack Compose components in React Native. No JavaScript reimplementations, no platform-specific code splitting. Just import once and get native components that follow platform conventions.&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%2F2egzxjj1369734ltpvr1.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%2F2egzxjj1369734ltpvr1.png" alt="Universal components in Expo UI, across Android, iOS and web." width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Universal components in Expo UI, across Android, iOS and web.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The library is bundled into Expo Go and included in the default &lt;code&gt;[create-expo-app](https://docs.expo.dev/more/create-expo/)&lt;/code&gt; template. You can start using it immediately in any new project.&lt;/p&gt;

&lt;p&gt;This release caps three SDK cycles of development:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SDK 53&lt;/strong&gt; — basic SwiftUI and Jetpack Compose components&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SDK 54&lt;/strong&gt; — &lt;code&gt;[&amp;lt;Host&amp;gt;](https://expo.dev/blog/liquid-glass-app-with-expo-ui-and-swiftui)&lt;/code&gt;&lt;a href="https://expo.dev/blog/liquid-glass-app-with-expo-ui-and-swiftui" rel="noopener noreferrer"&gt;, modifiers, and container views like &lt;/a&gt;&lt;code&gt;[Form](https://expo.dev/blog/liquid-glass-app-with-expo-ui-and-swiftui)&lt;/code&gt;&lt;a href="https://expo.dev/blog/liquid-glass-app-with-expo-ui-and-swiftui" rel="noopener noreferrer"&gt; and &lt;/a&gt;&lt;code&gt;[List](https://expo.dev/blog/liquid-glass-app-with-expo-ui-and-swiftui)&lt;/code&gt;. The &lt;a href="https://github.com/expo/hot-chocolate/tree/sdk-55" rel="noopener noreferrer"&gt;hot-chocolate&lt;/a&gt; demo showed you could build complete apps with it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SDK 55&lt;/strong&gt; — &lt;a href="https://expo.dev/blog/expo-ui-in-sdk-55-jetpack-compose-now-available-for-react-native-apps" rel="noopener noreferrer"&gt;Jetpack Compose support in beta&lt;/a&gt;, plus API alignment with Apple's SwiftUI documentation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SDK 56&lt;/strong&gt; — Compose API audit, universal layer, and drop-in replacements for popular community packages&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's what each piece looks like in practice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Universal components: write once, run natively everywhere
&lt;/h2&gt;

&lt;p&gt;The universal layer is the biggest change in SDK 56. Instead of importing from platform-specific packages like &lt;code&gt;@expo/ui/swift-ui&lt;/code&gt; or &lt;code&gt;@expo/ui/jetpack-compose&lt;/code&gt;, you import from &lt;code&gt;@expo/ui&lt;/code&gt; and get the right implementation automatically:&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;Host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;FieldGroup&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Switch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Slider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Spacer&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/ui&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;Each universal component is a thin wrapper that renders the SwiftUI version on iOS and the Compose version on Android. No JavaScript fallback layer means you're always using the actual platform component. The universal layer covers layout primitives, text, inputs, controls, and sheets: &lt;code&gt;Host&lt;/code&gt;, &lt;code&gt;Row&lt;/code&gt;, &lt;code&gt;Column&lt;/code&gt;, &lt;code&gt;ScrollView&lt;/code&gt;, and more. &lt;a href="https://docs.expo.dev/versions/v56.0.0/sdk/ui/universal/" rel="noopener noreferrer"&gt;Learn more about universal components&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The naming leans toward React Native conventions. You write &lt;code&gt;Switch&lt;/code&gt; instead of &lt;code&gt;Toggle&lt;/code&gt;, and &lt;code&gt;Column&lt;/code&gt;/&lt;code&gt;Row&lt;/code&gt; instead of &lt;code&gt;HStack&lt;/code&gt;/&lt;code&gt;VStack&lt;/code&gt;. The SwiftUI-style names are still available under &lt;code&gt;@expo/ui/swift-ui&lt;/code&gt; when you need them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Building a settings screen with universal components
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;FieldGroup&lt;/code&gt; is the universal equivalent of SwiftUI's &lt;code&gt;Form&lt;/code&gt;. It creates grouped, sectioned lists with section titles, footers, and platform-appropriate styling on iOS and Android. The component uses compound patterns like &lt;code&gt;FieldGroup.Section&lt;/code&gt; and &lt;code&gt;FieldGroup.SectionFooter&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;FieldGroup&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Slider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Spacer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Switch&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="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@expo/ui&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;SettingsScreen&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;notifications&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setNotifications&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&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;sounds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setSounds&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&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;brightness&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setBrightness&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.6&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Host&lt;/span&gt; &lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="na"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;FieldGroup&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;FieldGroup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Section&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Notifications&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;LabeledRow&lt;/span&gt; &lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Push notifications&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Switch&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;notifications&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;onValueChange&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;setNotifications&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/LabeledRow&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;LabeledRow&lt;/span&gt; &lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Sounds&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Switch&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;sounds&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;onValueChange&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;setSounds&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/LabeledRow&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;FieldGroup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SectionFooter&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Text&lt;/span&gt; &lt;span class="nx"&gt;textStyle&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="na"&gt;fontSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#6c6c70&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
              &lt;span class="nx"&gt;Notification&lt;/span&gt; &lt;span class="nx"&gt;previews&lt;/span&gt; &lt;span class="nx"&gt;can&lt;/span&gt; &lt;span class="nx"&gt;expose&lt;/span&gt; &lt;span class="nx"&gt;sensitive&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="nx"&gt;on&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;lock&lt;/span&gt; &lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
            &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Text&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/FieldGroup.SectionFooter&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/FieldGroup.Section&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;FieldGroup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Section&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Display&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;LabeledRow&lt;/span&gt; &lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Brightness&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Slider&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;brightness&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;onValueChange&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;setBrightness&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/LabeledRow&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/FieldGroup.Section&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;FieldGroup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Section&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Row&lt;/span&gt; &lt;span class="nx"&gt;alignment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;center&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="na"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Spacer&lt;/span&gt; &lt;span class="nx"&gt;flexible&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
            &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Button&lt;/span&gt; &lt;span class="nx"&gt;variant&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;outlined&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;onPress&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Signed out&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt; &lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Sign out&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
            &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Spacer&lt;/span&gt; &lt;span class="nx"&gt;flexible&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Row&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/FieldGroup.Section&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/FieldGroup&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Host&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;LabeledRow&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ReactNode&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Row&lt;/span&gt; &lt;span class="nx"&gt;alignment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;center&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;spacing&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Text&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Text&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Spacer&lt;/span&gt; &lt;span class="nx"&gt;flexible&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Row&lt;/span&gt;&lt;span class="err"&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;This single code snippet produces a SwiftUI &lt;code&gt;Form&lt;/code&gt; with inset-grouped sections on iOS and a Material 3 grouped list on Android. The &lt;code&gt;Switch&lt;/code&gt; and &lt;code&gt;Slider&lt;/code&gt; components automatically become system controls on iOS and Material components on Android. No platform-specific files needed.&lt;/p&gt;

&lt;p&gt;You can still reach for &lt;code&gt;@expo/ui/swift-ui&lt;/code&gt; and &lt;code&gt;@expo/ui/jetpack-compose&lt;/code&gt; when you need platform-specific features like SwiftUI's &lt;code&gt;glassEffect()&lt;/code&gt; or Compose's &lt;code&gt;DockedSearchBar&lt;/code&gt;. Mix them freely within the same &lt;code&gt;Host&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Notes on the universal layer:&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;This is version one.&lt;/strong&gt; Some universal components work well across all platforms, others don't because SwiftUI, Compose, and web don't always offer equivalent primitives. Let us know which components work, which don't, which are missing, and where you use the platform-specific packages directly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Web is experimental.&lt;/strong&gt; Universal components have web implementations but they're not production-quality yet. Expect improvements in upcoming releases based on your feedback.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Stable APIs and new capabilities
&lt;/h2&gt;

&lt;p&gt;Both the SwiftUI and Compose APIs now match their platform documentation. Most code samples you find online (Apple docs, Google docs, blog posts) are one search-and-replace away from working in Expo UI. Component names, prop names, modifier names all match.&lt;/p&gt;

&lt;p&gt;SwiftUI's &lt;code&gt;Toggle&lt;/code&gt; and &lt;code&gt;Form&lt;/code&gt; keep their names in &lt;code&gt;@expo/ui/swift-ui&lt;/code&gt;. Compose's &lt;code&gt;LazyColumn&lt;/code&gt; keeps its name in &lt;code&gt;@expo/ui/jetpack-compose&lt;/code&gt;. Props and modifiers follow the same pattern.&lt;/p&gt;

&lt;p&gt;SDK 56 includes new features alongside the API stabilization:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Build your own components.&lt;/strong&gt; SwiftUI and Jetpack Compose have hundreds of views and modifiers. To bridge that gap, you can now create your own SwiftUI and Jetpack Compose views and modifiers while Expo UI handles the layout, props, and events. &lt;a href="https://docs.expo.dev/guides/expo-ui-swift-ui/extending/" rel="noopener noreferrer"&gt;Learn more for SwiftUI&lt;/a&gt; and &lt;a href="https://docs.expo.dev/guides/expo-ui-jetpack-compose/extending/" rel="noopener noreferrer"&gt;Jetpack Compose&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Material 3 Dynamic Colors.&lt;/strong&gt; &lt;code&gt;[useMaterialColors](https://docs.expo.dev/versions/v56.0.0/sdk/ui/jetpack-compose/colors/)&lt;/code&gt; gives you Material 3 Dynamic Colors that adapt to the system theme.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Full Material Symbols access.&lt;/strong&gt; The &lt;code&gt;[Icon](https://docs.expo.dev/versions/v56.0.0/sdk/ui/jetpack-compose/icon/)&lt;/code&gt; component works with &lt;code&gt;[@expo/material-symbols](https://www.npmjs.com/package/@expo/material-symbols)&lt;/code&gt; to make the entire Material Symbols catalog importable.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;UI thread state management.&lt;/strong&gt; &lt;code&gt;useNativeState&lt;/code&gt; for &lt;a href="https://docs.expo.dev/versions/v56.0.0/sdk/ui/swift-ui/usenativestate/" rel="noopener noreferrer"&gt;SwiftUI&lt;/a&gt; and &lt;a href="https://docs.expo.dev/versions/v56.0.0/sdk/ui/jetpack-compose/usenativestate/" rel="noopener noreferrer"&gt;Jetpack Compose&lt;/a&gt; lets you build smooth, UI-thread-driven controls without flicker. We'll cover this in a dedicated post soon.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Replace community packages with native alternatives
&lt;/h2&gt;

&lt;p&gt;Most React Native apps install similar community packages for platform primitives like pickers and sliders. Each adds a native dependency with its own release schedule and potential SDK compatibility issues. SDK 56 ships native replacements for seven common packages under &lt;code&gt;@expo/ui/community&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Community package&lt;/th&gt;
&lt;th&gt;Drop-in replacement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;@react-native-community/datetimepicker&lt;/td&gt;
&lt;td&gt;@expo/ui/community/datetime-picker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@react-native-community/slider&lt;/td&gt;
&lt;td&gt;@expo/ui/community/slider&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;react-native-pager-view&lt;/td&gt;
&lt;td&gt;@expo/ui/community/pager-view&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@react-native-picker/picker&lt;/td&gt;
&lt;td&gt;@expo/ui/community/picker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@react-native-segmented-control/segmented-control&lt;/td&gt;
&lt;td&gt;@expo/ui/community/segmented-control&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@react-native-masked-view/masked-view&lt;/td&gt;
&lt;td&gt;@expo/ui/community/masked-view&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@react-native-menu/menu&lt;/td&gt;
&lt;td&gt;@expo/ui/community/menu&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;a class="mentioned-user" href="https://dev.to/gorhom"&gt;@gorhom&lt;/a&gt;/bottom-sheet&lt;/td&gt;
&lt;td&gt;@expo/ui/community/bottom-sheet&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Most migrations are simple import swaps. Some props differ because Expo UI uses SwiftUI and Jetpack Compose instead of UIKit and Android Views:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;DateTimePicker&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@react-native-community/datetimepicker&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// After&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;DateTimePicker&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/ui/community/datetime-picker&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;&lt;a href="https://docs.expo.dev/versions/v56.0.0/sdk/ui/drop-in-replacements/" rel="noopener noreferrer"&gt;Learn more about drop-in replacements&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The result: fewer dependencies, one upgrade path, consistent foundation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choose the right tool for each use case
&lt;/h2&gt;

&lt;p&gt;Expo UI isn't a UI component library or design system. Like writing &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;span&amp;gt;&lt;/code&gt; in react-dom, you write &lt;code&gt;Column&lt;/code&gt; and &lt;code&gt;Row&lt;/code&gt; in &lt;code&gt;@expo/ui&lt;/code&gt;. It exposes the primitives iOS and Android already provide as React components.&lt;/p&gt;

&lt;p&gt;But you don't have to use Expo UI everywhere. The Expo framework lets you mix different approaches within the same app:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;For custom designs&lt;/strong&gt; — brand pages, bespoke design systems, anything highly styled — use React Native &lt;code&gt;**View**&lt;/code&gt;** / &lt;strong&gt;`&lt;/strong&gt;Text**`. These styling-agnostic primitives work with CSS-style props, flexbox layout, and styling libraries like NativeWind.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;For native-feeling UI&lt;/strong&gt; — settings screens, modals, pickers, sheets, anything users expect to "look like the OS" — use &lt;strong&gt;&lt;a href="https://docs.expo.dev/versions/latest/sdk/ui/" rel="noopener noreferrer"&gt;Expo UI&lt;/a&gt;&lt;/strong&gt;. Inside a &lt;code&gt;Host&lt;/code&gt;, layout uses SwiftUI/Compose primitives (&lt;code&gt;HStack&lt;/code&gt;/&lt;code&gt;VStack&lt;/code&gt;, &lt;code&gt;Row&lt;/code&gt;/&lt;code&gt;Column&lt;/code&gt;) instead of Yoga flexbox.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;For custom graphics&lt;/strong&gt; — charts, shaders, complex animations — use &lt;strong&gt;&lt;a href="https://shopify.github.io/react-native-skia/" rel="noopener noreferrer"&gt;react-native-skia&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;For 3D and GPU work&lt;/strong&gt; — use &lt;strong&gt;&lt;a href="https://github.com/wcandillon/react-native-webgpu" rel="noopener noreferrer"&gt;react-native-webgpu&lt;/a&gt;&lt;/strong&gt; and &lt;strong&gt;&lt;a href="https://docs.swmansion.com/TypeGPU/" rel="noopener noreferrer"&gt;TypeGPU&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;For web ecosystem solutions&lt;/strong&gt; (like &lt;a href="https://ui.shadcn.com/" rel="noopener noreferrer"&gt;shadcn&lt;/a&gt;) — use &lt;a href="https://docs.expo.dev/guides/dom-components/" rel="noopener noreferrer"&gt;DOM components&lt;/a&gt; to run web code in webviews on native and directly on web.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These tools compose at the component level. You can mix React Native, Expo UI, Skia, WebGPU/TypeGPU, and DOM components in the same screen and view tree. Build experiences that are both universal and deeply platform-native.&lt;/p&gt;

&lt;h2&gt;
  
  
  Updated demo apps
&lt;/h2&gt;

&lt;p&gt;We've updated &lt;a href="https://github.com/expo/hot-chocolate" rel="noopener noreferrer"&gt;hot-chocolate&lt;/a&gt; to SDK 56. The app started as SwiftUI-only but now uses universal components and runs on Android and web too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Apple TV and Android TV support
&lt;/h2&gt;

&lt;p&gt;Expo UI now works on Apple TV and Android TV, thanks largely to &lt;a href="https://github.com/douglowder" rel="noopener noreferrer"&gt;Douglas Lowder&lt;/a&gt;. Most components and APIs work on TV platforms, though some native SwiftUI and Compose APIs aren't available on TV. The &lt;a href="https://github.com/react-native-tvos/ExpoUITV" rel="noopener noreferrer"&gt;ExpoUITV&lt;/a&gt; app demonstrates supported APIs on both TV and mobile.&lt;/p&gt;

&lt;h2&gt;
  
  
  Community contributions
&lt;/h2&gt;

&lt;p&gt;Expo UI reached stability because of community involvement. SDK 56 was unusually community-driven. You filed issues, audited APIs, added components, and fixed crashes before we found them. Special thanks to:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/2hwayoung" rel="noopener noreferrer"&gt;2hwayoung&lt;/a&gt;, &lt;a href="https://github.com/akshayjadhav4" rel="noopener noreferrer"&gt;AKSHAY JADHAV&lt;/a&gt;, &lt;a href="https://github.com/axeelz" rel="noopener noreferrer"&gt;Axel&lt;/a&gt;, &lt;a href="https://github.com/benjaminkomen" rel="noopener noreferrer"&gt;Benjamin Komen&lt;/a&gt;, &lt;a href="https://github.com/betomoedano" rel="noopener noreferrer"&gt;Beto&lt;/a&gt;, &lt;a href="https://github.com/cwooldridge1" rel="noopener noreferrer"&gt;Christian Wooldridge&lt;/a&gt;, &lt;a href="https://github.com/morellodev" rel="noopener noreferrer"&gt;Dennis Morello&lt;/a&gt;, &lt;a href="https://github.com/dylancom" rel="noopener noreferrer"&gt;Dylan&lt;/a&gt;, &lt;a href="https://github.com/eliotgevers" rel="noopener noreferrer"&gt;Eliot Gevers&lt;/a&gt;, &lt;a href="https://github.com/fedeciancaglini" rel="noopener noreferrer"&gt;fedeciancaglini&lt;/a&gt;, &lt;a href="https://github.com/hryhoriiK97" rel="noopener noreferrer"&gt;Gregory Moskaliuk&lt;/a&gt;, &lt;a href="https://github.com/huextrat" rel="noopener noreferrer"&gt;Hugo Extrat&lt;/a&gt;, &lt;a href="https://github.com/hypnokermit" rel="noopener noreferrer"&gt;hypnokermit&lt;/a&gt;, &lt;a href="https://github.com/iankberry" rel="noopener noreferrer"&gt;Ian Berry&lt;/a&gt;, &lt;a href="https://github.com/Isaiah-Hamilton" rel="noopener noreferrer"&gt;Isaiah Hamilton&lt;/a&gt;, &lt;a href="https://github.com/Jeroen-G" rel="noopener noreferrer"&gt;JeroenG&lt;/a&gt;, &lt;a href="https://github.com/jossmac" rel="noopener noreferrer"&gt;Joss Mackison&lt;/a&gt;, &lt;a href="https://github.com/dileepapeiris" rel="noopener noreferrer"&gt;K.Dileepa Thushan Peiris&lt;/a&gt;, &lt;a href="https://github.com/kfirfitousi" rel="noopener noreferrer"&gt;Kfir Fitousi&lt;/a&gt;, &lt;a href="https://github.com/kimchi-developer" rel="noopener noreferrer"&gt;kimchi-developer&lt;/a&gt;, &lt;a href="https://github.com/focux" rel="noopener noreferrer"&gt;Leonardo E. Dominguez&lt;/a&gt;, &lt;a href="https://github.com/liestig" rel="noopener noreferrer"&gt;Liès&lt;/a&gt;, &lt;a href="https://github.com/chollier" rel="noopener noreferrer"&gt;Loic CHOLLIER&lt;/a&gt;, &lt;a href="https://github.com/LouisRaverdy" rel="noopener noreferrer"&gt;Louis&lt;/a&gt;, &lt;a href="https://github.com/lucabc2000" rel="noopener noreferrer"&gt;lucabc2000&lt;/a&gt;, &lt;a href="https://github.com/pchalupa" rel="noopener noreferrer"&gt;Petr Chalupa&lt;/a&gt;, &lt;a href="https://github.com/Pflaumenbaum" rel="noopener noreferrer"&gt;Pflaumenbaum&lt;/a&gt;, &lt;a href="https://github.com/ramonclaudio" rel="noopener noreferrer"&gt;Ray&lt;/a&gt;, &lt;a href="https://github.com/sam-shubham" rel="noopener noreferrer"&gt;Sam Shubham&lt;/a&gt;, &lt;a href="https://github.com/shubh73" rel="noopener noreferrer"&gt;Shubh Porwal&lt;/a&gt;, &lt;a href="https://github.com/starsky-nev" rel="noopener noreferrer"&gt;starsky-nev&lt;/a&gt;, &lt;a href="https://github.com/suveshmoza" rel="noopener noreferrer"&gt;Suvesh Moza&lt;/a&gt;, &lt;a href="https://github.com/terijaki" rel="noopener noreferrer"&gt;Terijaki&lt;/a&gt;, &lt;a href="https://github.com/tmallet" rel="noopener noreferrer"&gt;ThiMal&lt;/a&gt;, &lt;a href="https://github.com/yousofabouhalawa" rel="noopener noreferrer"&gt;Yousof Abouhalawa&lt;/a&gt;, &lt;a href="https://github.com/doombladeoff" rel="noopener noreferrer"&gt;Zhovtonizhko Dmitriy&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thanks to everyone who tested, filed issues, joined office hours, or participated in Discord discussions. Your feedback shaped this release as much as the code contributions.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;npx create-expo-app@latest --template default@sdk-56&lt;/code&gt; — Expo UI comes with the default template, so &lt;code&gt;Host&lt;/code&gt;, &lt;code&gt;Switch&lt;/code&gt;, and &lt;code&gt;Picker&lt;/code&gt; are ready to import.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If you have any packages from the drop-in table, try swapping one import to test the replacement.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Check out the demo apps: &lt;a href="https://github.com/expo/hot-chocolate" rel="noopener noreferrer"&gt;hot-chocolate&lt;/a&gt; and &lt;a href="https://github.com/react-native-tvos/ExpoUITV" rel="noopener noreferrer"&gt;ExpoUITV&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Report issues, send PRs, chat with us in &lt;a href="https://chat.expo.dev" rel="noopener noreferrer"&gt;Discord&lt;/a&gt;. Stable means the foundation is solid for the next phase of building.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Expo UI is stable and ready for production. Time to build something native.&lt;/p&gt;

&lt;p&gt;This post is based on content from the &lt;a href="https://expo.dev/blog/expo-ui-stable-sdk-56" 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>javascript</category>
    </item>
    <item>
      <title>Kotlin Compiler Plugin Cuts Android Startup Time by 30% in Expo SDK 56</title>
      <dc:creator>Dan</dc:creator>
      <pubDate>Thu, 18 Jun 2026 18:37:38 +0000</pubDate>
      <link>https://dev.to/expo/kotlin-compiler-plugin-cuts-android-startup-time-by-30-in-expo-sdk-56-2nje</link>
      <guid>https://dev.to/expo/kotlin-compiler-plugin-cuts-android-startup-time-by-30-in-expo-sdk-56-2nje</guid>
      <description>&lt;p&gt;Expo SDK 56 ships with a custom Kotlin compiler plugin that eliminates reflection from Expo Modules on Android. The result: 70% faster module initialization and a 30% reduction in time to first render.&lt;/p&gt;

&lt;p&gt;The plugin runs during compilation, so app developers get these performance gains automatically without changing any code. Module authors can unlock even bigger wins with a single annotation.&lt;/p&gt;

&lt;p&gt;This post walks through how we built it and why this approach succeeded where previous attempts failed. For the Swift side where we now talk to JSI directly, check out our companion post &lt;a href="https://expo.dev/blog/talking-to-jsi-in-swift" rel="noopener noreferrer"&gt;Talking to JSI in Swift&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reflection problem we inherited
&lt;/h2&gt;

&lt;p&gt;Before Expo Modules, we had Unimodules. They worked like old React Native bridge modules: you'd sprinkle annotations across methods you wanted to expose, and the runtime would discover everything through reflection.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ClipboardModule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ExportedModule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;getName&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"ExpoClipboard"&lt;/span&gt;

  &lt;span class="nd"&gt;@ExpoMethod&lt;/span&gt;
  &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;getStringAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;promise&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;clip&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;clipboardManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;primaryClip&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;getItemAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clip&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nd"&gt;@ExpoMethod&lt;/span&gt;
  &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;setStringAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;promise&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;clipboardManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setPrimaryClip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ClipData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newPlainText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reflection made sense when we needed metadata about our own code. &lt;em&gt;What methods does this module export? What arguments do they accept?&lt;/em&gt; The JVM could answer those questions. But reflection costs time, and on Android that time comes straight out of your startup budget. Every module the runtime introspects adds milliseconds before users see your app.&lt;/p&gt;

&lt;p&gt;Building the &lt;a href="https://docs.expo.dev/modules/overview/" rel="noopener noreferrer"&gt;Expo Modules API&lt;/a&gt; gave us a chance to fix this. We wanted better ergonomics and less reflection. The Kotlin DSL delivered both in one move, removing most reflection while making modules easier to write. But we couldn't eliminate all of it. Type information for function arguments and Record properties still required runtime reflection calls like &lt;code&gt;typeOf&amp;lt;T&amp;gt;()&lt;/code&gt; and the metadata parsing that comes with them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where reflection actually hurts
&lt;/h2&gt;

&lt;p&gt;The remaining cost shows up in two places. First, reconstructing type parameters. Our DSL reads argument and return types through &lt;code&gt;typeOf&amp;lt;T&amp;gt;()&lt;/code&gt;, which works because &lt;code&gt;T&lt;/code&gt; is &lt;em&gt;&lt;a href="https://kotlinlang.org/docs/inline-functions.html#reified-type-parameters" rel="noopener noreferrer"&gt;reified&lt;/a&gt;&lt;/em&gt;. The JVM normally erases generics at runtime, so you can't ask what &lt;code&gt;T&lt;/code&gt; actually was. Reified type parameters work around this limitation. The compiler inlines the function and substitutes the real type directly. Getting type information this way is usually cheap, but costs add up when modules have many functions or deeply nested generics.&lt;/p&gt;

&lt;p&gt;The second cost is heavier: Record conversion. A Record represents a typed JS object on the native side. Converting one means discovering its shape at runtime: which properties it declares, which ones are exposed to JS, and what type each property has.&lt;/p&gt;

&lt;p&gt;This discovery process is expensive because it involves multiple layers of reflection. You ask the JVM for the class's &lt;code&gt;memberProperties&lt;/code&gt;, then ask each property for its annotations and type, then make the field accessible for writing. Some of that information isn't even directly available in bytecode. The JVM knows about classes and members, but nothing about Kotlin's type system. The Kotlin reflection library has to reconstruct that by parsing the &lt;code&gt;@Metadata&lt;/code&gt; annotation, which contains a binary blob the compiler generates.&lt;/p&gt;

&lt;p&gt;We could sidestep some of this work. Top-level nullability doesn't need full reflection with a reified &lt;code&gt;T&lt;/code&gt;, a simple &lt;code&gt;null is T&lt;/code&gt; check answers it. But nested cases like the &lt;code&gt;T&lt;/code&gt; in &lt;code&gt;List&amp;lt;T&amp;gt;&lt;/code&gt; are different. The JVM erases generics, so type arguments disappear from bytecode at runtime. It has no concept of Kotlin nullability either. The only place that information survives is the &lt;code&gt;@Metadata&lt;/code&gt; annotation, and there's no shortcut to reading it. You have to parse that metadata, which is exactly the cost we were trying to avoid.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why we skipped code generation
&lt;/h2&gt;

&lt;p&gt;The standard solution for this problem is code generation. Both Java and Kotlin have established tools for it. Annotation processors (kapt) and the Kotlin Symbol Processing API (KSP) run at build time and emit source files that pre-compute type metadata, so you never touch reflection at runtime. We also looked at standalone codegen tools that run before compilation, like React Native's TurboModule generator.&lt;/p&gt;

&lt;p&gt;We tested this approach and didn't like what we found. Generated code becomes part of your project. It appears in call stacks, you step through it in the debugger, and when something breaks in the JS-to-native bridge, you're reading machine output that's painful to debug. Also, kapt and KSP can only add new files, never modify existing ones. Instead of augmenting a Record class in place, you'd generate a parallel class from scratch. Standalone tools just swap those problems for others: another build step, more toolchain integration, more maintenance overhead.&lt;/p&gt;

&lt;p&gt;We were stuck for a while. We lived with the reflection cost and watched for better options.&lt;/p&gt;

&lt;h2&gt;
  
  
  What K2 changed
&lt;/h2&gt;

&lt;p&gt;Kotlin 2.0 shipped with the new K2 compiler and changed what was possible. The add-only limitation of kapt and KSP is exactly what K2 removes. The new compiler plugin API gives you access to the intermediate representation (IR) the compiler produces. You're editing code as the compiler sees it, before it becomes bytecode. If you produce invalid code, the compiler catches it. You can write tests against the transformed IR. Unlike codegen, the result isn't a parallel layer of code you have to maintain. It's small, surgical changes in well-defined places.&lt;/p&gt;

&lt;p&gt;We always knew we could modify bytecode directly, but we didn't want to maintain that. Too fragile, too easy to produce something that only breaks at runtime on specific Android versions. The compiler plugin API gives the same power with actual safety guarantees.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the plugin works
&lt;/h2&gt;

&lt;p&gt;The plugin's approach is simple: everything reflection discovers at runtime, the compiler already knew at build time. Built on the K2 API, the plugin targets the two most expensive operations we described:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Pre-computed type descriptors
&lt;/h3&gt;

&lt;p&gt;When an Expo Module needs type information, it calls &lt;code&gt;typeDescriptorOf&amp;lt;T&amp;gt;()&lt;/code&gt;. The function itself is a stub that throws if it ever runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;typeDescriptorOf&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;PTypeDescriptor&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nc"&gt;NotImplementedError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"typeDescriptorOf&amp;lt;T&amp;gt;() should be replaced by the compiler plugin"&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It exists so code compiles, but should never execute. During compilation, the plugin intercepts every call to &lt;code&gt;typeDescriptorOf&amp;lt;T&amp;gt;()&lt;/code&gt; and replaces it with a direct reference to a pre-computed type descriptor object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// What you write:&lt;/span&gt;
&lt;span class="n"&gt;typeDescriptorOf&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;()&lt;/span&gt;

&lt;span class="c1"&gt;// The equivalent of what the compiler emits:&lt;/span&gt;
&lt;span class="nc"&gt;PTypeDescriptorRegistry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getOrCreateParameterized&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;java&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;isNullable&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;parameters&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;arrayOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;PTypeDescriptorRegistry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getOrCreateConcrete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nc"&gt;Int&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;java&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
      &lt;span class="n"&gt;isNullable&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Think of &lt;code&gt;typeDescriptorOf&amp;lt;T&amp;gt;()&lt;/code&gt; as our own, leaner version of &lt;code&gt;typeOf&amp;lt;T&amp;gt;()&lt;/code&gt;. Both return objects that describe types, but where &lt;code&gt;typeOf&lt;/code&gt; returns a full &lt;code&gt;KType&lt;/code&gt;, ours returns a &lt;code&gt;PTypeDescriptor&lt;/code&gt; (P for Pika, the plugin's internal codename) that carries only what we actually use: a &lt;code&gt;Class&amp;lt;?&amp;gt;&lt;/code&gt; reference, a nullability flag, and a list of parameter descriptors. No dependency on the Kotlin reflection library.&lt;/p&gt;

&lt;p&gt;The lean shape also reduces allocation. For simple types like &lt;code&gt;String&lt;/code&gt; or &lt;code&gt;Int&lt;/code&gt;, the registry returns pre-allocated static fields, so there's no allocation. For parameterized generics, descriptors are cached and deduplicated across modules, so the cost is paid once. In JVM microbenchmarks, building descriptors this way runs roughly 2x faster than &lt;code&gt;typeOf&lt;/code&gt; for complex types like &lt;code&gt;Map&amp;lt;String, List&amp;lt;Int?&amp;gt;&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Pre-computed Record metadata
&lt;/h3&gt;

&lt;p&gt;The fix for reflection-heavy conversion is a single annotation. Mark a Record with &lt;code&gt;@OptimizedRecord&lt;/code&gt; and the plugin takes over:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@OptimizedRecord&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserRecord&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Record&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nd"&gt;@Field&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;
  &lt;span class="nd"&gt;@Field&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;age&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
  &lt;span class="nd"&gt;@Field&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;AddressRecord&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That annotation is the opt-in. For any class marked with &lt;code&gt;@OptimizedRecord&lt;/code&gt;, the plugin does at compile time exactly what SDK 55 did at startup: reads property names, types, and annotations and bakes them into bytecode as plain objects, paired with direct accessors that use simple index-based dispatch. Setting a field goes from "make it accessible via reflection, then set it" to a plain assignment.&lt;/p&gt;

&lt;p&gt;If compiled metadata is present, the runtime takes the fast path. If not (annotation was omitted or plugin didn't run), it falls back to the same reflection-based conversion from SDK 55. Either way, your module keeps working.&lt;/p&gt;

&lt;p&gt;Records aren't the only beneficiary. Jetpack Compose props marked with &lt;code&gt;@OptimizedComposeProps&lt;/code&gt; get the same treatment, applied to prop resolution instead of field conversion. This matters because prop resolution was a major bottleneck for packages like &lt;code&gt;expo-ui&lt;/code&gt; that use Android's declarative UI heavily.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance results
&lt;/h2&gt;

&lt;p&gt;Performance gains depend on how many Expo Modules your app uses and what types they export. We measured cold starts of a module-heavy test app (all official Expo modules plus popular third-party TurboModules) on two devices: a OnePlus 9 Pro and an older Samsung Galaxy S9.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Android module initialization: ~70% faster&lt;/li&gt;
&lt;li&gt;Time to first render: ~30% improvement
&lt;/li&gt;
&lt;li&gt;Record conversion: ~6x faster&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Raw cold-start numbers from our module-heavy test app (clean mean over 150 iterations, outliers removed):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;SDK 55&lt;/th&gt;
&lt;th&gt;SDK 56&lt;/th&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cold launch (Activity.onCreate)&lt;/td&gt;
&lt;td&gt;93 ms&lt;/td&gt;
&lt;td&gt;55 ms&lt;/td&gt;
&lt;td&gt;-41%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time to first render&lt;/td&gt;
&lt;td&gt;797 ms&lt;/td&gt;
&lt;td&gt;508 ms&lt;/td&gt;
&lt;td&gt;-36%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;First animation frame&lt;/td&gt;
&lt;td&gt;808 ms&lt;/td&gt;
&lt;td&gt;520 ms&lt;/td&gt;
&lt;td&gt;-36%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What you need to do
&lt;/h2&gt;

&lt;p&gt;App developers: nothing. The compiler plugin runs automatically in SDK 56. The &lt;code&gt;typeDescriptorOf&lt;/code&gt; replacement applies to all types without code changes.&lt;/p&gt;

&lt;p&gt;Module maintainers who use Records can opt into faster conversion with &lt;code&gt;@OptimizedRecord&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@OptimizedRecord&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyConfig&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Record&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nd"&gt;@Field&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;apiUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;
  &lt;span class="nd"&gt;@Field&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you use props with our Compose integration, annotate with &lt;code&gt;@OptimizedComposeProps&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@OptimizedComposeProps&lt;/span&gt;
&lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;MyViewProps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;MutableState&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mutableStateOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;MutableState&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mutableIntStateOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ComposeProps&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Skipping these annotations doesn't break anything. Modules fall back to the same reflection-based conversion from SDK 55. You just miss out on the 6x speedup for Records.&lt;/p&gt;

&lt;h2&gt;
  
  
  Looking ahead
&lt;/h2&gt;

&lt;p&gt;This isn't the finish line. The compiler plugin currently handles type metadata and Record conversion, but the same approach can extend to other parts of the module lifecycle, like function dispatch. We're also investing in the plugin itself, making it more capable and easier to maintain, so we can expand its scope without expanding maintenance costs. The goal remains the same: keep the ergonomic APIs module authors write today while pushing more work to compile time.&lt;/p&gt;

&lt;p&gt;This post is based on content from the &lt;a href="https://expo.dev/blog/how-a-kotlin-compiler-plugin-cut-android-time-to-first-render" 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>javascript</category>
      <category>reactnative</category>
    </item>
    <item>
      <title>Why You Should Drop @expo/vector-icons for React Native Vector Icons</title>
      <dc:creator>Dan</dc:creator>
      <pubDate>Wed, 17 Jun 2026 20:19:00 +0000</pubDate>
      <link>https://dev.to/expo/why-you-should-drop-expovector-icons-for-react-native-vector-icons-3m1n</link>
      <guid>https://dev.to/expo/why-you-should-drop-expovector-icons-for-react-native-vector-icons-3m1n</guid>
      <description>&lt;p&gt;Switching from &lt;code&gt;@expo/vector-icons&lt;/code&gt; to React Native's official icon packages can cut your bundle size by 4MB or more. Here's how to make the change and why Expo now recommends it.&lt;/p&gt;

&lt;p&gt;If you've built React Native apps with Expo, you've probably used &lt;code&gt;@expo/vector-icons&lt;/code&gt;. It's been the go-to solution for icons, but that's changing. Expo now recommends switching to the official &lt;code&gt;@react-native-vector-icons&lt;/code&gt; packages, and for good reason.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem with @expo/vector-icons
&lt;/h2&gt;

&lt;p&gt;When Expo first created &lt;code&gt;@expo/vector-icons&lt;/code&gt;, it solved a real problem. The original &lt;code&gt;react-native-vector-icons&lt;/code&gt; didn't work well with Expo projects, especially in Expo Go or with over-the-air updates. So Expo built a wrapper that used &lt;code&gt;expo-font&lt;/code&gt; to load icon fonts dynamically.&lt;/p&gt;

&lt;p&gt;But this solution came with baggage. To support libraries that expected &lt;code&gt;react-native-vector-icons&lt;/code&gt;, Expo had to alias it to &lt;code&gt;@expo/vector-icons&lt;/code&gt; using Babel transforms. This created complexity and maintenance overhead.&lt;/p&gt;

&lt;p&gt;More importantly, maintaining icon font packages isn't what Expo does best. They focus on platform-level capabilities, not wrapping third-party icon sets.&lt;/p&gt;

&lt;h2&gt;
  
  
  The New Approach Works Better
&lt;/h2&gt;

&lt;p&gt;The latest &lt;code&gt;@react-native-vector-icons&lt;/code&gt; packages (different from the old deprecated &lt;code&gt;react-native-vector-icons&lt;/code&gt; package) now integrate directly with &lt;code&gt;expo-font&lt;/code&gt;. They call the native font loading API when needed, so they work everywhere: Expo Go, development builds, and production apps.&lt;/p&gt;

&lt;p&gt;Since these packages handle Expo integration natively, there's no need for Expo's wrapper anymore. Expo plans to deprecate &lt;code&gt;@expo/vector-icons&lt;/code&gt; in a future SDK release.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You Get from Migrating
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Smaller bundles&lt;/strong&gt;: Apps often accidentally bundle all icon fonts, even when they only use one or two sets. Our test app shrank by 4MB just by changing imports and dependencies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Latest icon sets&lt;/strong&gt;: Access to newer versions and icon sets that weren't available in &lt;code&gt;@expo/vector-icons&lt;/code&gt;, like &lt;a href="https://www.npmjs.com/package/@react-native-vector-icons/lucide" rel="noopener noreferrer"&gt;Lucide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Better tooling&lt;/strong&gt;: You can now type-check icon names when using &lt;code&gt;createIconSetFromFontello&lt;/code&gt; or &lt;code&gt;createIconSetFromIcoMoon&lt;/code&gt;. Creating custom icon sets is easier with the &lt;a href="https://github.com/oblador/react-native-vector-icons/blob/master/docs/CREATE_FONT_PACKAGE.md" rel="noopener noreferrer"&gt;generator&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cleaner setup&lt;/strong&gt;: No more aliasing configuration or version drift between packages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making the Switch
&lt;/h2&gt;

&lt;p&gt;Expo provides a codemod that handles most of the migration automatically:&lt;/p&gt;

&lt;p&gt;Run &lt;code&gt;npx @react-native-vector-icons/codemod&lt;/code&gt; in your project root. Check the changes it makes before committing.&lt;/p&gt;

&lt;p&gt;Verify the migration worked by running &lt;code&gt;npx expo doctor&lt;/code&gt;. This checks that old packages aren't lingering in your project.&lt;/p&gt;

&lt;p&gt;Make sure &lt;code&gt;expo-font&lt;/code&gt; is installed and configured properly. Don't add font paths from &lt;code&gt;node_modules/@react-native-vector-icons/&lt;/code&gt; to the expo-font config plugin—this will break your build.&lt;/p&gt;

&lt;p&gt;If you use custom fonts, double-check any icons created with &lt;code&gt;createIconSetFromIcoMoon&lt;/code&gt; or similar helpers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Watch Out for These Issues
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Font conflicts&lt;/strong&gt;: If your project mixes old and new icon packages, you might see icons render as &lt;code&gt;?&lt;/code&gt; or empty squares. The &lt;code&gt;expo doctor&lt;/code&gt; command warns about these conflicts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dependency confusion&lt;/strong&gt;: Some libraries might still expect the old package names. Most should work fine, but check your dependencies if you see unexpected behavior.&lt;/p&gt;

&lt;p&gt;About 60% of apps on EAS Build currently use &lt;code&gt;@expo/vector-icons&lt;/code&gt;, so Expo knows this affects many projects. That's why they built the codemod and will maintain the old package during the transition period.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Migration is Worth It
&lt;/h2&gt;

&lt;p&gt;This change reduces complexity in your project and can significantly shrink your bundle size. It's part of Expo's broader effort to simplify the ecosystem while improving performance.&lt;/p&gt;

&lt;p&gt;The new packages give you direct access to the latest icon sets and better development tools. Plus, you're future-proofing your app since Expo will eventually deprecate the old wrapper.&lt;/p&gt;

&lt;p&gt;If you run into problems during migration, Expo wants to hear about them. The smoother this transition goes, the better it is for the entire React Native ecosystem.&lt;/p&gt;

&lt;p&gt;This post is based on content from the &lt;a href="https://expo.dev/blog/moving-away-from-expo-vector-icons" 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>
