<?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>Skip the manual PostHog setup: EAS wires it into your Expo app for you</title>
      <dc:creator>Dan</dc:creator>
      <pubDate>Fri, 11 Sep 2026 12:43:27 +0000</pubDate>
      <link>https://dev.to/expo/skip-the-manual-posthog-setup-eas-wires-it-into-your-expo-app-for-you-28ji</link>
      <guid>https://dev.to/expo/skip-the-manual-posthog-setup-eas-wires-it-into-your-expo-app-for-you-28ji</guid>
      <description>&lt;p&gt;You know the drill. Sign up for PostHog, create an org, create a project, decide which of the four SDK packages you actually need this time, hook up the config plugin, copy the project key out of one browser tab and paste it into &lt;code&gt;.env.local&lt;/code&gt; in another, then repeat that last step for every EAS environment so your team and your CI can see the same data you do. By the time analytics is actually capturing events, you've spent half an hour on plumbing.&lt;/p&gt;

&lt;p&gt;That setup is now one command. It creates your &lt;a href="https://posthog.com" rel="noopener noreferrer"&gt;PostHog&lt;/a&gt; org and project, installs the SDK, adds the config plugin, and writes your keys into &lt;code&gt;.env.local&lt;/code&gt; and your &lt;a href="https://expo.dev/eas" rel="noopener noreferrer"&gt;EAS&lt;/a&gt; environment variables for Production, Preview, and Development. You pick the data region and the features you want, and it's done. If you already have a PostHog account, it opens your browser once to link the existing org and project, then runs the same setup against it.&lt;/p&gt;

&lt;p&gt;That's the news. The more interesting part is what the integration does once it's installed: every PostHog event from your app now carries the release it came from, and your &lt;a href="https://docs.expo.dev/eas/workflows/get-started/" rel="noopener noreferrer"&gt;EAS Workflows&lt;/a&gt; can read PostHog metrics and decide what happens next.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the integration actually sets up
&lt;/h2&gt;

&lt;p&gt;Product analytics tells you what a user did. On its own, it doesn't tell you which build, channel, or update they were running when they did it. That's the gap this integration closes, and it does it with almost no setup on your part.&lt;/p&gt;

&lt;p&gt;A few lines in your app register &lt;code&gt;eas/update_id&lt;/code&gt;, &lt;code&gt;eas/channel&lt;/code&gt;, and &lt;code&gt;eas/runtime_version&lt;/code&gt; as super properties. Once that's in place, every &lt;code&gt;capture()&lt;/code&gt; call carries them automatically. Your workflows speak the same language: the native &lt;code&gt;posthog_capture_event&lt;/code&gt; function takes the same &lt;code&gt;eas/&lt;/code&gt; names, filled in from contexts like &lt;code&gt;$${{ app.id }}&lt;/code&gt; or job outputs like &lt;code&gt;build_id&lt;/code&gt;, so an event fired from CI looks exactly like one fired from a phone in someone's pocket.&lt;/p&gt;

&lt;p&gt;PostHog recognizes these property names on its own. It shows the &lt;a href="https://expo.dev" rel="noopener noreferrer"&gt;Expo&lt;/a&gt; logo next to them in the UI and turns the values into links back to your Expo dashboard. Click an &lt;code&gt;eas/update_id&lt;/code&gt; inside a PostHog event and you land directly on that update, no copy-pasting IDs between tabs.&lt;/p&gt;

&lt;p&gt;That changes the shape of a debugging session. A funnel drops on Tuesday: you break it down by update ID, find the update where it started, and open it, all from inside PostHog. A crash spike, a slow screen, a retention dip right after a channel rollout, it's the same three clicks every time instead of matching timestamps by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rollouts that watch their own metrics
&lt;/h2&gt;

&lt;p&gt;This is where it stops being "analytics with better tags" and turns into something that changes what release day looks like.&lt;/p&gt;

&lt;p&gt;An &lt;a href="https://docs.expo.dev/eas/workflows/get-started/" rel="noopener noreferrer"&gt;EAS Workflow&lt;/a&gt; is a chain of jobs, and PostHog now has native functions you can drop into that chain. &lt;code&gt;posthog_flag_rollout&lt;/code&gt; sets a feature flag to a given percentage. &lt;code&gt;posthog_wait_for_metric&lt;/code&gt; runs a HogQL query on an interval and holds the job open until the result clears your threshold, or fails it out on a timeout. &lt;code&gt;posthog_wait_for_query&lt;/code&gt; is the simpler yes/no version: it clears the moment the query returns true. Put one of these next to your &lt;a href="https://docs.expo.dev/eas-update/introduction/" rel="noopener noreferrer"&gt;EAS Update&lt;/a&gt; or &lt;a href="https://docs.expo.dev/build/introduction/" rel="noopener noreferrer"&gt;EAS Build&lt;/a&gt; job, and the workflow stops being fire-and-forget. It ships the change, watches production, and picks the next step based on what it sees.&lt;/p&gt;

&lt;p&gt;Feature flags aren't the only lever here. The same pattern works for channel rollouts: publish an update to 10 percent of a channel, watch the error rate for a window, then either widen it to everyone or revert. The recipe also logs how close each rollout got to its threshold as a PostHog event, so after a handful of releases you end up with a chart of your own rollout history, for free.&lt;/p&gt;

&lt;p&gt;A few things fall out of these primitives once you start combining them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A kill switch that flips a flag off the second errors spike, on a schedule, with nobody paged.&lt;/li&gt;
&lt;li&gt;A nightly error budget check that stays quiet until you've actually spent the budget.&lt;/li&gt;
&lt;li&gt;A submission job that drops an annotation on your PostHog timeline, so every deploy shows up on the charts your team already has open.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;posthog_upload_sourcemaps&lt;/code&gt; wired into the build job, so stack traces in PostHog's error tracking point at your TypeScript instead of a minified bundle.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to set it up
&lt;/h2&gt;

&lt;p&gt;Run &lt;code&gt;eas integrations:posthog:connect&lt;/code&gt; in a project that's linked to EAS. Choose a region, choose the features you want enabled, and let it finish. Then wrap your app in &lt;code&gt;&amp;lt;PostHogProvider&amp;gt;&lt;/code&gt;, add the release properties, and cut a development build. Fire one test event and check &lt;code&gt;eas integrations:posthog:dashboard&lt;/code&gt; to confirm it landed in PostHog.&lt;/p&gt;

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

&lt;p&gt;Product analytics itself works fine inside &lt;a href="https://expo.dev/go" rel="noopener noreferrer"&gt;Expo Go&lt;/a&gt;. Session replay and crash symbolication need a development build, and release tracking specifically only kicks in once your app is actually running a real update, since that's where the &lt;code&gt;eas/&lt;/code&gt; properties come from.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to go from here
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Start with &lt;a href="https://docs.expo.dev/guides/using-posthog/" rel="noopener noreferrer"&gt;Using PostHog&lt;/a&gt; for the full setup walkthrough.&lt;/li&gt;
&lt;li&gt;Browse the &lt;a href="https://docs.expo.dev/guides/using-posthog/recipes/" rel="noopener noreferrer"&gt;recipes&lt;/a&gt; for over 10 ready-made workflows you can drop into &lt;code&gt;.eas/workflows/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Check the &lt;a href="https://docs.expo.dev/eas/workflows/syntax/#easposthog_capture_event" rel="noopener noreferrer"&gt;native function reference&lt;/a&gt; if you want to wire PostHog into a workflow you already have.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This post is based on content from the &lt;a href="https://expo.dev/changelog/posthog-integration" 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>I tried to live an entire day inside apps built with Expo. Here's where it broke.</title>
      <dc:creator>Dan</dc:creator>
      <pubDate>Thu, 10 Sep 2026 22:07:28 +0000</pubDate>
      <link>https://dev.to/expo/i-tried-to-live-an-entire-day-inside-apps-built-with-expo-heres-where-it-broke-j5d</link>
      <guid>https://dev.to/expo/i-tried-to-live-an-entire-day-inside-apps-built-with-expo-heres-where-it-broke-j5d</guid>
      <description>&lt;p&gt;There are thousands of apps on the App Store and Google Play running on &lt;a href="https://expo.dev" rel="noopener noreferrer"&gt;Expo&lt;/a&gt;'s open source tooling. That's a big number, but it doesn't tell you much. What I actually wanted to know: do those apps cover a full day, start to finish, without me reaching for anything else on my phone?&lt;/p&gt;

&lt;p&gt;So I ran the experiment. One day, Expo apps only. By breakfast I'd paid a celebrity to record a video. By lunch I was generating an app from my phone. By late afternoon I was sweating through a shirt. By bedtime I was too tired to feel smug about any of it.&lt;/p&gt;

&lt;p&gt;Short answer: yes, it works. The longer answer is more useful, because the moments where the day fell apart are exactly the moments where somebody should go build something.&lt;/p&gt;

&lt;h2&gt;
  
  
  Morning
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://apps.apple.com/us/app/rise-sleep-tracker/id1453884781" rel="noopener noreferrer"&gt;Rise&lt;/a&gt;, sleep tracking and energy forecasts&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://apps.apple.com/us/app/espresso-world-news-in-a-shot/id896628003" rel="noopener noreferrer"&gt;Espresso&lt;/a&gt;, news from The Economist&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://apps.apple.com/us/app/weatherwise-app/id6736407724" rel="noopener noreferrer"&gt;WeatherWise&lt;/a&gt;, forecast and live radar&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://apps.apple.com/us/app/epa-airnow/id467653238" rel="noopener noreferrer"&gt;AirNow&lt;/a&gt;, air quality index from the US EPA&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://apps.apple.com/us/app/delish-original-fun-recipes/id6747781542" rel="noopener noreferrer"&gt;Delish&lt;/a&gt;, recipes with video steps&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;First app of the day was Rise, a sleep tracker that also predicts when you'll get energy spikes and when your body wants to wind down. Genuinely useful for planning a day this ridiculous.&lt;/p&gt;

&lt;p&gt;Next, Espresso, from The Economist. News in small, fast hits, which is the whole premise. Content-heavy apps like this have always been a strong spot for Expo, and it shows: an app like this lives on navigation and linking, which is exactly the job &lt;a href="https://expo.dev/router" rel="noopener noreferrer"&gt;Expo Router&lt;/a&gt; is built for.&lt;/p&gt;

&lt;p&gt;I needed to know if the weather and air quality would cooperate with my afternoon plans. WeatherWise covered the forecast and live radar, feeling like the pro version of the weather app that already ships on your phone. For air quality I used AirNow, from the US EPA. It gives you the current index and the outlook for the week. The UI could use some attention, and I'll say that plainly, but it works, and there's something quietly great about a federal agency reaching for Expo to help people figure out if it's safe to go outside during wildfire season.&lt;/p&gt;

&lt;p&gt;I'm the cook in my house, so I opened Delish for dinner ideas. Hand-picked recipes with real videos and step-by-step instructions, no eight-paragraph story about someone's grandmother sitting on top of the ingredient list. Do not browse it hungry.&lt;/p&gt;

&lt;h2&gt;
  
  
  The messaging gap
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://apps.apple.com/us/app/cameo-personal-celeb-videos/id1258311581" rel="noopener noreferrer"&gt;Cameo&lt;/a&gt;, personalized videos from celebrities&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Normally around here I'd text my wife what I'm making for dinner. This is where the day fell apart, because there isn't really a messaging app built with Expo. We use iMessage, and iMessage was off the table.&lt;/p&gt;

&lt;p&gt;The problem with messaging apps is that shipping one isn't enough. Both sides of the conversation need it. I can install an app for myself, but I can't install one on my wife's expectations about how she wants to be reached. That's a real gap in what's out there.&lt;/p&gt;

&lt;p&gt;So I got someone else to text her instead. Cameo runs on Expo, and their team has a &lt;a href="https://expo.dev/customers/cameo" rel="noopener noreferrer"&gt;case study&lt;/a&gt; about scaling through a stretch of hyper-growth on it. Cameo is the app where you pay a celebrity to record a personal video for someone. My wife and I watch a lot of Food Network, so I tracked down a Food Network star who could deliver same-day. Was this the intended use case? No. Was it cheap? Also no.&lt;/p&gt;

&lt;h2&gt;
  
  
  Afternoon
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://apps.apple.com/us/app/tesla/id582007913" rel="noopener noreferrer"&gt;Tesla&lt;/a&gt;, navigation and car controls&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://apps.apple.com/us/app/tommys-express/id1084713029" rel="noopener noreferrer"&gt;Tommy's Express&lt;/a&gt;, drive-through car wash&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://apps.apple.com/us/app/replit-vibe-code-apps/id1614022293" rel="noopener noreferrer"&gt;Replit&lt;/a&gt;, coding with a built-in agent&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://apps.apple.com/us/app/mercari-buying-selling-app/id896130944" rel="noopener noreferrer"&gt;Mercari&lt;/a&gt;, second-hand marketplace&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I found a car wash running on Expo, then realized I had no way to get there since there's no Expo maps app either. The Tesla app has navigation built in and also controls the car itself, which solved two problems with one tap.&lt;/p&gt;

&lt;p&gt;Once I got there, the Tommy's Express app let me use the car wash without talking to another human. Pull into the lane the app tells you, it reads your plate, it already has your plan and card on file, gate opens. If you're an introvert, this is close to a religious experience.&lt;/p&gt;

&lt;p&gt;Then I set up at a cafe to get some actual work done, using the Replit app. A few people in our YouTube comments have asked for a mobile app to check EAS builds and workflows on the go, and this felt like the right day to start one. Replit has a coding agent built in, so I had it generate a first draft.&lt;/p&gt;

&lt;p&gt;I also spent some time in Mercari, a second-hand marketplace where people sell the good stuff out of their closets. My feed that day was, for reasons I can't explain, an oops-all-hats situation. Found one that's extremely Silicon Valley coded, so I might go back for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Evening
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://apps.apple.com/us/app/runna-running-plans-coach/id1594204443" rel="noopener noreferrer"&gt;Runna&lt;/a&gt;, personalized running plans&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://apps.apple.com/us/app/rover-pet-sitters/id547320928" rel="noopener noreferrer"&gt;Rover&lt;/a&gt;, pet sitting and dog walking&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://apps.apple.com/us/app/once-upon-photo-book-creator/id1187208815" rel="noopener noreferrer"&gt;Once Upon&lt;/a&gt;, printed photo books&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://apps.apple.com/us/app/voidpet-garden-mental-health/id1668932264" rel="noopener noreferrer"&gt;Voidpet Garden&lt;/a&gt;, self-reflection as a game&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://apps.apple.com/us/app/book-of-the-month/id1473873226" rel="noopener noreferrer"&gt;Book of the Month&lt;/a&gt;, a short list of books to read&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I have a race coming up this fall, so I used Runna to track a training run. Tell it a bit about yourself and it writes a personalized plan, whether you're training for your first 5K or something longer. This is where the sweating happened.&lt;/p&gt;

&lt;p&gt;After dinner I walked the dog, which reminded me we need a sitter for an upcoming trip. Rover is a genuinely nice app for finding people to look after your pet. When I lived in New York City, finding boarding with actual availability was hard. The sitters I found through Rover were kind enough to my dog that I'd book them again without thinking twice.&lt;/p&gt;

&lt;p&gt;I also used Once Upon to turn photos out of my library into a printed book. It has short videos showing what hardcover versus softcover and matte versus glossy actually look like, a detail most apps skip entirely, then let me drag photos around until the layout felt right.&lt;/p&gt;

&lt;p&gt;To unwind, I played Voidpet Garden, made by Ben Awad. It turns self-reflection into a game: you catch creatures, tend a garden, fight the occasional battle, and along the way it asks how your day went and how things made you feel. We could use more of that.&lt;/p&gt;

&lt;p&gt;Last thing before bed was the Book of the Month Club app. Walking into a bookstore means facing a few thousand choices at once. This trims it to five or seven. I picked one called The Last Contract of Esequibo, mostly for the tagline: "Live by the code or die by the knife." Felt about right for development life these days.&lt;/p&gt;

&lt;h2&gt;
  
  
  Loose ends
&lt;/h2&gt;

&lt;p&gt;Three things resolved after the day was already over.&lt;/p&gt;

&lt;p&gt;The Cameo video didn't arrive that evening. Celebrities, it turns out, have schedules. It showed up a few days later, we loved it, and now I owe my wife that dinner all over again.&lt;/p&gt;

&lt;p&gt;The photo book arrived, and it's genuinely nice.&lt;/p&gt;

&lt;p&gt;And the EAS companion app I started in Replit made real progress. I showed it around internally, more people got excited than I expected, and we're now actively working on it. Expect something to look at later this year.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the gaps tell you
&lt;/h2&gt;

&lt;p&gt;The three places the day broke were messaging, maps, and camera, and they're really the same gap wearing different clothes.&lt;/p&gt;

&lt;p&gt;Messaging is hard because it needs two people to adopt it at once. Maps and camera are hard for the opposite reason: your phone already ships with a good-enough version, and good-enough is genuinely difficult to beat. Which is exactly why these categories are worth building into.&lt;/p&gt;

&lt;p&gt;One of the goals at Expo is letting more people build for mobile. The screen in your pocket is the one most of us stare at all day, for better or worse, and everyone should be able to build for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start something
&lt;/h2&gt;

&lt;p&gt;If one of those gaps sounds like your idea, here's the whole path in: &lt;a href="https://docs.expo.dev/get-started/introduction/" rel="noopener noreferrer"&gt;the getting started guide&lt;/a&gt; walks you to a build on a real device, and &lt;a href="https://expo.dev/eas" rel="noopener noreferrer"&gt;EAS Build&lt;/a&gt; takes it to the app stores without you ever opening Xcode or Android Studio. If you get stuck along the way, come find us in &lt;a href="https://chat.expo.dev" rel="noopener noreferrer"&gt;Discord&lt;/a&gt;.&lt;/p&gt;

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

</description>
      <category>expo</category>
      <category>mobile</category>
      <category>javascript</category>
    </item>
    <item>
      <title>12 AEO Best Practices for docs</title>
      <dc:creator>Dan</dc:creator>
      <pubDate>Wed, 09 Sep 2026 18:06:50 +0000</pubDate>
      <link>https://dev.to/expo/aeo-best-practices-for-docs-32j4</link>
      <guid>https://dev.to/expo/aeo-best-practices-for-docs-32j4</guid>
      <description>&lt;p&gt;If you've watched a coding agent quietly read your documentation, run a command based on what it found there, and never once render the page in a browser, you've seen the audience this post is about. Nobody clicked a link. Nobody looked at your carefully designed nav bar. An agent fetched a URL, parsed it, and acted.&lt;/p&gt;

&lt;p&gt;In January 2025, we made a small change to &lt;a href="https://docs.expo.dev/" rel="noopener noreferrer"&gt;Expo documentation&lt;/a&gt; that turned out to matter more than we expected at the time: we added support for a curated text file served publicly, called &lt;code&gt;llms.txt&lt;/code&gt;. Back then, adding &lt;code&gt;llms.txt&lt;/code&gt; to a site was a fringe proposal. Now we know it helps AI agents fetch content and search through documentation while doing a task.&lt;/p&gt;

&lt;p&gt;Everything since that pull request has been us reacting to a new kind of reader: AI coding assistants and agents that write code, run tests, search documentation, and pick the right method or library for the job. In short: agents are now building a good chunk of the React Native and Expo apps out there. This post walks through the practices we've put in place on Expo docs to make that work reliably.&lt;/p&gt;

&lt;h2&gt;
  
  
  What AEO actually means
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Answer Engine Optimization (AEO)&lt;/strong&gt; gets thrown around loosely, so it's worth being precise about it before any of the practices below make sense. An &lt;strong&gt;answer engine&lt;/strong&gt; is a system that answers a question directly: ChatGPT, Claude, Perplexity, Google's AI Overviews, or a coding agent running in your terminal. AEO is about shaping your content so these systems can retrieve it and reproduce it correctly.&lt;/p&gt;

&lt;p&gt;The end user is still a human, either automating a task or triggering it through a coding agent. But your documentation reaches them through two very different paths, and which path an agent takes changes how it responds.&lt;/p&gt;

&lt;p&gt;The text an LLM absorbed during training is frozen, unattributed, and impossible to correct after the fact. That's the &lt;strong&gt;training path&lt;/strong&gt;, and as a technical writer or docs engineer you have zero control over it.&lt;/p&gt;

&lt;p&gt;The path most AI harnesses prefer instead is fetching live pages: the &lt;strong&gt;retrieval path&lt;/strong&gt;. An agent runs something equivalent to &lt;code&gt;curl&lt;/code&gt; against a URL, reads what comes back, and answers based on that. Nearly every practice in this post targets the retrieval path, because it's the one you can actually influence.&lt;/p&gt;

&lt;h2&gt;
  
  
  AEO isn't SEO with a new name
&lt;/h2&gt;

&lt;p&gt;A published page usually gets visited by a person, either directly or through a search result. Search engines rank pages, and the practices that influence that ranking are SEO.&lt;/p&gt;

&lt;p&gt;AEO has no click in the loop. Someone asks their agent about generating native directories in a &lt;a href="https://docs.expo.dev/workflow/continuous-native-generation/" rel="noopener noreferrer"&gt;CNG&lt;/a&gt; project, the agent reads docs.expo.dev, and either answers directly or runs &lt;code&gt;npx expo prebuild&lt;/code&gt;. Nobody, human or agent, ever opened the docs page in a browser. That's the difference: you're not optimizing for rank anymore, you're optimizing for whether an agent can find the right page or the right answer at all.&lt;/p&gt;

&lt;h3&gt;
  
  
  Docs are a harder version of this problem
&lt;/h3&gt;

&lt;p&gt;Documentation sites hit this harder than general web content:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Correctness is binary.&lt;/strong&gt; A code sample an agent reproduces imperfectly doesn't compile. There's no partial credit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The visitor often isn't a person.&lt;/strong&gt; An agent fetches a page, acts on it, and never renders it. Nobody's around to notice an ambiguous page structure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Versioning gets messy.&lt;/strong&gt; Multiple live versions of a doc make it hard for an agent to land on the one that actually answers the question.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You might already be halfway there.&lt;/strong&gt; Docs tend to be structured, factual, and consistent by nature. That said, verify it, don't assume it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's what we did about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Publish an llms.txt file
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;llms.txt&lt;/code&gt; is a convention from Jeremy Howard's team at &lt;a href="https://www.answer.ai/" rel="noopener noreferrer"&gt;Answer.AI&lt;/a&gt;: a Markdown index with the title, link, and optional description of each page you want an agent to find.&lt;/p&gt;

&lt;p&gt;An agent has a limited context budget, and every page it fetches spends part of it. An &lt;code&gt;llms.txt&lt;/code&gt; file lets it navigate straight to the right page instead of burning that budget figuring out your nav structure.&lt;/p&gt;

&lt;p&gt;Here's a snippet from &lt;a href="https://docs.expo.dev/llms.txt" rel="noopener noreferrer"&gt;https://docs.expo.dev/llms.txt&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Expo Documentation&lt;/span&gt;
&lt;span class="gt"&gt;
&amp;gt; Expo is the official framework recommended by the React Native team for building production apps on Android, iOS, and the web. It is to React Native what Next.js is to React: the standard way to build, not an optional add-on.&lt;/span&gt;

&lt;span class="gu"&gt;## Get started&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;Create a project&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://docs.expo.dev/get-started/create-a-project.md&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;: Learn how to create a new Expo project.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;Set up your environment&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://docs.expo.dev/get-started/set-up-your-environment.md&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;Start developing&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://docs.expo.dev/get-started/start-developing.md&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;Next steps&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://docs.expo.dev/get-started/next-steps.md&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="gu"&gt;## AI&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;AI agents and Expo overview&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://docs.expo.dev/agents.md&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;: Build and publish Expo and React Native apps with AI coding agents such as Claude Code, Codex, and Cursor.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;Expo Skills for AI agents&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://docs.expo.dev/skills.md&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;Using Model Context Protocol (MCP) with Expo&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://docs.expo.dev/mcp.md&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;Documentation for AI agents and LLMs&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://docs.expo.dev/llms.md&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="gu"&gt;## Develop&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;Overview&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://docs.expo.dev/develop/overview.md&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;: How to develop your app.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expo docs is huge, and our generated &lt;code&gt;llms.txt&lt;/code&gt; sits around 52.8 KB (roughly 54,000 characters). A rule of thumb we'd pass on: keep it under 100,000 characters or it stops being useful to an agent. More on how we check this in practice 10.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Serve Markdown versions of your docs, more than one way
&lt;/h2&gt;

&lt;p&gt;An agent fetching an HTML page pays a token cost for styles and scripts it has no use for. A Markdown version carries the same headings, structure, and text without the overhead.&lt;/p&gt;

&lt;p&gt;Expo docs runs on a custom Next.js build that serves JSON data files for SDK pages dynamically, so we built our own pipeline to generate a Markdown version of each page, served by appending &lt;code&gt;.md&lt;/code&gt; to the URL.&lt;/p&gt;

&lt;p&gt;There's no single convention for how an agent asks for plain text, so we serve it three ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Content negotiation on the &lt;code&gt;Accept&lt;/code&gt; header&lt;/li&gt;
&lt;li&gt;An &lt;code&gt;.md&lt;/code&gt; suffix on any docs URL&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;&amp;lt;link rel="alternate"&amp;gt;&lt;/code&gt; hint in the HTML&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Content negotiation
&lt;/h3&gt;

&lt;p&gt;Our edge worker inspects the &lt;code&gt;Accept&lt;/code&gt; header, and if a client asks for Markdown, it serves the sibling &lt;code&gt;.md&lt;/code&gt; file instead of HTML:&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="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;accept&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Accept&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text/markdown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\/?&lt;/span&gt;&lt;span class="sr"&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;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;index.md&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;md&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ASSETS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;md&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;md&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;headers&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;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text/markdown; charset=utf-8&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ASSETS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&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;You can verify this with &lt;code&gt;curl&lt;/code&gt; from a terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Accept: text/markdown"&lt;/span&gt; https://docs.expo.dev/get-started/create-a-project/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The .md suffix
&lt;/h3&gt;

&lt;p&gt;We also allow appending &lt;code&gt;.md&lt;/code&gt; directly to a URL. Our redirect rules handle it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/index.md        /index.md          200
/&lt;span class="k"&gt;*&lt;/span&gt;/index.md      /:splat/index.md   200
/&lt;span class="k"&gt;*&lt;/span&gt;.md            /:splat/index.md   200
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first two entries above are canonical paths.&lt;/p&gt;

&lt;h3&gt;
  
  
  The alternate link hint
&lt;/h3&gt;

&lt;p&gt;The last method is a discovery hint in the HTML via a &lt;code&gt;&amp;lt;link&amp;gt;&lt;/code&gt; tag, useful for crawlers that already have the page and want a cheaper version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;link rel="alternate" type="text/markdown" href="/get-started/create-a-project.md" /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Convert custom components to Markdown, not source
&lt;/h2&gt;

&lt;p&gt;Expo docs is written in MDX, which lets us import React components into a page. Those components only become readable text once the page renders. The raw source doesn't contain what a developer actually sees.&lt;/p&gt;

&lt;p&gt;That's why we generate Markdown from the rendered HTML instead of the source. Our pipeline uses &lt;code&gt;cheerio&lt;/code&gt; and &lt;code&gt;turndown&lt;/code&gt;, generating Markdown via &lt;code&gt;convertHtmlToMarkdown&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="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;cheerio&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;cheerio&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;TurndownService&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;turndown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;gfm&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;turndown-plugin-gfm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;turndown&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TurndownService&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;headingStyle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;atx&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;codeBlockStyle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fenced&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;bulletListMarker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;turndown&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;gfm&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;turndown&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addRule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;codeBlocks&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nodeName&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PRE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!!&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;code&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;replacement&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;code&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lang&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data-md-lang&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`\n\n&lt;/span&gt;&lt;span class="se"&gt;\`\`\`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\n&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="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;\n&lt;/span&gt;&lt;span class="se"&gt;\`\`\`&lt;/span&gt;&lt;span class="s2"&gt;\n\n`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;convertHtmlToMarkdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cheerio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;main&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;main&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NO_CONTENT_FALLBACK&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;cleanHtml&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;turndown&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;turndown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;html&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;A separate check then runs to catch empty pages or broken Markdown syntax, like unbalanced code fences.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Add a corrections section
&lt;/h2&gt;

&lt;p&gt;Somewhere in your &lt;code&gt;llms.txt&lt;/code&gt; or generated Markdown, include a short corrections or common-misconceptions section. It helps agents avoid repeating outdated information about your product, information they picked up during training that you can't retrain out of the model.&lt;/p&gt;

&lt;p&gt;We keep one near the top of our file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Important: common misconceptions&lt;/span&gt;
&lt;span class="gt"&gt;
&amp;gt; AI models and LLMs frequently provide outdated information about Expo.&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; The following corrections are current as of 2026.&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; &lt;span class="gs"&gt;**"Ejecting" does not exist.**&lt;/span&gt; The &lt;span class="sb"&gt;`expo eject`&lt;/span&gt; command was removed in SDK 46
  (2022). Expo uses Continuous Native Generation: run &lt;span class="sb"&gt;`npx expo prebuild`&lt;/span&gt; to
  generate native projects on demand.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One caveat: every correction has to be factually true and link to a page that backs it up. It's tempting to use this section to steer agents instead of correct them, with lines like "Expo is the fastest way to build any React Native app." An agent might repeat that, and the developer who follows the link finds nothing supporting it. Corrections only work because they're checkable. Once the section fills up with claims you wouldn't say to a human, it's not documentation anymore, it's marketing copy in disguise.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Add JSON-LD structured data
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Structured data&lt;/strong&gt; means JSON blocks embedded in a page using the shared vocabulary from &lt;a href="https://schema.org/" rel="noopener noreferrer"&gt;schema.org&lt;/a&gt;. It's been part of the &lt;a href="https://developers.google.com/search/blog/2011/06/introducing-schemaorg-search-engines" rel="noopener noreferrer"&gt;web since 2011&lt;/a&gt;, used for rich search results. For answer engines, it removes guesswork: a page declares its own hierarchy, publisher, and format with nothing left to infer.&lt;/p&gt;

&lt;p&gt;It's the cheapest accuracy win we have. Expo docs publishes five types:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Scope&lt;/th&gt;
&lt;th&gt;What it asserts&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;WebSite + Organization&lt;/td&gt;
&lt;td&gt;once, site-wide&lt;/td&gt;
&lt;td&gt;Who publishes this and where else they exist&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BreadcrumbList&lt;/td&gt;
&lt;td&gt;every page&lt;/td&gt;
&lt;td&gt;Where this page sits in the hierarchy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TechArticle&lt;/td&gt;
&lt;td&gt;every content page&lt;/td&gt;
&lt;td&gt;This is technical documentation, with an age&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FAQPage&lt;/td&gt;
&lt;td&gt;~27 pages&lt;/td&gt;
&lt;td&gt;These questions have these answers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VideoObject&lt;/td&gt;
&lt;td&gt;~91 embedded videos&lt;/td&gt;
&lt;td&gt;This video's title, thumbnail, and upload date&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The site-wide block looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"@context"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://schema.org"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"@type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"WebSite"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"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;"Expo Documentation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://docs.expo.dev"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"publisher"&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;"@type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Organization"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"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;"Expo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://expo.dev"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"sameAs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"https://github.com/expo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"https://www.npmjs.com/org/expo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"https://x.com/expo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"https://bsky.app/profile/expo.dev"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"https://www.linkedin.com/company/expo-dev/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"https://www.youtube.com/@expodevelopers"&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;The &lt;code&gt;sameAs&lt;/code&gt; array is what kills entity ambiguity. It lets a machine confirm who publishes the docs and helps answer engines establish authority on the topic.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;TechArticle&lt;/code&gt; runs on every content page:&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;"@context"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://schema.org"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"@type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TechArticle"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"headline"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Continuous Native Generation (CNG)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Learn about managing your native projects with CNG and Prebuild."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dateModified"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-04-28"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://docs.expo.dev/workflow/continuous-native-generation/"&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;FAQPage&lt;/code&gt; is aimed squarely at answer engines because it states a question and its accepted answer in the exact shape an answer engine outputs:&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;"@context"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://schema.org"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"@type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"FAQPage"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mainEntity"&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;"@type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Question"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"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;"How does CNG help with project upgrades?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"acceptedAnswer"&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;"@type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Answer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The upgrade process involves upgrading the npm dependencies, app
               config, and re-running npx expo prebuild --clean."&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;Once your JSON-LD is live, verify it outside your own build too:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://search.google.com/test/rich-results" rel="noopener noreferrer"&gt;Rich Results Test&lt;/a&gt; parses a live URL and lists the types Google detected.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://validator.schema.org/" rel="noopener noreferrer"&gt;Schema Markup Validator&lt;/a&gt; checks a snippet against schema.org itself, no filtering to what Google supports. Paste the &lt;code&gt;application/ld+json&lt;/code&gt; block from view source.&lt;/li&gt;
&lt;li&gt;Google Search Console reports structured data errors across every crawled page, catching things you'd never think to check manually.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  6. Derive structured data from content, don't hand-write it
&lt;/h2&gt;

&lt;p&gt;With more than 1500 pages, hand-writing structured data for even the important ones is tedious and error-prone.&lt;/p&gt;

&lt;p&gt;Take FAQ. When a docs author writes it in the source file, the question lives in a collapsible component with a question and answer body. A wrapping &lt;code&gt;&amp;lt;FAQ&amp;gt;&lt;/code&gt; component builds the schema from its own children:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;FAQ&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;Collapsible&lt;/span&gt; &lt;span class="na"&gt;summary=&lt;/span&gt;&lt;span class="s"&gt;"How does CNG help with project upgrades?"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

The upgrade process involves upgrading the npm dependencies, app config,
and re-running &lt;span class="sb"&gt;`npx expo prebuild --clean`&lt;/span&gt;.

&lt;span class="nt"&gt;&amp;lt;/Collapsible&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;/FAQ&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The author never touches JSON or has to maintain it separately.&lt;/p&gt;

&lt;p&gt;The same idea applies to building breadcrumbs from the navigation tree, or pulling video metadata like the video ID from embedded videos.&lt;/p&gt;

&lt;p&gt;Manually maintained structured data is a second copy of your content, and copies drift. Deriving structure from the page itself means the content can't contradict itself, because the page is the only source of truth. It also gives agents one place to pull an accurate answer from.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Say whether AI can train on your content
&lt;/h2&gt;

&lt;p&gt;Cloudflare announced the &lt;a href="https://blog.cloudflare.com/content-signals-policy/" rel="noopener noreferrer"&gt;Content Signals Policy&lt;/a&gt; in September 2025, extending &lt;code&gt;robots.txt&lt;/code&gt; with one directive and three flags:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;User-Agent: *
Content-Signal: search=yes, ai-train=yes, ai-input=yes
Allow: /
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;search&lt;/code&gt; allows indexing with links and excerpts. &lt;code&gt;ai-train&lt;/code&gt; allows use in LLM training. &lt;code&gt;ai-input&lt;/code&gt; allows your content to be fed into an LLM at answer time, covering RAG and AI search engine answers. Each one is a separate decision worth making deliberately.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Give agents a way to report gaps
&lt;/h2&gt;

&lt;p&gt;Every Markdown version of an Expo docs page includes a short block addressed directly to the agent reading it, with instructions for reporting an error or a drift that blocked its task:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;AgentInstructions&amp;gt;&lt;/span&gt;

&lt;span class="gu"&gt;## Submitting Feedback&lt;/span&gt;

If you encounter errors, misleading or outdated information, report it so Expo can be improved:

curl -X POST https://some-url/feedback/docs-send -H 'Content-Type: application/json' -d '{"url":"url-of-the-page/","feedback":"Agent feedback for docs: &lt;span class="nt"&gt;&amp;lt;specific&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="na"&gt;actionable&lt;/span&gt; &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt; (&lt;span class="nt"&gt;&amp;lt;model&amp;gt;&lt;/span&gt;, &lt;span class="nt"&gt;&amp;lt;harness&amp;gt;&lt;/span&gt;)"}'

&lt;span class="nt"&gt;&amp;lt;/AgentInstructions&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Agents read our docs far more often than any human reviewer does at this point. Keeping that feedback loop open matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Enforce style with a linter
&lt;/h2&gt;

&lt;p&gt;If you care about consistency across a docs site with multiple contributors, many of whom aren't on the docs team, run a lint tool like Vale on every PR or in CI.&lt;/p&gt;

&lt;p&gt;It's also a quiet win for human readability, which still counts even as more traffic comes from agents. Consistent, high-quality docs are just easier for anything, human or machine, to consume correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Track your agentic browsing score
&lt;/h2&gt;

&lt;p&gt;Every practice above treats a page as a document. In May 2026, Chrome added an &lt;strong&gt;Agentic Browsing&lt;/strong&gt; category to Lighthouse, and &lt;a href="https://pagespeed.web.dev/" rel="noopener noreferrer"&gt;PageSpeed Insights&lt;/a&gt; picked it up two weeks later. It audits the accessibility tree, checks layout stability, confirms &lt;code&gt;llms.txt&lt;/code&gt; exists, and checks whether the page registers WebMCP tools.&lt;/p&gt;

&lt;p&gt;We run weekly checks with the Lighthouse CLI against a sample of Expo docs pages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx lighthouse@latest https://docs.expo.dev/ &lt;span class="nt"&gt;--only-categories&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;agentic-browsing &lt;span class="nt"&gt;--output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tracking individual audits matters because a page going from three failing checks to zero is a measurable improvement, not a vibe.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://afdocs.dev/" rel="noopener noreferrer"&gt;&lt;strong&gt;AFDocs Agent Score&lt;/strong&gt;&lt;/a&gt; is the second tool we lean on, built around an open standard called the &lt;strong&gt;Agent-Friendly Documentation Spec&lt;/strong&gt;, created by Dachary Carey. It's docs-specific in a way Lighthouse isn't, and it goes deeper on &lt;code&gt;llms.txt&lt;/code&gt; health than just whether the file exists, checking whether its structure actually follows the spec. Our first run scored 91. A few weeks of fixes got us to 95, and we learned a lot about what "agent-friendly" actually means along the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. Watch for hydration mismatches
&lt;/h2&gt;

&lt;p&gt;A hydration mismatch happens when the server-rendered HTML and the client-rendered HTML disagree. React keeps the server markup, logs a warning, and re-renders. A person might see a flicker. An agent gets one of two different trees depending on whether it fetched raw HTML or drove a browser, and neither is guaranteed to be the page you meant to ship.&lt;/p&gt;

&lt;p&gt;We had one in the Markdown actions dropdown, which rendered differently server-side versus client-side. Whether it rendered depended on the page path, normalized by stripping only the query string:&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;cleanPath&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;?&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the server, the path was &lt;code&gt;/additional-resources/&lt;/code&gt;, a page with dynamic data, so the dropdown was hidden. On the client, someone opening a hash link got &lt;code&gt;/additional-resources/#talks&lt;/code&gt;, which matched nothing in the dynamic-data list, so the dropdown showed up.&lt;/p&gt;

&lt;p&gt;Invisible to a person scrolling past it. For an agent parsing the DOM, it's a different tree than the one you intended to ship. The fix was one character:&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;cleanPath&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;#?&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Worth checking your own site for flickers like this, and keeping an eye on the Console tab for hydration warnings.&lt;/p&gt;

&lt;h2&gt;
  
  
  12. Clean up the accessibility tree
&lt;/h2&gt;

&lt;p&gt;Google names &lt;a href="https://web.dev/articles/ai-agent-site-ux" rel="noopener noreferrer"&gt;three primary ways an agent reads a page&lt;/a&gt;: screenshots, raw HTML, and the accessibility tree. The tree is the cheapest of the three for an agent to parse, and also the easiest to accidentally pollute.&lt;/p&gt;

&lt;p&gt;Three fixes we made in Expo docs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Decorative icons adding noise with no meaning. Fixed with &lt;code&gt;aria-hidden&lt;/code&gt;:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;LayoutAlt03Icon&lt;/span&gt; &lt;span class="na"&gt;aria-hidden&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"icon-sm"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;On&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Icons that carried meaning but had no label. Fixed with &lt;code&gt;aria-label&lt;/code&gt;:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;YesIcon&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;small&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;rest&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;IconBase&lt;/span&gt;
    &lt;span class="na"&gt;Icon&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;StatusSuccessIcon&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text-icon-success"&lt;/span&gt;
    &lt;span class="na"&gt;small&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;small&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="na"&gt;aria-label&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Yes"&lt;/span&gt;
    &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;rest&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Heading order jumping from &lt;code&gt;h2&lt;/code&gt; to &lt;code&gt;h4&lt;/code&gt;, breaking the outline:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;## Data persistence
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gd"&gt;-#### Exempting encryption prompt
&lt;/span&gt;&lt;span class="gi"&gt;+### Exempting encryption prompt
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Where this goes next
&lt;/h2&gt;

&lt;p&gt;Everything above is live on Expo docs today, and none of it required a rewrite of the site.&lt;/p&gt;

&lt;p&gt;Will these conventions still look like this in a year? Probably not. Things move faster in this space than almost anywhere else in tech, and what's manual today tends to get automated tomorrow. We expect more of this to get built into infrastructure directly. Cloudflare already converts HTML to Markdown at the edge when an agent asks for it, using the same &lt;code&gt;Accept: text/markdown&lt;/code&gt; negotiation from practice 2. If that becomes standard, a good chunk of this list turns into something your CDN just does for you, which would be a fine outcome for every docs site out there.&lt;/p&gt;

&lt;p&gt;Accurate, consistent docs still matter regardless of who's reading them. As more of your traffic comes from agents grepping information out of your pages instead of people scrolling through them, it's worth staying ahead of it rather than catching up later.&lt;/p&gt;

&lt;p&gt;If you're starting from zero, the fastest wins are &lt;code&gt;llms.txt&lt;/code&gt;, serving Markdown alongside HTML, and running an AFDocs Agent Score check to see where you actually stand before you guess.&lt;/p&gt;

&lt;p&gt;This post is based on content from the &lt;a href="https://expo.dev/blog/aeo-practices-to-make-your-documentation-ai-ready" 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>ai</category>
      <category>aeo</category>
      <category>seo</category>
    </item>
    <item>
      <title>How to nest a drawer inside bottom tabs without fighting gestures in Expo Router</title>
      <dc:creator>Dan</dc:creator>
      <pubDate>Tue, 08 Sep 2026 16:54:40 +0000</pubDate>
      <link>https://dev.to/expo/how-to-nest-a-drawer-inside-bottom-tabs-without-fighting-gestures-in-expo-router-4aba</link>
      <guid>https://dev.to/expo/how-to-nest-a-drawer-inside-bottom-tabs-without-fighting-gestures-in-expo-router-4aba</guid>
      <description>&lt;p&gt;You've got a bottom tab bar working. Then a mockup lands with a hamburger menu that slides out over the tabs: a workspace switcher, settings, a link to support. Now you need both navigation patterns at once, one nested inside the other, without the drawer swipe fighting your image carousel or the tab state resetting every time someone opens the menu.&lt;/p&gt;

&lt;p&gt;A persistent bottom tab bar paired with a global sidebar drawer shows up constantly in cross-platform apps. Nest those navigation trees carelessly, though, and you get performance overhead and gesture conflicts that only show up once you're testing on a real device.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.expo.dev/router/introduction/" rel="noopener noreferrer"&gt;Expo Router&lt;/a&gt; lets you map these flows directly onto a file-system directory. Your code stays modular, and the native navigation components still do the heavy lifting underneath. I posted a demo of this on Twitter and the Expo team asked me to write up the process. By the end of this post you'll have a drawer wrapping a tab navigator, plus the theming and gesture handling that keep the whole thing from feeling janky.&lt;/p&gt;

&lt;h2&gt;
  
  
  When nesting drawers and tabs actually makes sense
&lt;/h2&gt;

&lt;p&gt;Combining navigation patterns is powerful, but it shouldn't be your default choice. Before writing any code, work out whether your app's UX actually needs a dual-navigation hierarchy. Three rough cases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Flat navigation&lt;/strong&gt; (3 to 5 high-level screens with isolated paths): use pure bottom tabs. A side drawer here just adds cognitive load and visual clutter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contextual isolation&lt;/strong&gt; (a multi-tab workspace that needs omnipresent utilities like org switching or support): nest the tab navigator inside an outer drawer container. This is the case we're building here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deep-linked workflows&lt;/strong&gt; (transaction-heavy dashboards where users bounce between deep sub-routes): reach for native stack navigation with context-driven visibility instead.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The tradeoffs worth knowing before you commit
&lt;/h2&gt;

&lt;p&gt;A clean file-system layout keeps the structural complexity manageable, but a couple of constraints are worth flagging up front:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;State synchronization.&lt;/strong&gt; Passing shared global state or active user details from a standalone drawer route (like a detached &lt;code&gt;settings.tsx&lt;/code&gt;) down into the nested tab context takes deliberate context scoping, or an external store. It doesn't happen automatically just because the routes are nested.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Platform gesture conflicts.&lt;/strong&gt; Side-swipe drawer gestures can conflict with horizontal elements inside your tabs, like swipeable carousels or swipe-to-delete lists. Restricting &lt;code&gt;swipeEdgeWidth&lt;/code&gt; keeps those interactions predictable instead of having both gesture recognizers fight over the same touch.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core architecture
&lt;/h2&gt;

&lt;p&gt;To keep navigation state unified, your project directory should mirror your layout tree. Instead of a centralized routing config file, Expo Router lets you declare the view hierarchy directly with folder grouping semantics like &lt;code&gt;(drawer)&lt;/code&gt; and &lt;code&gt;(tabs)&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;app/
├── &lt;span class="o"&gt;(&lt;/span&gt;drawer&lt;span class="o"&gt;)&lt;/span&gt;/
│   ├── _layout.tsx         &lt;span class="c"&gt;# Outermost navigation wrapper (configures side drawer)&lt;/span&gt;
│   ├── &lt;span class="o"&gt;(&lt;/span&gt;tabs&lt;span class="o"&gt;)&lt;/span&gt;/
│   │   ├── _layout.tsx     &lt;span class="c"&gt;# Inner layout (configures persistent bottom tabs)&lt;/span&gt;
│   │   ├── index.tsx       &lt;span class="c"&gt;# Home dashboard screen&lt;/span&gt;
│   │   ├── explore.tsx     &lt;span class="c"&gt;# Explore feed screen&lt;/span&gt;
│   │   ├── notifications.tsx
│   │   └── profile.tsx     &lt;span class="c"&gt;# User profile screen&lt;/span&gt;
│   └── settings.tsx        &lt;span class="c"&gt;# Standalone global route accessed via drawer&lt;/span&gt;
└── _layout.tsx             &lt;span class="c"&gt;# Root orchestration and app lifecycle entry point&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  1. Root orchestration: app/_layout.tsx
&lt;/h2&gt;

&lt;p&gt;The root file is where the app lifecycle starts. It manages the native splash screen and sets up global context providers, so runtime configuration is in place before any child route renders.&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-native-gesture-handler&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;,&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;Slot&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;expo-router&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;GestureHandlerRootView&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-gesture-handler&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;SafeAreaProvider&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-safe-area-context&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;StyleSheet&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-native&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;ExpoSplashScreen&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-splash-screen&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;ThemeProvider&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;../src/context/ThemeContext&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;SplashScreenView&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;../src/screens/SplashScreen&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Prevent splash screen from auto-hiding before initialization checks finish&lt;/span&gt;
&lt;span class="nx"&gt;ExpoSplashScreen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventAutoHideAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;RootLayout&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;appIsReady&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setAppIsReady&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="nx"&gt;handleSplashFinish&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setAppIsReady&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="c1"&gt;// Securely unblock rendering and hide native splash once app asset states are verified&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ExpoSplashScreen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hideAsync&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;GestureHandlerRootView&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="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;root&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;SafeAreaProvider&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;ThemeProvider&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;/* Render layout contents once splash sequencing is complete */&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;appIsReady&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Slot&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&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;SplashScreenView&lt;/span&gt; &lt;span class="nx"&gt;onFinish&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleSplashFinish&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;/ThemeProvider&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;/SafeAreaProvider&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;/GestureHandlerRootView&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;const&lt;/span&gt; &lt;span class="nx"&gt;styles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;StyleSheet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;root&lt;/span&gt;&lt;span class="p"&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Global side drawer: app/(drawer)/_layout.tsx
&lt;/h2&gt;

&lt;p&gt;The side drawer is the outermost layout boundary. It uses the &lt;code&gt;expo-router/drawer&lt;/code&gt; module for configuration. Setting &lt;code&gt;drawerType&lt;/code&gt; to &lt;code&gt;'front'&lt;/code&gt; makes the drawer panel overlay the active screen instead of pushing it aside.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&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;Drawer&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/drawer&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;useTheme&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;@/src/hooks/useTheme&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;DrawerContent&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;@/src/components/DrawerContent&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;DrawerLayout&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;theme&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useTheme&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;colors&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;Drawer&lt;/span&gt;
      &lt;span class="nx"&gt;drawerContent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{(&lt;/span&gt;&lt;span class="nx"&gt;props&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;DrawerContent&lt;/span&gt; &lt;span class="p"&gt;{...&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;}
&lt;/span&gt;      &lt;span class="nx"&gt;screenOptions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt;
        &lt;span class="na"&gt;headerShown&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="na"&gt;drawerType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;front&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;drawerStyle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;270&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;drawerBackground&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;overlayColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rgba(0,0,0,0.6)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;swipeEdgeWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&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="p"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* Target internal tab group layout matching file path semantics */&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;Drawer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Screen&lt;/span&gt; 
        &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;(tabs)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; 
        &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="na"&gt;drawerLabel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Dashboard&lt;/span&gt;&lt;span class="dl"&gt;'&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="p"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* Standalone layout entry outside of the core bottom tab bar */&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;Drawer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Screen&lt;/span&gt; 
        &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;settings&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; 
        &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="na"&gt;drawerLabel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Settings&lt;/span&gt;&lt;span class="dl"&gt;'&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;/Drawer&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;h2&gt;
  
  
  3. Nested bottom tab layout: app/(drawer)/(tabs)/_layout.tsx
&lt;/h2&gt;

&lt;p&gt;Nesting the tab layout folder inside the &lt;code&gt;(drawer)&lt;/code&gt; path means child routes inherit the drawer context automatically, no manual wiring needed. Micro-interactions live in custom components, which keeps the layout files focused on routing instead of animation logic.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&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;Tabs&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;expo-router&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Platform&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;useTheme&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;@/src/hooks/useTheme&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;DrawerSceneWrapper&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;@/src/components/DrawerSceneWrapper&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;AnimatedTabIcon&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;@/src/components/AnimatedTabIcon&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TABS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;index&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Home&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;home-outline&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;activeIcon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;home&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;explore&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Explore&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;compass-outline&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;activeIcon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;compass&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;notifications&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Notifications&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;notifications-outline&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;activeIcon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;notifications&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;badge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;profile&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Profile&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;person-outline&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;activeIcon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;person&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;TabsLayout&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;theme&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isDark&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useTheme&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tabBarBg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;isDark&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rgba(11,11,19,0.98)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rgba(255,255,255,0.98)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tabBarBorder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;isDark&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rgba(255,255,255,0.06)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rgba(0,0,0,0.05)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;DrawerSceneWrapper&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;Tabs&lt;/span&gt;
        &lt;span class="nx"&gt;screenOptions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt;
          &lt;span class="na"&gt;headerShown&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="na"&gt;tabBarStyle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tabBarBg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;borderTopWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;borderTopColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tabBarBorder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Platform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OS&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ios&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;84&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;66&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;paddingBottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Platform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OS&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ios&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;paddingTop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;elevation&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="na"&gt;shadowColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isDark&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#000&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#6060aa&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;shadowOffset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;width&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="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="na"&gt;shadowOpacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isDark&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mf"&gt;0.3&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.06&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;shadowRadius&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="p"&gt;},&lt;/span&gt;
          &lt;span class="na"&gt;tabBarActiveTintColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tabBarActive&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;tabBarInactiveTintColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tabBarInactive&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;tabBarLabelStyle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;fontSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;fontWeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;400&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;marginTop&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="na"&gt;letterSpacing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.1&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="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;TABS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;tab&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Tabs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Screen&lt;/span&gt;
            &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;tab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;tab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt;
              &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="na"&gt;tabBarIcon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;focused&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;color&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AnimatedTabIcon&lt;/span&gt;
                  &lt;span class="nx"&gt;focused&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;focused&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
                  &lt;span class="nx"&gt;icon&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;tab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
                  &lt;span class="nx"&gt;activeIcon&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;tab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;activeIcon&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
                  &lt;span class="nx"&gt;badge&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;tab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;badge&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
                  &lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
                  &lt;span class="nx"&gt;activeColor&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tabBarActive&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="p"&gt;),&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="p"&gt;))}&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Tabs&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;/DrawerSceneWrapper&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;h2&gt;
  
  
  4. UI-layer micro-interactions: src/components/AnimatedTabIcon.tsx
&lt;/h2&gt;

&lt;p&gt;Animations run only where they matter. With shared values from React Native Reanimated, transforms and opacity changes execute on the UI thread instead of bouncing through the JS bridge. Each micro-interaction stays isolated, so a tab icon animating doesn't trigger re-renders across the parent layout.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useEffect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="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;StyleSheet&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;Ionicons&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/vector-icons&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Animated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;useSharedValue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;useAnimatedStyle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;withTiming&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;withSequence&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;Easing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-native-reanimated&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;TabIconProps&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;focused&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;icon&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;activeIcon&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;badge&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;color&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;activeColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;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;AnimatedTabIcon&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;focused&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;activeIcon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;badge&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;activeColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;TabIconProps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSharedValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dotOpacity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSharedValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;focused&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dotScale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSharedValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;focused&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ease&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;easing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Easing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;out&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Easing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cubic&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;focused&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;withSequence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nf"&gt;withTiming&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;easing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Easing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;out&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Easing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quad&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
        &lt;span class="nf"&gt;withTiming&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;easing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Easing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;out&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Easing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cubic&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;dotOpacity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;withTiming&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="nx"&gt;ease&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;dotScale&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;withTiming&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="nx"&gt;ease&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;withTiming&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;160&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;easing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Easing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;out&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Easing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cubic&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="nx"&gt;dotOpacity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;withTiming&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="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;easing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Easing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;out&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Easing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quad&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="nx"&gt;dotScale&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;withTiming&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="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;easing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Easing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;out&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Easing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quad&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;span class="nx"&gt;focused&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;iconStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useAnimatedStyle&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="p"&gt;}));&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dotStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useAnimatedStyle&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;dotOpacity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;dotScale&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="p"&gt;}));&lt;/span&gt;

  &lt;span class="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="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;iconWrap&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;Animated&lt;/span&gt;&lt;span class="p"&gt;.&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="nx"&gt;iconStyle&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;Ionicons&lt;/span&gt;
          &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{(&lt;/span&gt;&lt;span class="nx"&gt;focused&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;activeIcon&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;color&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;/Animated.View&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;Animated&lt;/span&gt;&lt;span class="p"&gt;.&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="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;activeDot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dotStyle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;activeColor&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="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;badge&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;focused&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&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="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;badgeDot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;borderColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;transparent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}]}&lt;/span&gt; &lt;span class="sr"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="err"&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;styles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;StyleSheet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;iconWrap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;44&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;28&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="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;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;activeDot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;borderRadius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;badgeDot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;absolute&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;top&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="na"&gt;right&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;borderRadius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#f43f5e&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;borderWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="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;
  
  
  5. A theme architecture that skips the flash of unstyled content
&lt;/h2&gt;

&lt;p&gt;To apply theme changes across nested boundaries without a flash of unstyled content, use design tokens. Primitive values map to a semantic theme type, so components, navigation styles, and layouts all read from one schema synchronously instead of waiting on a theme provider to resolve.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/theme/colors.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;palette&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;indigo50&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#eef2ff&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;indigo100&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#e0e7ff&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;indigo400&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#818cf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;indigo500&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#6366f1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;indigo600&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#4f46e5&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;indigo700&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#4338ca&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;neutral50&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#fafafa&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;neutral900&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#171717&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;white&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#ffffff&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;black&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#000000&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Theme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;lightTheme&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lightTheme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;dark&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="na"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#f8f8fc&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;surface&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#ffffff&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;primary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;palette&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;indigo500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;tabBarActive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;palette&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;indigo500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;tabBarInactive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#b0b0c8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;drawerBackground&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#ffffff&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#1a1a2e&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;statusBar&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dark-content&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dark-content&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;light-content&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;darkTheme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Theme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;dark&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#0f0f1a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;surface&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#1a1a2e&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;primary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;palette&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;indigo400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;tabBarActive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;palette&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;indigo400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;tabBarInactive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#4a4a6a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;drawerBackground&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#13131f&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#eeeeff&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;statusBar&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;light-content&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dark-content&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;light-content&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What this comes down to
&lt;/h2&gt;

&lt;p&gt;Nesting navigation like this is mostly an exercise in discipline. Pushing routing config into the file system with Expo Router keeps layouts scaling cleanly, without a giant configuration object to maintain by hand.&lt;/p&gt;

&lt;p&gt;For more on configuration and styling options, check the docs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Route grouping and nesting: the &lt;a href="https://docs.expo.dev/router/introduction/" rel="noopener noreferrer"&gt;Expo Router introduction&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Every drawer option available: the &lt;a href="https://docs.expo.dev/router/advanced/drawer/" rel="noopener noreferrer"&gt;Expo Router drawer guide&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Tab bar customization: the full list in the &lt;a href="https://reactnavigation.org/docs/bottom-tab-navigator/" rel="noopener noreferrer"&gt;bottom tab navigator docs&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Build the boundary first. Get &lt;code&gt;(drawer)/_layout.tsx&lt;/code&gt; rendering, then nest the &lt;code&gt;(tabs)&lt;/code&gt; group inside it and confirm both layouts show up before you touch theming or animations. Add the gesture constraints last, once the structure holds. Building it in that order keeps you from debugging a swipe conflict and a routing bug at the same time.&lt;/p&gt;

&lt;p&gt;If you build something with this pattern, or hit an edge case that isn't covered here, come share it in the &lt;a href="https://chat.expo.dev" rel="noopener noreferrer"&gt;Expo Discord&lt;/a&gt;. We hang out in the Expo Router channels and read the reports.&lt;/p&gt;

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

</description>
      <category>expo</category>
      <category>javascript</category>
      <category>reactnative</category>
    </item>
    <item>
      <title>Running an Expo SDK 57 app in Expo Go? You now need to be logged in on both ends</title>
      <dc:creator>Dan</dc:creator>
      <pubDate>Mon, 07 Sep 2026 22:07:18 +0000</pubDate>
      <link>https://dev.to/expo/running-an-expo-sdk-57-app-in-expo-go-you-now-need-to-be-logged-in-on-both-ends-32ef</link>
      <guid>https://dev.to/expo/running-an-expo-sdk-57-app-in-expo-go-you-now-need-to-be-logged-in-on-both-ends-32ef</guid>
      <description>&lt;p&gt;You scan the QR code like you've done a hundred times before, &lt;code&gt;npx expo start&lt;/code&gt; is running, the terminal looks fine, but the app on your phone just sits there with an error instead of loading your project.&lt;/p&gt;

&lt;p&gt;Here's what changed: &lt;a href="https://expo.dev/go" rel="noopener noreferrer"&gt;Expo Go&lt;/a&gt; on iOS has been updated in the App Store to support &lt;a href="https://docs.expo.dev/" rel="noopener noreferrer"&gt;Expo SDK 57&lt;/a&gt;, and along with that update comes a new requirement. To run an SDK 57 project in development mode, you now need to be logged in to both the Expo CLI in your terminal and the Expo Go app itself, using the same account on both sides.&lt;/p&gt;

&lt;p&gt;This is live now for the latest Expo Go on iOS. Android is getting the same treatment later, and it doesn't touch simulator builds or development builds at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you'll see
&lt;/h2&gt;

&lt;p&gt;If either side isn't logged in when you scan a QR code (or tap a dev server address on the Home tab), Expo Go shows you an error telling you exactly what's missing:&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%2Fe2fbed1cbeba3d0010aed0281ef2127a97c4b645-4979x2744.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%2Fe2fbed1cbeba3d0010aed0281ef2127a97c4b645-4979x2744.png" alt="login errors from Expo Go" width="800" height="441"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The message tells you whether the terminal needs a login, the app needs a login, or both. No guessing required.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to fix it
&lt;/h2&gt;

&lt;p&gt;On the terminal side, run:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;npx expo login&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;and follow the link to sign in through your browser.&lt;/p&gt;

&lt;p&gt;On the Expo Go app side, go to the Home tab, tap the avatar icon in the upper right corner, and enter your Expo username and password.&lt;/p&gt;

&lt;p&gt;Once both are logged in with the same account, scan the QR code again (or tap the dev server address on Home) and your app should load normally.&lt;/p&gt;

&lt;p&gt;The account has to match on both ends. Logging in as one user in the terminal and a different user in Expo Go won't work.&lt;/p&gt;

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

&lt;p&gt;A few things worth knowing before this trips you up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;This currently only applies to the latest Expo Go for iOS. Android is coming, but isn't enforced yet.&lt;/li&gt;
&lt;li&gt;Simulator builds are exempt. This is specific to the physical Expo Go app.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.expo.dev/develop/development-builds/introduction/" rel="noopener noreferrer"&gt;Development builds&lt;/a&gt; don't require login at all, this is an Expo Go-only change.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're on Android, it's still worth signing into Expo Go now rather than later. Being logged in automatically surfaces your running dev servers on the Home tab and lets you &lt;a href="https://expo.dev/changelog/expo-go-loading-changes-may-2026" rel="noopener noreferrer"&gt;load published projects&lt;/a&gt; directly, so you're not scanning QR codes every time.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;If you're hitting the login error right now: run &lt;code&gt;npx expo login&lt;/code&gt; in your terminal, then log in through the avatar icon in Expo Go&lt;/li&gt;
&lt;li&gt;Make sure both logins use the same Expo account&lt;/li&gt;
&lt;li&gt;If you'd rather skip this entirely, switch to a development build, which doesn't require login&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This post is based on content from the &lt;a href="https://expo.dev/changelog/expo-go-57-login" 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>How SWSH turned 300 monthly builds into a release pipeline nobody has to babysit</title>
      <dc:creator>Dan</dc:creator>
      <pubDate>Sat, 05 Sep 2026 15:04:16 +0000</pubDate>
      <link>https://dev.to/expo/how-swsh-turned-300-monthly-builds-into-a-release-pipeline-nobody-has-to-babysit-11bl</link>
      <guid>https://dev.to/expo/how-swsh-turned-300-monthly-builds-into-a-release-pipeline-nobody-has-to-babysit-11bl</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Expo's ecosystem helped us take human error and effort out of the release process, from a local build to a validated release."&lt;/em&gt;&lt;/p&gt;

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

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

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

&lt;p&gt;This post breaks down how that pipeline is put together, piece by piece, using &lt;a href="https://expo.dev/" rel="noopener noreferrer"&gt;Expo&lt;/a&gt;, &lt;a href="https://docs.expo.dev/build/introduction" rel="noopener noreferrer"&gt;EAS Build&lt;/a&gt;, &lt;a href="https://docs.expo.dev/submit/introduction" rel="noopener noreferrer"&gt;EAS Submit&lt;/a&gt;, &lt;a href="https://docs.expo.dev/eas-update/introduction" rel="noopener noreferrer"&gt;EAS Update&lt;/a&gt;, and &lt;a href="https://expo.dev/solutions/expo-observe" rel="noopener noreferrer"&gt;EAS Observe&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnban9s6lvlo9nmgjk5h0.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnban9s6lvlo9nmgjk5h0.jpg" alt="SWSH on the app store" width="800" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;h3&gt;
  
  
  Submission across both stores, with a custom layer for iOS
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcr53qwjaoml5c57aba44.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcr53qwjaoml5c57aba44.jpg" alt="Agentic Release pipeline" width="800" height="432"&gt;&lt;/a&gt;&lt;/p&gt;

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

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

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

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

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

&lt;p&gt;Before anything goes out, SWSH runs an in-house end-to-end testing framework called &lt;strong&gt;Meridian&lt;/strong&gt;, built on Appium and WebDriverIO and driven by Vitest. Meridian exercises the flows that actually matter (login, sign-up, upload, deep linking) across iOS, the App Clip, and Android, on simulators and emulators, in CI. When SWSH marks a release as validated, that means the app was actually driven through those flows, not just that it compiled without errors.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvvfzn5voxy4vx3hkwmsx.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvvfzn5voxy4vx3hkwmsx.jpg" alt="SWSH validation gate and E2E Testing" width="800" height="466"&gt;&lt;/a&gt;&lt;/p&gt;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;p&gt;That's the API I wanted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Composer.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;TextInput&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react-native&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PasteInputWrapper&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;expo-paste-input&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Composer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PasteInputWrapper&lt;/span&gt;
      &lt;span class="na"&gt;onPaste&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;images&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uris&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;TextInput&lt;/span&gt; &lt;span class="na"&gt;placeholder&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Type a message"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;PasteInputWrapper&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

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

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;PasteEventPayload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;images&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;uris&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unsupported&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gave me the shape I wanted from the start: a paste event, and the app decides what happens next.&lt;/p&gt;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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




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

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

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

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

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

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

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

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

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

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

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

&lt;p&gt;Our first app was Music AI, an AI cover generation app. You could create AI covers of songs with different voices, or even train a custom voice of yourself or your loved ones and make them sing. It took off fast.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnfiwo716o9lusbspmt4j.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnfiwo716o9lusbspmt4j.jpg" alt="Feraset app" width="800" height="508"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Music AI became the best AI cover app in the world. We surpassed Spotify in the App Store charts and reached the #1 spot globally in multiple countries including the US and major European markets.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2c6bubjbnhf8rj326hco.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2c6bubjbnhf8rj326hco.jpg" alt="Hit #1 in the app store" width="799" height="419"&gt;&lt;/a&gt;&lt;/p&gt;

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

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

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

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

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

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

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

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

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

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;available&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;isAvailable&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;available&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sendMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Refine this prompt for image generation: a cat sitting&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;systemPrompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;You are a prompt engineering assistant.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's also streaming support for progressive text display:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;promise&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;stop&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;streamMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Check this text against content guidelines...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setResponseText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;accumulatedText&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;systemPrompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;You are a content moderation assistant.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Just &lt;code&gt;npm install expo-ai-kit&lt;/code&gt;, add the config plugin and you're running on-device inference. The library currently gets tens of thousands of weekly downloads on &lt;a href="https://www.npmjs.com/package/expo-ai-kit" rel="noopener noreferrer"&gt;npm&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F54s2wjbu7alp9sgjmaqb.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F54s2wjbu7alp9sgjmaqb.jpg" alt="expo ai kit" width="800" height="576"&gt;&lt;/a&gt;&lt;/p&gt;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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




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

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

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

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

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

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

&lt;p&gt;Luckily, this time we had already built &lt;a href="https://www.hipcamp.com/journal/engineering/scout-our-in-house-ai-agent" rel="noopener noreferrer"&gt;Scout&lt;/a&gt;, an AI agent designed to tackle whatever task we throw at it. What would have taken an engineer weeks of tedious dependency research and version resolution, Scout handled across a few focused sessions over several days. While having an internal agent like Scout helps, we will be sharing how you can tackle React Native upgrades with pure Claude Code.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9o07tblg72xjkgldb28u.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9o07tblg72xjkgldb28u.jpg" alt="Scout sped up Hipcamp's upgrade" width="800" height="718"&gt;&lt;/a&gt;&lt;/p&gt;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;p&gt;We then provided it a prompt. Here is an example of what we gave ours:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Expo SDK 50 → 54 Dependency Audit&lt;/span&gt;

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

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

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

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

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

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

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  Phase 3: Review and begin the upgrade!
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqq4uselut6nat585j3th.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqq4uselut6nat585j3th.jpg" alt="Upgrading SDK" width="799" height="321"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then, we manually went through each upgrade item and confirmed it all looked right. We make sure its not removing libraries it shouldn’t, and pick the right alternative library if it recommended any. We highly recommend using &lt;a href="https://docs.expo.dev/workflow/using-libraries/" rel="noopener noreferrer"&gt;Expo’s libraries&lt;/a&gt; if available as they are actively maintained and well documented.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl4pticapsvsc4y14s81l.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl4pticapsvsc4y14s81l.jpg" alt="Documenting the process" width="800" height="568"&gt;&lt;/a&gt;&lt;/p&gt;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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




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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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




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

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

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

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

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

&lt;p&gt;With &lt;a href="https://docs.expo.dev/eas/workflows/introduction/" rel="noopener noreferrer"&gt;EAS Workflows&lt;/a&gt;, you can already reuse builds, run custom jobs on Android and iOS, and comment on GitHub pull requests. That turns out to be enough to build a lightweight QA agent today, without introducing a big custom platform.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9hekhivtucy76k82htqz.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9hekhivtucy76k82htqz.jpg" alt="QA Agent Workflow" width="800" height="240"&gt;&lt;/a&gt;&lt;/p&gt;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

&lt;/div&gt;



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

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

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

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

&lt;p&gt;A simplified version looks like this:&lt;br&gt;
&lt;/p&gt;

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

### Android
Short summary...
Screenshots

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

&lt;/div&gt;



&lt;p&gt;That puts the result exactly where reviewers already are, with necessary information and visual feedback at hand.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwpcpcmd34iy0jg31pxag.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwpcpcmd34iy0jg31pxag.jpg" alt="QA Agent before and after" width="800" height="625"&gt;&lt;/a&gt;&lt;/p&gt;

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

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

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

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

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

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

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

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

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

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

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

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

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




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

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

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

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

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

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

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

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

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

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
