DEV Community

Cover image for From React web to Native in one week
Dan for Expo

Posted on • Edited on • Originally published at expo.dev

From React web to Native in one week

Originally published on expo.dev/blog
By Ishika Chakraborty


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

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

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

Not a tutorial app, not a counter, not another todo list. I built Sun Buddy - a native iOS app with a cute, animated sun mascot, real-time UV tracking from my location, haptic feedback, daily local notifications, persistent progress, and a sleeping nighttime state when the sun is down.

Hero-image

I built it with Expo, Claude Code, and Expo Skills.

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

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

The short version: I did not have to become an app developer to build this app. I had to learn the native edges around the React skills I already had.

The idea: Sun Buddy

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

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

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

Sunny has a few states:

  • drowsy at 0%

  • curious when you have started

  • happy when you are halfway there

  • absolutely radiant when you hit your goal

  • asleep at night when the sun is down

A little weird. A little cute. The kind of app I would actually keep on my phone.

mascot-stages

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

Those patterns are not specific to a sun mascot; they are the same patterns you hit in fitness apps, habit trackers, field tools, travel apps, internal dashboards, delivery workflows, or anything else that needs to feel at home on a phone.

The setup: Expo first, Claude Code second

The most important part of this experience was not that I used AI. It was that I was able to build a native app using the React mental model I already had, and that is where **Expo**** mattered**.
Expo gave me the app structure, routing, device APIs, build path, and the bridge from "I know React" to "this is running on my phone."

Claude Code gave me the pairing loop, and Expo Skills made that pairing loop much more useful.

That distinction matters. Claude Code on its own can help you write code, but native development has a lot of details where generic advice is not enough: permissions, config plugins, EAS Build profiles, App Store constraints, native module support. These are exactly the places where stale or vague guidance wastes time.

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

Expo Skills brought Expo-specific context into the workflow.

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

  • Should this be a normal Expo Go flow or a dev client?

  • Where does this permission need to be configured?

  • Is this a runtime library, a config plugin, or both?

  • What is the right path to a real iPhone build?

  • What changes when I move from simulator testing to TestFlight?

That is where Expo Skills mattered - they gave the pairing loop Expo-specific context instead of generic React Native guesses. I added the Skills inside Claude Code:

// expo-skills
/plugin marketplace add expo/skills
/plugin install expo
Enter fullscreen mode Exit fullscreen mode

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

  • **building-native-ui** - Expo Router patterns, native UI structure, Apple HIG guidance

  • **expo-deployment** - EAS Build, TestFlight, App Store, credentials

  • **expo-dev-client** - development builds and native module support

That made Claude Code feel less like a generic coding assistant and more like a collaborator that understood the Expo ecosystem.

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

The surprise: my React skills transferred almost immediately

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

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

That does not mean everything from web transfers perfectly. But the core React instincts transferred much more than I expected.

Here is how it felt in practice:

What I knew from web How it mapped to native
React components React Native components
useState, useEffect, custom hooks Same patterns
Next.js file-based routing Expo Router
Flexbox layout React Native flexbox
Browser geolocation expo-location
localStorage AsyncStorage
CSS/Framer-style motion instincts Reanimated
Deploying a web app EAS Build / TestFlight / App Store

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

That table explains why this felt approachable. I was not starting over, I was learning the edges of a new runtime.

The part that did not transfer automatically

The surrounding layer is where native feels different. Not the component model, not the React part, but the native edges

Things like:

  • permission prompts

  • device APIs

  • build-time configuration

  • native modules

  • simulator state

  • development builds

  • code signing

  • App Store constraints

  • testing on a real device

That is where I would have taken wrong turns without Expo and Expo Skills.

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

The shape of the logic was familiar, but the platform expectations were different - not hard code, but you do need to know the right sequence.

const { status } = await Location.requestForegroundPermissionsAsync();

if (status !== "granted") {
  // Handle denial properly
  return;
}

const location = await Location.getCurrentPositionAsync({});
Enter fullscreen mode Exit fullscreen mode

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

My app needed plugin entries like this:

"plugins": [
  "expo-router",
  "expo-location",
  [
    "expo-notifications",
    {
      "icon": "./assets/notification-icon.png"
    }
  ]
]
Enter fullscreen mode Exit fullscreen mode

This is exactly where Expo-specific context helped. Without it, I probably would have written perfectly reasonable React code and then wasted time debugging native behavior that had nothing to do with my components.

The native capabilities I added

Once the app structure was in place, the fun part was adding things that actually feel native.

I used expo-location to get the phone's GPS coordinates, then passed those to the Open-Meteo API to get the current UV index - no API key, no backend, just the phone's location and a public weather API. That alone made the app feel different from a normal web project. It was not asking the user to type a city; it already knew where the device was.

Then I added haptics. This was the smallest feature that made the biggest difference. I used expo-haptics for two moments:

  • light impact feedback when starting a sun session

  • success feedback when hitting 100%

The code was tiny:

await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
Enter fullscreen mode Exit fullscreen mode

And:

await Haptics.notificationAsync(
  Haptics.NotificationFeedbackType.Success
);
Enter fullscreen mode Exit fullscreen mode

But the effect was immediate.

Tap. Bounce. Haptic.

Suddenly it did not feel like a React screen pretending to be an app. It felt like something that belonged on the phone.

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

I used expo-notifications for daily local reminders with copy that felt intentionally a little strange - scheduled on-device, not remote push. (Remote push would require APNs credentials and the paid Apple Developer Program, neither of which the app needed.)

Sunny is waiting for you outside. Probably.

That felt right for the mascot's personality.

notifications

Sunny was built with @shopify/react-native-skia and animated with react-native-reanimated - bouncing, blinking, emotional state transitions. If you have used Framer Motion, the mental model is not alien, and the result was much smoother than I expected. Not "good enough for a demo" smooth but actually smooth.

For daily progress, I used @react-native-async-storage/async-storage, which felt like async localStorage. I used expo-font for Nunito and @expo/vector-icons for tab icons. None of this felt like learning mobile development from scratch. Instead, it felt like using React to reach parts of the device that the web usually cannot reach.

Where things actually broke

A lot worked faster than I expected, and some things broke.

The Skia crash on web

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

The fix was platform file splitting:

components/
  SunBuddy.native.tsx   # full Skia implementation
  SunBuddy.web.tsx      # SVG fallback
Enter fullscreen mode Exit fullscreen mode

Metro automatically resolves .native.tsx for iOS and .web.tsx for web.

That pattern was new to me.

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

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

The stale Metro bundler

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

npx expo start --clear
Enter fullscreen mode Exit fullscreen mode

The old bundle was still being served without the Reanimated Babel plugin properly registered.

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

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

I did not love that, but once I hit it and understood it, it became one of those "okay, now I know" moments.

The nighttime bug

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

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

Tools execute on your intent, and your job is to have the right intent.

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

Sunny is sleeping. See you at sunrise.

sleeping-state

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

Code signing

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

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

But I did have to understand what was happening.

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

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

If you want TestFlight or App Store distribution, you need the Apple Developer Program and that part is not going away.

EAS is the path I recommend first

I tried local iOS setup because I wanted to understand what was happening under the hood, but if I were telling another React web developer how to start, I would point them to EAS earlier.

The basic flow is:

eas build:configure
eas build --platform ios
eas submit --platform ios
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

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

The risk of trying is lower than it looks.

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

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

Native setup is real, but it is not a reason to avoid native forever.

The moment it clicked

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

Tap. Animation. Haptic. Progress. Not a browser tab, not a responsive web view, not a prototype pretending to be an app, but a real thing on my phone, built with React.

phone

It was on my home screen. It knew where I was. It responded to touch in a way the web usually does not.

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

What I would tell another React web developer

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

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

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

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

Build something small around that. Run it on a device.

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

Sun Buddy was built with Expo, Claude Code, and Expo Skills. If you are a React web developer thinking about going native, I would start here: Expo for React web developers.

Top comments (0)