<?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>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>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>
    <item>
      <title>Expo's MCP Server is Now Free - Connect Your AI Assistant to Expo Projects</title>
      <dc:creator>Dan</dc:creator>
      <pubDate>Thu, 11 Jun 2026 13:34:29 +0000</pubDate>
      <link>https://dev.to/expo/expos-mcp-server-is-now-free-connect-your-ai-assistant-to-expo-projects-3dd2</link>
      <guid>https://dev.to/expo/expos-mcp-server-is-now-free-connect-your-ai-assistant-to-expo-projects-3dd2</guid>
      <description>&lt;p&gt;The &lt;a href="https://docs.expo.dev/eas/ai/mcp/" rel="noopener noreferrer"&gt;Expo MCP Server&lt;/a&gt; dropped its paywall. Anyone with an Expo account can now hook up their AI coding assistant to Expo docs and tools.&lt;/p&gt;

&lt;p&gt;Previously, you needed a paid plan to use MCP (Model Context Protocol) with Expo. That barrier is gone. Free accounts now include monthly MCP usage for individual development and prototyping.&lt;/p&gt;

&lt;h2&gt;
  
  
  How developers use Expo MCP
&lt;/h2&gt;

&lt;p&gt;MCP is an open standard that connects AI assistants to external tools and project context. We built the Expo MCP Server to give your assistant direct access to Expo's ecosystem. Here's how teams are using it:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Getting Expo answers without tab-switching.&lt;/strong&gt; Your assistant pulls official documentation and SDK guidance while you're deep in code. No more breaking focus to search docs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Debugging builds from chat.&lt;/strong&gt; The assistant can inspect EAS build status, check workflow runs, parse logs, and even pull in TestFlight crash reports or user feedback.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Testing apps through conversation.&lt;/strong&gt; Point it at your local dev server and simulator. It takes screenshots, navigates flows, inspects UI elements, and grabs console logs.&lt;/p&gt;

&lt;p&gt;This workflow was detailed in &lt;a href="https://expo.dev/blog/become-an-ai-native-developer-with-the-expo-mcp-server" rel="noopener noreferrer"&gt;Become an AI-native developer with the Expo MCP Server&lt;/a&gt;. Now it's available to everyone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Free plan limits and usage
&lt;/h2&gt;

&lt;p&gt;Free accounts get monthly MCP usage designed for individual developers, evaluation projects, and occasional Expo-specific help. Usage counts at the billing account level.&lt;/p&gt;

&lt;p&gt;If you're on a team account, all members share the same monthly allowance. Hit the limit and MCP requests fail with an error until the next billing cycle.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://expo.dev/pricing" rel="noopener noreferrer"&gt;Paid plans&lt;/a&gt; include higher usage limits. They also get early access to new MCP features before they roll out to free accounts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting it up
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://docs.expo.dev/eas/ai/mcp/" rel="noopener noreferrer"&gt;MCP docs&lt;/a&gt; walk through connecting your AI assistant to Expo. MCP handles the technical connection. &lt;a href="https://docs.expo.dev/skills/" rel="noopener noreferrer"&gt;Expo Skills&lt;/a&gt; provide the Expo-specific knowledge your assistant needs for building, deploying, and debugging.&lt;/p&gt;

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

&lt;p&gt;We're curious about your MCP use cases. Checking release readiness? Monitoring update rollouts? Turning build failures into actionable fixes? Let us know in &lt;a href="https://chat.expo.dev/" rel="noopener noreferrer"&gt;Discord&lt;/a&gt; or on &lt;a href="https://x.com/expo" rel="noopener noreferrer"&gt;X&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This post is based on content from the &lt;a href="https://expo.dev/changelog/the-expo-mcp-server-is-now-available-on-the-free-plan" 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>reactnative</category>
      <category>mobile</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Expo Router v56 Ships SSR and Breaks Free from React Navigation</title>
      <dc:creator>Dan</dc:creator>
      <pubDate>Wed, 10 Jun 2026 13:15:36 +0000</pubDate>
      <link>https://dev.to/expo/expo-router-v56-ships-ssr-and-breaks-free-from-react-navigation-4pfb</link>
      <guid>https://dev.to/expo/expo-router-v56-ships-ssr-and-breaks-free-from-react-navigation-4pfb</guid>
      <description>&lt;p&gt;Expo Router just made its biggest change since launch. Version 56 forks from React Navigation, adds streaming server-side rendering, and gives Android developers the toolbar API they've been waiting for.&lt;/p&gt;

&lt;p&gt;The fork is the big story here, affecting every Expo Router app. But there's more: new SSR capabilities, an Android toolbar that matches iOS, and expanded Native Tabs. Let's break down what changed and why it matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  The React Navigation fork explained
&lt;/h2&gt;

&lt;p&gt;Expo has deep roots with React Navigation. &lt;a href="https://x.com/notbrent?s=20" rel="noopener noreferrer"&gt;Brent Vatne&lt;/a&gt;, Expo's VP of Engineering, led React Navigation through versions 1.0 and 2.0 alongside &lt;a href="https://github.com/ericvicenti" rel="noopener noreferrer"&gt;@ericvicenti&lt;/a&gt; and &lt;a href="https://github.com/satya164" rel="noopener noreferrer"&gt;@satya164&lt;/a&gt;. We helped React Navigation grow from an early experiment, influenced by React Native's &lt;code&gt;NavigationExperimental&lt;/code&gt; and our own &lt;code&gt;ex-navigation&lt;/code&gt;, into the standard navigation solution for React Native.&lt;/p&gt;

&lt;p&gt;React Navigation remains in Satya's capable hands while our focus shifted to Expo Router. As Expo Router evolved, we needed deeper control over React Navigation's internals. After talking with Satya, we agreed that forking the pieces Expo Router depends on would serve both projects better.&lt;/p&gt;

&lt;p&gt;This isn't about abandoning React Navigation. It's about giving each project room to grow in different directions.&lt;/p&gt;

&lt;p&gt;For Expo Router, the fork opens space for changes specific to file-based routing, web features, and server rendering. Some improvements we want to make wouldn't help React Navigation users directly, and pushing those changes upstream would add unnecessary complexity.&lt;/p&gt;

&lt;p&gt;We can now create a focused version of React Navigation's internals that works specifically for Expo Router. This lets us simplify integration, reduce workaround code, and make maintenance easier.&lt;/p&gt;

&lt;p&gt;There's also a practical benefit: dependency versioning. Projects sometimes ended up with multiple React Navigation versions installed, causing conflicts. Forking the internal pieces gives us better control over compatibility and makes releases more predictable.&lt;/p&gt;

&lt;p&gt;Expo Router's web capabilities like data loaders and server-side rendering need changes that are easier to make in a codebase built specifically for these features.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the fork affects your code
&lt;/h2&gt;

&lt;p&gt;Since Expo Router no longer depends on React Navigation, you can't import code directly from &lt;code&gt;@react-navigation/*&lt;/code&gt; packages. We know this breaks existing code, so we built tools to help with migration.&lt;/p&gt;

&lt;p&gt;Use our codemod to migrate all React Navigation imports to &lt;code&gt;expo-router/react-navigation&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;Our &lt;a href="https://docs.expo.dev/router/migrate/sdk-55-to-56/" rel="noopener noreferrer"&gt;migration guide&lt;/a&gt; walks through manual migration steps. To ease the transition, imports of &lt;code&gt;@react-navigation/core&lt;/code&gt; from libraries will automatically redirect to &lt;code&gt;expo-router/react-navigation&lt;/code&gt; for at least one release cycle.&lt;/p&gt;

&lt;p&gt;If you run into problems, let us know.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next for both projects
&lt;/h2&gt;

&lt;p&gt;We're working with the React Navigation team on a shared API for library authors who need to support both libraries.&lt;/p&gt;

&lt;p&gt;If you maintain a library that should work with both Expo Router and React Navigation, reach out. We'd like to collaborate on adapting it to the new APIs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Custom Suspense fallbacks
&lt;/h2&gt;

&lt;p&gt;You can now customize the Suspense fallback for routes within a &lt;code&gt;_layout&lt;/code&gt;. &lt;a href="https://docs.expo.dev/router/error-handling/#loading-states-with-suspense-fallback" rel="noopener noreferrer"&gt;Export a &lt;/a&gt;&lt;code&gt;[SuspenseFallback](https://docs.expo.dev/router/error-handling/#loading-states-with-suspense-fallback)&lt;/code&gt;&lt;a href="https://docs.expo.dev/router/error-handling/#loading-states-with-suspense-fallback" rel="noopener noreferrer"&gt; to customize the loading screen&lt;/a&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;ActivityIndicator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;View&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-native&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Stack&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;expo-router&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;SuspenseFallback&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;View&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="na"&gt;justifyContent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;center&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;alignItems&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;center&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ActivityIndicator&lt;/span&gt; &lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;large&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="sr"&gt;/View&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="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;Layout&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Stack&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Streaming SSR arrives
&lt;/h2&gt;

&lt;p&gt;Expo Router already supported &lt;a href="https://docs.expo.dev/router/web/api-routes/" rel="noopener noreferrer"&gt;API routes&lt;/a&gt;, &lt;a href="https://docs.expo.dev/router/web/middleware/" rel="noopener noreferrer"&gt;middleware&lt;/a&gt;, &lt;a href="http://docs.expo.dev/router/web/data-loaders/" rel="noopener noreferrer"&gt;data loaders&lt;/a&gt;, and &lt;a href="https://docs.expo.dev/router/web/static-rendering/" rel="noopener noreferrer"&gt;static rendering&lt;/a&gt;. Version 56 adds streaming &lt;a href="https://docs.expo.dev/router/web/server-rendering/" rel="noopener noreferrer"&gt;server-side rendering&lt;/a&gt;, which improves perceived performance by letting you prioritize important UI elements over slower, data-dependent components.&lt;/p&gt;

&lt;p&gt;Enable it by setting &lt;code&gt;unstable_useServerRendering&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt; in your Expo Router config.&lt;/p&gt;

&lt;p&gt;Streaming SSR includes a new way to generate HTML metadata using a &lt;a href="https://docs.expo.dev/router/web/server-rendering/#metadata" rel="noopener noreferrer"&gt;generateMetadata&lt;/a&gt; function.&lt;/p&gt;

&lt;p&gt;We also added helper utilities for type-safe data loaders: &lt;code&gt;[createStaticLoader](https://docs.expo.dev/versions/latest/sdk/server/#createstaticloaderfn)&lt;/code&gt; and &lt;code&gt;[createServerLoader](https://docs.expo.dev/versions/latest/sdk/server/#createserverloaderfn)&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Android gets the toolbar
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://expo.dev/changelog/sdk-55" rel="noopener noreferrer"&gt;SDK 55&lt;/a&gt; brought toolbar support to iOS. Version 56 adds the same feature to Android, achieving cross-platform compatibility. Like iOS, you can place the Android toolbar in three positions: left, right, and bottom.&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%2Fqi8wa3s60ey4ezrzeumw.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%2Fqi8wa3s60ey4ezrzeumw.png" alt="Android toolbar in Expo Router" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;See all available options in the toolbar &lt;a href="https://docs.expo.dev/router/advanced/stack-toolbar/" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Native Tabs expansion
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Last fall, &lt;a href="https://expo.dev/blog/expo-router-v6" rel="noopener noreferrer"&gt;in Router v6&lt;/a&gt; (&lt;em&gt;we changed our Router naming convention since then&lt;/em&gt;), we added Native Tabs support. We're working toward full cross-platform support and plan to mark the API as stable in the next release.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Expo Router v56 adds new options to Native Tabs. The most requested feature was preventing tab selection. You can now use the &lt;code&gt;disabled&lt;/code&gt; property to show a tab in the tab bar while preventing user interaction.&lt;/p&gt;

&lt;p&gt;Check all available options and platform support in the Native Tabs &lt;a href="https://docs.expo.dev/versions/latest/sdk/router/native-tabs/" rel="noopener noreferrer"&gt;reference page&lt;/a&gt;.&lt;/p&gt;

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

&lt;p&gt;Three key takeaways from this release:&lt;/p&gt;

&lt;p&gt;The React Navigation fork requires a one-time import update for every Expo Router app. Our codemod and compatibility layer make migration straightforward.&lt;/p&gt;

&lt;p&gt;Streaming SSR, &lt;code&gt;generateMetadata&lt;/code&gt;, and new loader helpers improve web experiences with Expo Router.&lt;/p&gt;

&lt;p&gt;Android now has toolbar support and expanded Native Tabs, matching iOS feature parity.&lt;/p&gt;

&lt;p&gt;For existing apps, start with the &lt;a href="https://docs.expo.dev/router/migrate/sdk-55-to-56/" rel="noopener noreferrer"&gt;migration guide&lt;/a&gt; and run the codemod. New projects get all these features by default in SDK 56.&lt;/p&gt;

&lt;p&gt;Find us on &lt;a href="https://chat.expo.dev/" rel="noopener noreferrer"&gt;Discord&lt;/a&gt; and &lt;a href="https://github.com/expo/expo" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. We want to hear what you build and when something breaks.&lt;/p&gt;

&lt;p&gt;This post is based on content from the &lt;a href="https://expo.dev/blog/expo-router-v56-decoupling-from-react-navigation" 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>Write Swift and Kotlin directly in your React Native app with Expo SDK 56</title>
      <dc:creator>Dan</dc:creator>
      <pubDate>Tue, 09 Jun 2026 13:19:36 +0000</pubDate>
      <link>https://dev.to/expo/write-swift-and-kotlin-directly-in-your-react-native-app-with-expo-sdk-56-anh</link>
      <guid>https://dev.to/expo/write-swift-and-kotlin-directly-in-your-react-native-app-with-expo-sdk-56-anh</guid>
      <description>&lt;p&gt;Working with native code in React Native usually involves creating separate packages and manually maintaining TypeScript interfaces. Expo SDK 56 changes this by letting you write Swift and Kotlin files right alongside your React components and automatically generating the TypeScript types for you.&lt;/p&gt;

&lt;p&gt;Writing Expo modules traditionally meant dealing with package boilerplate and keeping multiple interfaces in sync. You'd create a module as a standalone package, then manually maintain TypeScript interfaces that matched your native Swift and Kotlin code. This workflow worked but added friction.&lt;/p&gt;

&lt;p&gt;SDK 56 introduces &lt;strong&gt;inline modules&lt;/strong&gt; and the &lt;code&gt;expo-type-information&lt;/code&gt; package to remove these pain points. You can now write native modules directly in your project structure and generate matching TypeScript types automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing native code next to your components
&lt;/h2&gt;

&lt;p&gt;Inline modules let you place Swift and Kotlin files anywhere in your project structure. Need a custom native view? Create &lt;code&gt;NativeView.kt&lt;/code&gt; and &lt;code&gt;NativeView.swift&lt;/code&gt; files right next to your &lt;code&gt;App.tsx&lt;/code&gt; and write your view there.&lt;/p&gt;

&lt;p&gt;Setting up inline modules is simple. In your app configuration file, specify &lt;code&gt;watchedDirectories&lt;/code&gt; - the directories containing your inline modules. After running &lt;code&gt;npx expo prebuild&lt;/code&gt; to sync the native projects, you're ready to go.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"experiments"&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;"inlineModules"&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;"watchedDirectories"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"app"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;"app"&lt;/code&gt; in your &lt;code&gt;watchedDirectories&lt;/code&gt;, you can create Swift and Kotlin files anywhere within the &lt;code&gt;app&lt;/code&gt; directory or its subdirectories. Open &lt;code&gt;app/nested/InlineModule.swift&lt;/code&gt; and write your Expo module:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;internal&lt;/span&gt; &lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="kt"&gt;ExpoModulesCore&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kt"&gt;InlineModule&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Module&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;definition&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;ModuleDefinition&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;Constant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Hello"&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="s"&gt;"Hello iOS inline modules!"&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;Once written, access it from JavaScript using &lt;code&gt;requireNativeModule('InlineModule')&lt;/code&gt;. For views, use &lt;code&gt;requireNativeView('InlineModule')&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automatic TypeScript type generation
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;expo-type-information&lt;/code&gt; package parses your Swift modules and generates matching TypeScript types automatically. This eliminates the manual work of maintaining type interfaces.&lt;/p&gt;

&lt;p&gt;The package includes a CLI tool with commands designed for inline modules.&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%2Fpsg14dkyzuke1grvt2ee.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%2Fpsg14dkyzuke1grvt2ee.png" alt="CLI Tool" width="800" height="233"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;inline-modules-interface&lt;/code&gt; command finds all Swift inline modules in your project and generates two TypeScript files for each one. After running the command, you'll see these files appear next to your Swift file:&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%2Fkt54wkytu34vnq0l9f6r.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%2Fkt54wkytu34vnq0l9f6r.png" alt="InlineModule" width="492" height="180"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;Generated File&lt;/strong&gt; (&lt;code&gt;[ModuleName].generated.ts&lt;/code&gt;) contains all type information about the module, including function declarations, constants, classes, and views. This file gets overwritten every time you run the command.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="c1"&gt;// InlineModule.generated.ts&lt;/span&gt;
&lt;span class="cm"&gt;/*Automatically generated by expo-type-information.*/&lt;/span&gt;

&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kt"&gt;ViewProps&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="n"&gt;react&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;native&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kt"&gt;NativeModule&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="n"&gt;expo&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;export&lt;/span&gt; &lt;span class="n"&gt;declare&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kt"&gt;InlineModuleNativeModuleType&lt;/span&gt; &lt;span class="n"&gt;extends&lt;/span&gt; &lt;span class="kt"&gt;NativeModule&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;Hello&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;strong&gt;Stable File&lt;/strong&gt; (&lt;code&gt;[ModuleName].tsx&lt;/code&gt;) re-exports the module interface and provides a default export for the main view if one exists. You can edit this file and it won't be overwritten.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="c1"&gt;// InlineModule.tsx&lt;/span&gt;
&lt;span class="c1"&gt;// File hash: 8dfc86f5416afbe08cc1ee581c850fc9cec446479211d85501d9a5e2d24cc534&lt;/span&gt;
&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kt"&gt;InlineModuleNativeModuleType&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="o"&gt;./&lt;/span&gt;&lt;span class="kt"&gt;InlineModule&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;generated&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;requireNativeModule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requireNativeView&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="n"&gt;expo&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;InlineModule&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;InlineModuleNativeModuleType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="n"&gt;requireNativeModule&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;InlineModuleNativeModuleType&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="kt"&gt;InlineModule&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;export&lt;/span&gt; &lt;span class="n"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;Hello&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;InlineModule&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;Hello&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This split lets you tweak imperfect generated output while still allowing core declarations to be regenerated when you update the native side.&lt;/p&gt;

&lt;h2&gt;
  
  
  Current limitations
&lt;/h2&gt;

&lt;p&gt;File naming requires that an inline module's name exactly matches its file name. Module names must be globally unique since the name identifies the module in the global object. You can't have both &lt;code&gt;app/InlineView.swift&lt;/code&gt; and &lt;code&gt;src/InlineView.swift&lt;/code&gt; in the same project.&lt;/p&gt;

&lt;p&gt;Type generation currently only works for Swift modules and on macOS.&lt;/p&gt;

&lt;h3&gt;
  
  
  When types can't be resolved
&lt;/h3&gt;

&lt;p&gt;Sometimes the tool can't resolve types of native module declarations. This happens because of &lt;code&gt;SourceKitten&lt;/code&gt; limitations and our decision to parse only provided files instead of doing full compilation. When we can't resolve a Swift type, we generate an &lt;code&gt;unknown&lt;/code&gt; type in TypeScript.&lt;/p&gt;

&lt;p&gt;Common scenarios include nested declarations where &lt;code&gt;SourceKitten&lt;/code&gt; has trouble with deeply nested closures, making it hard to find types inside a &lt;code&gt;Class&lt;/code&gt;. Consider this &lt;code&gt;ExpoBlob&lt;/code&gt; module:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ExpoBlob.swift&lt;/span&gt;
&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="kt"&gt;Foundation&lt;/span&gt;
&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="kt"&gt;ExpoModulesCore&lt;/span&gt;

&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kt"&gt;ExpoBlob&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Module&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;definition&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;ModuleDefinition&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ExpoBlob"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="kt"&gt;Class&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Blob&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kt"&gt;Constructor&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;blobParts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;EitherOfThree&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Blob&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;TypedArray&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;]?,&lt;/span&gt; &lt;span class="nv"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;BlobOptions&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;endings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;endings&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transparent&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;blobPartsProcessed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;processBlobParts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blobParts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;endings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;endings&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kt"&gt;Blob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;blobParts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;blobPartsProcessed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="kt"&gt;BlobOptions&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="kt"&gt;Property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"size"&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="nv"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Blob&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
        &lt;span class="n"&gt;blob&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="kt"&gt;Property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"type"&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="nv"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Blob&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
        &lt;span class="n"&gt;blob&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="kt"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"slice"&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="nv"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Blob&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;?,&lt;/span&gt; &lt;span class="nv"&gt;end&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;?,&lt;/span&gt; &lt;span class="nv"&gt;contentType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;blobSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;blob&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;safeStart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;safeEnd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="n"&gt;blobSize&lt;/span&gt;

        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;relativeStart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;safeStart&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blobSize&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;safeStart&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="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;safeStart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;blobSize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;relativeEnd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;safeEnd&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blobSize&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;safeEnd&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="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;safeEnd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;blobSize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;blob&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;relativeStart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;end&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;relativeEnd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;contentType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;contentType&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="p"&gt;}&lt;/span&gt;

      &lt;span class="kt"&gt;AsyncFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"text"&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="nv"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Blob&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;blob&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="kt"&gt;AsyncFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"bytes"&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="nv"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Blob&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Data&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;blob&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kt"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ExpoBlob.generated.ts&lt;/span&gt;
&lt;span class="cm"&gt;/*Automatically generated by expo-type-information.*/&lt;/span&gt;

&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kt"&gt;ViewProps&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="n"&gt;react&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;native&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kt"&gt;NativeModule&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="n"&gt;expo&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// These types haven't been defined in provided file(s).&lt;/span&gt;
&lt;span class="n"&gt;export&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="kt"&gt;Data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;export&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="kt"&gt;TypedArray&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;export&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="kt"&gt;BlobOptions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nv"&gt;endings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;EndingType&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="n"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;enum&lt;/span&gt; &lt;span class="kt"&gt;EndingType&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;transparent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="n"&gt;transparent&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;native&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="n"&gt;native&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;enum&lt;/span&gt; &lt;span class="kt"&gt;BlobPart&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;blob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="n"&gt;blob&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;export&lt;/span&gt; &lt;span class="n"&gt;declare&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kt"&gt;Blob&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nv"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Blob&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;number&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;end&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;number&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;contentType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;undefined&lt;/span&gt;
  &lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;unknown&lt;/span&gt; &lt;span class="cm"&gt;/*The type couldn't be resolved automatically.*/&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Blob&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Blob&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;Data&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;readonly&lt;/span&gt; &lt;span class="nv"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;unknown&lt;/span&gt; &lt;span class="cm"&gt;/*The type couldn't be resolved automatically.*/&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;readonly&lt;/span&gt; &lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;unknown&lt;/span&gt; &lt;span class="cm"&gt;/*The type couldn't be resolved automatically.*/&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nv"&gt;blobParts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kt"&gt;Blob&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kt"&gt;TypedArray&lt;/span&gt;&lt;span class="p"&gt;)[]&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;BlobOptions&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;undefined&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;export&lt;/span&gt; &lt;span class="n"&gt;declare&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kt"&gt;ExpoBlobNativeModuleType&lt;/span&gt; &lt;span class="n"&gt;extends&lt;/span&gt; &lt;span class="kt"&gt;NativeModule&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kt"&gt;Blob&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;typeof&lt;/span&gt; &lt;span class="kt"&gt;Blob&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;Notice how the return types of &lt;code&gt;slice&lt;/code&gt;, &lt;code&gt;type&lt;/code&gt;, and &lt;code&gt;size&lt;/code&gt; haven't been resolved. You can often fix this by manually annotating the return type of the closure in Swift.&lt;/p&gt;

&lt;p&gt;Imported declarations also cause issues since we only parse provided files and ignore imports. If a function or type from outside influences a return value, the tool may not resolve it.&lt;/p&gt;

&lt;p&gt;The tool struggles with return types if you omit the &lt;code&gt;return&lt;/code&gt; keyword and don't explicitly annotate the closure. To avoid this, annotate DSL declarations or include the &lt;code&gt;return&lt;/code&gt; keyword. Try using &lt;code&gt;--type-inference PREPROCESS_AND_INFERENCE&lt;/code&gt; in the CLI for better return type inference.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works under the hood
&lt;/h2&gt;

&lt;p&gt;Let's examine what happens when you use inline modules. Suppose you've created a module at &lt;code&gt;app/nested/InlineModule.swift&lt;/code&gt; and set &lt;code&gt;watchedDirectories&lt;/code&gt; to include the &lt;code&gt;app&lt;/code&gt; folder.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;expo&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;experiments&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;inlineModules&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;watchedDirectories&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;app&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="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;h3&gt;
  
  
  Prebuild process
&lt;/h3&gt;

&lt;p&gt;Running &lt;code&gt;npx expo prebuild&lt;/code&gt; updates your native projects in two ways:&lt;/p&gt;

&lt;p&gt;The Xcode project gets updated so the &lt;code&gt;app&lt;/code&gt; folder becomes a &lt;strong&gt;file system synchronized group&lt;/strong&gt;. This makes all files in the folder show up in the Xcode editor and get automatically included in the iOS 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%2F4tk405n9ze6gpqo17nqf.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%2F4tk405n9ze6gpqo17nqf.png" alt="file system synchronized group" width="799" height="299"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Project properties for both iOS and Android get updated with your &lt;code&gt;watchedDirectories&lt;/code&gt;. On Android, this is essential for adding files to Android Studio. These properties are used later during autolinking.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Podfile.properties.json&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="s"&gt;"expo.jsEngine"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"hermes"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s"&gt;"EX_DEV_CLIENT_NETWORK_INSPECTOR"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s"&gt;"expo.inlineModules.watchedDirectories"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"[&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;app&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;]"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// gradle.properties&lt;/span&gt;
&lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="n"&gt;expo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inlineModules&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;watchedDirectories&lt;/span&gt;&lt;span class="p"&gt;=[&lt;/span&gt;&lt;span class="s"&gt;"app"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Android project updates
&lt;/h3&gt;

&lt;p&gt;On Android, the project updates during the Gradle configuration phase. This happens when you click &lt;strong&gt;Sync Project with Gradle Files&lt;/strong&gt; in Android Studio or automatically before building the app.&lt;/p&gt;

&lt;p&gt;During this phase, a folder structure gets created that mirrors your &lt;code&gt;watchedDirectories&lt;/code&gt;. Symlinks are created to the Kotlin files inside those directory trees.&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%2Ftqif0wg5wgydxx96wott.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%2Ftqif0wg5wgydxx96wott.png" alt="watchedDirectories" width="800" height="661"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This mirror structure ensures native files get compiled and are visible in Android Studio, while other project files are ignored. JavaScript and TypeScript files won't be indexed by Android Studio since they're not in this mirror directory.&lt;/p&gt;

&lt;h3&gt;
  
  
  Module registration
&lt;/h3&gt;

&lt;p&gt;All Expo modules live on a global object exposed through native &lt;strong&gt;module providers&lt;/strong&gt;. During build on both iOS and Android, a module provider class gets generated containing references to all regular Expo modules and your inline modules. When you call &lt;code&gt;requireNativeModule('InlineModule')&lt;/code&gt; in JavaScript, it accesses this global object.&lt;/p&gt;

&lt;h2&gt;
  
  
  Type generation internals
&lt;/h2&gt;

&lt;p&gt;Type generation works by parsing the native module's structured declaration and generating its TypeScript interface directly from native code. The &lt;code&gt;expo-type-information&lt;/code&gt; package has four main parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Swift parser&lt;/li&gt;
&lt;li&gt;Abstraction over module types
&lt;/li&gt;
&lt;li&gt;TypeScript code generator&lt;/li&gt;
&lt;li&gt;CLI tool&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They work together to automate writing TypeScript interfaces for your modules.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bridging type systems
&lt;/h3&gt;

&lt;p&gt;When defining a module in the DSL, you provide the native interface with functions, constants, classes, and views - all strictly typed in Swift's type system.&lt;/p&gt;

&lt;p&gt;When interfacing with the module from TypeScript, you're working with JavaScript objects under TypeScript's type system. This differs from Swift or Kotlin, and there's no strict one-to-one mapping between them. The conversions between JavaScript and Swift aren't always obvious.&lt;/p&gt;

&lt;p&gt;Expo provides converters for many types, but multiple TypeScript constructs sometimes convert to the same Swift type.&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%2Frp8y7b5pit0mpj3207z1.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%2Frp8y7b5pit0mpj3207z1.png" alt="Type systems" width="800" height="324"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For example, when working with Swift's &lt;code&gt;UIColor&lt;/code&gt;, Expo can convert several JavaScript objects: color strings ('red'), hex strings (&lt;code&gt;#00ffaa00&lt;/code&gt;), or hex numbers (&lt;code&gt;0xff66dd00&lt;/code&gt;). All of these can be type-annotated differently in TypeScript - &lt;code&gt;string&lt;/code&gt;, &lt;code&gt;number&lt;/code&gt; and &lt;code&gt;ColorValue&lt;/code&gt; (from &lt;code&gt;react-native&lt;/code&gt;) all convert to &lt;code&gt;UIColor&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Currently we only support mapping basic types (&lt;code&gt;number&lt;/code&gt;, &lt;code&gt;string&lt;/code&gt;, &lt;code&gt;boolean&lt;/code&gt;, etc.). Check the exact list in the &lt;a href="https://docs.expo.dev/modules/type-generation-reference/" rel="noopener noreferrer"&gt;reference&lt;/a&gt;. We'll continue updating the package with additional type mappings based on converters available in &lt;code&gt;expo-modules-core&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Library components
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;expo-type-information&lt;/code&gt; package consists of several components working together.&lt;/p&gt;

&lt;h4&gt;
  
  
  Swift file parsing
&lt;/h4&gt;

&lt;p&gt;We use &lt;code&gt;SourceKitten&lt;/code&gt; to parse Swift files. &lt;code&gt;SourceKitten&lt;/code&gt; provides structured information about the whole code, letting us parse the Swift DSL, enums, and structs.&lt;/p&gt;

&lt;p&gt;Consider the &lt;code&gt;Hello&lt;/code&gt; constant declaration from &lt;strong&gt;InlineModule.swift&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kt"&gt;Constant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Hello"&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="s"&gt;"Hello iOS inline modules!"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That Swift declaration corresponds to this &lt;code&gt;SourceKitten&lt;/code&gt; output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"key.bodylength"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;56&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"key.bodyoffset"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;124&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"key.kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"source.lang.swift.expr.call"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"key.length"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;66&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"key.name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Constant"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"key.namelength"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"key.nameoffset"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;115&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"key.offset"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;115&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"key.substructure"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"key.bodylength"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"key.bodyoffset"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;124&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"key.kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"source.lang.swift.expr.argument"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"key.length"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"key.offset"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;124&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"key.bodylength"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"key.bodyoffset"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;133&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"key.kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"source.lang.swift.expr.argument"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"key.length"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"key.offset"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;133&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"key.substructure"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"key.bodylength"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;46&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"key.bodyoffset"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;134&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"key.kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"source.lang.swift.expr.closure"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"key.length"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"key.offset"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;133&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"key.substructure"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"key.bodylength"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;46&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"key.bodyoffset"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;134&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"key.kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"source.lang.swift.stmt.brace"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"key.length"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"key.offset"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;133&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;SourceKitten&lt;/code&gt; lets us parse just a single file. This saves time since it doesn't have to compile the whole Xcode project, which is essential when you want to constantly regenerate TypeScript interfaces. But not having access to the whole project means types and functions defined in other files can't be resolved.&lt;/p&gt;

&lt;h4&gt;
  
  
  Type information abstraction
&lt;/h4&gt;

&lt;p&gt;An abstraction layer defines what type information is relevant for Expo modules. The abstraction is agnostic to the underlying native language, so we can add Kotlin support in the future. It's close to the TypeScript type system since it generates TS declarations. Our &lt;code&gt;SourceKitten&lt;/code&gt;-based parser outputs this abstraction.&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="cm"&gt;/**
 * `FileTypeInformation` object abstracts over type related information in a file.
 * The abstraction is closely related to Typescript and expo NativeModules (both to be independent of the actual native side
 * and to give accurate information about what and how we can use the given module).
 * @header TypeInfoTypes
 */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;FileTypeInformation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="cm"&gt;/**
   * @field Set of all type identifiers declared and used in the file.
   */&lt;/span&gt;
  &lt;span class="na"&gt;usedTypeIdentifiers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Set&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="cm"&gt;/**
   * @field Set of all type identifiers declared in the file.
   */&lt;/span&gt;
  &lt;span class="nl"&gt;declaredTypeIdentifiers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Set&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="cm"&gt;/**
   * @field For parametrized types it is the maximum number of parameters this type is used with.
   * This map is useful if we want to infer how many parameters a type declared in other file has.
   *
   * For example if `Set&amp;lt;string&amp;gt;` exists in a file then inferredTypeParametersCount['Set'] == 1.
   * If `Map&amp;lt;number, string&amp;gt;` exists then inferredTypeParametersCount['Map'] == 2.
   * If you use both `SomeParametrizedType&amp;lt;Type1, Type2&amp;gt;` and `SomeParametrizedType&amp;lt;Type3&amp;gt;` then inferredTypeParametersCount['SomeParametrizedType'] == 2.
   */&lt;/span&gt;
  &lt;span class="nl"&gt;inferredTypeParametersCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="cm"&gt;/**
   * @field Maps string identifier to the appropriate declaration object. For now only enum and records identifiers are mapped.
   */&lt;/span&gt;
  &lt;span class="nl"&gt;typeIdentifierDefinitionMap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TypeIdentifierDefinitionMap&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="cm"&gt;/**
   * @field Array of all module classes declared in the given file.
   */&lt;/span&gt;
  &lt;span class="nl"&gt;moduleClasses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ModuleClassDeclaration&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="cm"&gt;/**
   * @field Array of all record classes declared in the given file.
   */&lt;/span&gt;
  &lt;span class="nl"&gt;records&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RecordType&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="cm"&gt;/**
   * @field Array of all enums declared in the given file.
   */&lt;/span&gt;
  &lt;span class="nl"&gt;enums&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;EnumType&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;h4&gt;
  
  
  TypeScript emission
&lt;/h4&gt;

&lt;p&gt;With Expo Modules types abstracted, we generate a TypeScript Abstract Syntax Tree (AST). We build this using the Compiler API with custom wrappers that handle different declaration types - imports, enums, classes, functions, types, interfaces. Once the AST is complete, we format the generated TypeScript with Prettier.&lt;/p&gt;

&lt;h4&gt;
  
  
  CLI interface
&lt;/h4&gt;

&lt;p&gt;The CLI tool sits on top of everything. It exposes commands to work with regular modules, inline modules, and to debug the functions from previous steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next steps
&lt;/h2&gt;

&lt;p&gt;Check out the tutorials for &lt;a href="https://docs.expo.dev/modules/inline-modules-tutorial/" rel="noopener noreferrer"&gt;inline modules&lt;/a&gt; and &lt;a href="https://docs.expo.dev/modules/type-generation-tutorial/" rel="noopener noreferrer"&gt;type generation&lt;/a&gt;, plus the reference pages for &lt;a href="https://docs.expo.dev/modules/inline-modules-reference/" rel="noopener noreferrer"&gt;inline modules&lt;/a&gt; and the &lt;a href="https://docs.expo.dev/modules/type-generation-reference/" rel="noopener noreferrer"&gt;expo-type-information package&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Both features are experimental, so your feedback matters as we continue developing them. File an issue or create a pull request on GitHub, or share your thoughts on Twitter.&lt;/p&gt;

&lt;p&gt;This post is based on content from the &lt;a href="https://expo.dev/blog/native-code-expo-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>reactnative</category>
    </item>
    <item>
      <title>Swift Calls JSI Directly in Expo SDK 56: Removing the Objective-C++ Layer</title>
      <dc:creator>Dan</dc:creator>
      <pubDate>Mon, 08 Jun 2026 20:28:57 +0000</pubDate>
      <link>https://dev.to/expo/swift-calls-jsi-directly-in-expo-sdk-56-removing-the-objective-c-layer-33jc</link>
      <guid>https://dev.to/expo/swift-calls-jsi-directly-in-expo-sdk-56-removing-the-objective-c-layer-33jc</guid>
      <description>&lt;p&gt;SDK 56 makes JavaScript to native calls significantly faster on iOS by letting Swift talk to JSI directly. We eliminated the Objective-C++ layer and saw 1.6-2.3x performance improvements across our benchmarks.&lt;/p&gt;

&lt;p&gt;Before this change, every native module call went through three languages. Now it's just Swift making a direct C++ call. Here's how we did it and what the performance gains look like.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three-language problem
&lt;/h2&gt;

&lt;p&gt;Prior to SDK 56, calling an &lt;a href="https://docs.expo.dev/modules/native-module-tutorial/" rel="noopener noreferrer"&gt;Expo native module&lt;/a&gt; from JavaScript meant crossing multiple language boundaries. Your Swift module code sat behind an Objective-C++ translation layer (&lt;code&gt;EXJavaScriptRuntime&lt;/code&gt;, &lt;code&gt;EXJavaScriptValue&lt;/code&gt;, etc.), which then called into JSI's C++ implementation.&lt;/p&gt;

&lt;p&gt;This architecture existed for one reason: Swift couldn't talk to C++ directly. Objective-C++ was the only practical bridge between them.&lt;/p&gt;

&lt;p&gt;The performance cost was significant. Every call crossed two language boundaries in each direction. Each value got converted twice: &lt;code&gt;std::string&lt;/code&gt; → &lt;code&gt;NSString&lt;/code&gt; → Swift &lt;code&gt;String&lt;/code&gt;, &lt;code&gt;std::vector&lt;/code&gt; → &lt;code&gt;NSArray&lt;/code&gt; → Swift &lt;code&gt;Array&lt;/code&gt;. Each conversion allocated memory and copied data.&lt;/p&gt;

&lt;p&gt;Three different languages in the call path meant three different ways to debug problems. Stack traces changed shape mid-call. Memory management worked differently at each layer. When something went wrong, you had to understand all three languages to fix it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Swift/C++ interop changes the game
&lt;/h2&gt;

&lt;p&gt;Swift historically needed Objective-C as a bridge to reach C++. Any C++ type had to be wrapped in an Objective-C class before Swift could use it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.swift.org/documentation/cxx-interop/" rel="noopener noreferrer"&gt;Swift/C++ interop&lt;/a&gt; (introduced in Swift 5.9) removes this requirement. Swift can &lt;code&gt;import&lt;/code&gt; C++ headers directly. The compiler automatically maps C++ classes and methods onto Swift types you can use naturally.&lt;/p&gt;

&lt;p&gt;The result: what used to be a three-language relay race becomes a single Swift expression that compiles down to a direct C++ call. Performance matches what you'd get writing the call in C++ from the start.&lt;/p&gt;

&lt;p&gt;We're not the first to explore this in React Native. &lt;a href="https://nitro.margelo.com/" rel="noopener noreferrer"&gt;Nitro Modules&lt;/a&gt; pioneered this approach when Swift/C++ interop was even less mature.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building ExpoModulesJSI
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ExpoModulesJSI&lt;/code&gt; is our Swift package that wraps JSI in Swift types. Despite the name, it's purely a JSI wrapper with no Expo-specific code. We could ship it standalone, but JSI only exists in React Native contexts, so the naming stays conservative.&lt;/p&gt;

&lt;p&gt;The type system mirrors JSI exactly: &lt;code&gt;JavaScriptRuntime&lt;/code&gt;, &lt;code&gt;JavaScriptValue&lt;/code&gt;, &lt;code&gt;JavaScriptObject&lt;/code&gt;, &lt;code&gt;JavaScriptArray&lt;/code&gt;, &lt;code&gt;JavaScriptFunction&lt;/code&gt;, etc. Each maps to its JSI equivalent but with a modern Swift API.&lt;/p&gt;

&lt;p&gt;We preserve JSI's ownership model using non-copyable types. JSI's value types like &lt;code&gt;jsi::Value&lt;/code&gt; and &lt;code&gt;jsi::Object&lt;/code&gt; own runtime resources and follow move-only semantics. Swift 5.9's &lt;code&gt;~Copyable&lt;/code&gt; protocol lets us mirror this behavior. The Swift compiler enforces the same single-owner rules that JSI expects underneath.&lt;/p&gt;

&lt;p&gt;The package builds as a SwiftPM package with C++ interop enabled, then gets bundled into an xcframework. Most React Native projects use CocoaPods, so we also provide a podspec that wraps the prebuilt binary. The podspec creates a stub xcframework at &lt;code&gt;pod install&lt;/code&gt; time, then a build script runs the real SwiftPM build with content-hash caching.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling different concurrency models
&lt;/h2&gt;

&lt;p&gt;React Native's threading predates Swift Concurrency. JavaScript runs on a dedicated thread with a run loop. Native work uses &lt;code&gt;dispatch_queue_t&lt;/code&gt;s and callbacks. No actors, no &lt;code&gt;await&lt;/code&gt; points, just queues and blocks with thread-switching contracts.&lt;/p&gt;

&lt;p&gt;We wanted our Swift API to use modern Swift: &lt;code&gt;async&lt;/code&gt;/&lt;code&gt;await&lt;/code&gt;, structured concurrency, actor isolation where appropriate. This required building a bridge between Swift Concurrency and React Native's callback world without breaking either system's invariants.&lt;/p&gt;

&lt;p&gt;The boundary layer handles most of this work. We'll skip the implementation details here since they could fill another post and the design is still evolving under production load.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation challenges
&lt;/h2&gt;

&lt;p&gt;Swift/C++ interop is experimental and comes with compilation costs. Here are the main issues we encountered:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Experimental status.&lt;/strong&gt; Years after Swift 5.9, C++ interop remains opt-in and officially experimental. APIs and behavior can change between Swift versions. Not a blocker for us, but worth knowing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Capability gaps.&lt;/strong&gt; Swift and C++ have different memory models. ARC and value semantics versus manual lifetime management and raw pointers. Complex template metaprogramming and some inheritance patterns have no clean Swift mapping. Some gaps will close with tooling improvements; others are conceptually unbridgeable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compilation performance.&lt;/strong&gt; Enabling C++ interop adds noticeable compile time per file. It also spreads: any module importing an interop-enabled module must enable interop too. We solve this by shipping prebuilt xcframeworks. Apps link against binaries instead of recompiling interop sources, and downstream modules see a regular Swift library.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Generated headers.&lt;/strong&gt; Swift emits a C++ header exposing all public symbols to C++. This gets large quickly and sometimes emits declarations in the wrong order. There's an undocumented flag &lt;code&gt;-clang-header-expose-decls=has-expose-attr&lt;/code&gt; that restricts the header to explicitly annotated declarations. It's mentioned only in &lt;code&gt;FrontendOptions.td&lt;/code&gt; in the Swift compiler source.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Third-party type annotations.&lt;/strong&gt; Swift imports C++ classes as value types by default, but types with virtual methods need reference semantics. For code we control, Swift provides macros like &lt;code&gt;SWIFT_SHARED_REFERENCE&lt;/code&gt;. For third-party headers like JSI, we use Clang's &lt;a href="https://clang.llvm.org/docs/APINotes.html" rel="noopener noreferrer"&gt;APINotes&lt;/a&gt; - YAML files that add import attributes without modifying the original headers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Exception handling.&lt;/strong&gt; C++ exceptions don't cross into Swift. Swift assumes imported C++ functions don't throw unless proven otherwise. When JSI methods like &lt;code&gt;evaluateJavaScript&lt;/code&gt; throw &lt;code&gt;jsi::JSError&lt;/code&gt;, the exception crashes the app if it reaches Swift frames. We built a bridge that catches C++ exceptions, stores them in thread-local storage, and rethrows them as Swift errors after each call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance benchmarks
&lt;/h2&gt;

&lt;p&gt;Our goal was simple: don't sacrifice performance for better Swift APIs. Turbo Modules set the bar for modern React Native native modules, and we wanted to match that performance while providing superior ergonomics.&lt;/p&gt;

&lt;h3&gt;
  
  
  Methodology
&lt;/h3&gt;

&lt;p&gt;We tested four micro-benchmarks across three native module architectures on two SDK versions. Each benchmark ran 100,000 iterations, averaged across three trials on an iPhone 16 Pro release build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sync no-op function&lt;/li&gt;
&lt;li&gt;Adding two numbers (0 + 1)
&lt;/li&gt;
&lt;li&gt;String concatenation ('hello' + 'world')&lt;/li&gt;
&lt;li&gt;Async no-op function&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The architectures tested were Expo Modules, React Native Turbo Modules, and the legacy Bridge. We used trivial inputs intentionally - these measure boundary crossing costs, not computation costs.&lt;/p&gt;

&lt;p&gt;Note on async testing: Expo Modules use Swift Concurrency (&lt;code&gt;async&lt;/code&gt;/&lt;code&gt;await&lt;/code&gt;), which requires more work per call than callback-style async (Task creation, continuations, scheduler interaction). Turbo Modules and Bridge use callbacks. This compares the same logical operation done idiomatically in each system.&lt;/p&gt;

&lt;h3&gt;
  
  
  Results
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;CODE_BLOCK_N&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Expo Modules became 1.6-2.3x faster across all benchmarks. The improvements match our architectural changes: boundary costs dominated the no-op test, marshaling costs affected strings most, and async showed the largest absolute gains due to removed overhead.&lt;/p&gt;

&lt;p&gt;Before SDK 56, Expo Modules trailed Turbo Modules on every test. After the rewrite, we match Turbo Modules on simple sync calls and lead by 55% on async operations. The async advantage matters most in real apps where promises chain across module boundaries.&lt;/p&gt;

&lt;p&gt;Turbo Modules also improved between SDK 55 and 56 from upstream React Native changes, so we were catching up to a moving target.&lt;/p&gt;

&lt;p&gt;The Bridge results show the old story: 3-4x slower on sync operations due to JSON serialization overhead. The async gap narrows to 1.6x because Promise allocation and scheduling costs affect all architectures similarly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Limitations
&lt;/h3&gt;

&lt;p&gt;These micro-benchmarks measure boundary crossing costs. Real app performance depends on call frequency, payload size, and actual work being done. Device differences, OS versions, and Hermes builds will shift absolute numbers, but the performance ratios should remain consistent.&lt;/p&gt;

&lt;h2&gt;
  
  
  What comes next
&lt;/h2&gt;

&lt;p&gt;Removing the Objective-C++ layer makes previously difficult features straightforward to implement. It also opens up performance optimizations that are now practical with a single-language call path.&lt;/p&gt;

&lt;p&gt;This rewrite provides the foundation for the next round of API improvements we're planning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using SDK 56 native modules
&lt;/h2&gt;

&lt;p&gt;SDK 56 ships the new native module architecture on iOS, tvOS, and macOS. Check the &lt;a href="https://expo.dev/changelog/sdk-56" rel="noopener noreferrer"&gt;SDK 56 release notes&lt;/a&gt; for complete details. The &lt;code&gt;expo-modules-jsi&lt;/code&gt; package is available on &lt;a href="https://github.com/expo/expo/tree/main/packages/expo-modules-jsi" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; for bug reports, feature requests, and contributions.&lt;/p&gt;

&lt;p&gt;Android takes a different approach in SDK 56. The major win there is our Kotlin compiler plugin, which moves more work to compile time and delivers larger performance gains than a JSI rewrite would provide. We may explore a Kotlin-first JSI wrapper eventually, but Android's JSI performance was already in better shape.&lt;/p&gt;

&lt;p&gt;One final note: AI significantly accelerated this rewrite. It covered almost the entire JSI C++ surface in Swift and pushed test coverage to nearly 90%. Doing this work manually would have taken much longer.&lt;/p&gt;

&lt;p&gt;This post is based on content from the &lt;a href="https://expo.dev/blog/talking-to-jsi-in-swift" 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>
  </channel>
</rss>
