Skip to content
Back to Project Tutorials

Mobile App

Ship a cross-platform mobile app with React Native / Expo from planning to app store.

Planning & Setup

Define scope, choose Expo vs bare workflow, and set up the project skeleton.

Why

Mobile planning locks in constraints that are expensive to change later: navigation structure, state shape, and offline behavior. Expo vs bare is a 1-week-vs-3-months decision — Expo handles native modules and OTA updates for you; bare gives full native control but you own Gradle/Xcode builds.

When

Before any screen code. 1-2 weeks for a focused MVP. Decide iOS-only vs both platforms now — it affects every dependency choice.

How

1. Define MVP scope - List 3-5 core screens (e.g., auth, feed, detail, settings) - Pick one platform first (iOS) if it shortens time-to-test 2. Choose workflow - Expo (recommended): npx create-expo-app, no Xcode/Android Studio needed for dev - Bare: npx react-native init, full native control, you maintain native builds 3. Set up navigation - expo-router (file-based, like Next.js) — recommended for new apps - Define tab/stack/modal structure upfront 4. Choose state + data layer - State: Zustand (simple) or Redux Toolkit (structured) - Data: TanStack Query for server state, AsyncStorage for local cache - Plan offline: cache-first reads, queue writes for sync 5. Configure environment - .env for API URL, Sentry DSN, feature flags - EAS Build (Expo Application Services) for cloud builds without Xcode

Example

Scaffold: npx create-expo-app@latest my-app --template tabs cd my-app && npx expo install expo-router zustand @tanstack/react-query app/ (expo-router file-based) _layout.tsx — root stack + QueryClientProvider (tabs)/_layout.tsx — bottom tab navigator (tabs)/index.tsx — feed screen (tabs)/detail/[id].tsx login.tsx .env.local: EXPO_PUBLIC_API_URL=https://api.example.com EXPO_PUBLIC_SENTRY_DSN=... Decision: Expo managed + EAS Build (no local Xcode for first 3 months)

Common Problems

  • ⚠Choosing bare workflow too early — you spend weeks on Gradle/Xcode instead of features
  • ⚠No offline plan — app shows blank screens on flaky networks, users perceive it as broken
  • ⚠Deep links and navigation structure decided ad-hoc, causing refactors when adding screens

Best Practices

  • ✓Start with Expo managed — eject only when you need a native module Expo can't provide
  • ✓Design navigation as a tree upfront with expo-router; file paths are your routes
  • ✓Treat server state and local state separately — TanStack Query + Zustand, not one giant Redux store

Development & Testing

Build screens with fast refresh, device testing, and CI on every commit.

Why

Mobile bugs are harder to reproduce than web — device fragmentation, OS versions, and network conditions multiply edge cases. Fast refresh and on-device testing from week 1 catch layout and gesture issues that simulators miss. Without CI, regressions in native dependencies silently break builds.

When

After setup. Build the riskiest screen first (auth or core flow) to validate navigation + data layer early.

How

1. Develop with fast refresh - npx expo start — scan QR on physical device (Expo Go) - Press 'j' for web preview, 'i' for iOS sim, 'a' for Android emu 2. Build screens incrementally - One screen per PR, keep PRs < 400 lines for reviewability - Use React Native Paper / Tamagui for consistent components - Test gestures with react-native-gesture-handler early 3. Handle platform differences - Platform.OS checks for divergent code - Platform.select({ ios: ..., android: ... }) - SafeAreaView for notches; react-native-safe-area-context 4. Set up testing - Jest + React Native Testing Library for component/unit tests - Detox or Maestro for E2E (Maestro is simpler, YAML flows) - Run tests in CI on every PR (GitHub Actions with EAS) 5. Add observability - Sentry React Native (crashes, breadcrumbs, performance) - LogRocket / Embrace for session replay (mobile-specific)

Example

Component test (Jest + RNTL): test('feed renders items', () => { const { getByText } = render(<Feed items={[{id:1,title:'Hi'}]} />); expect(getByText('Hi')).toBeTruthy(); }); E2E (Maestro flow.yml): appId: com.example.app - launchApp - tapOn: { id: 'login-button' } - assertVisible: 'Welcome' CI (.github/workflows/ci.yml): - npm ci && npx expo prebuild - npm test -- --coverage - npx maestro test flows/

Common Problems

  • ⚠Testing only on simulator — gesture and keyboard bugs appear only on real devices
  • ⚠Ignoring Android during iOS dev — layout breaks on small Android screens at submission
  • ⚠No crash reporting until after launch — first bad device is a 1-star review

Best Practices

  • ✓Test on a real device from day 1 — scan the Expo QR on your phone, not just the simulator
  • ✓Run Maestro E2E on both iOS and Android in CI — platform bugs are asymmetric
  • ✓Install Sentry before first TestFlight — you need crash data before reviewers hit them

Deployment & OTA Updates

Build production binaries with EAS and ship JS updates over the air without store review.

Why

EAS Build produces signed binaries in the cloud without wrestling with Xcode signing. OTA updates (expo-updates) let you push JS-only fixes in minutes instead of waiting 1-7 days for store review — critical for crash fixes, but only safe if you structure updates correctly.

When

When core flows are tested on device. Set up EAS and expo-updates before first TestFlight so the update channel exists when you need it.

How

1. Configure EAS - eas.json: profiles for development, preview, production - eas build --platform ios --profile production - Credentials: EAS auto-manages signing (Apple Developer account needed) 2. Set up OTA updates - npx expo install expo-updates - eas update --branch production --message "fix crash on login" - JS-only changes ship instantly; native changes need a new binary 3. Staged rollout - Update a staging channel first, smoke test - Promote to production channel after 24h clean - expo-updates rollback if a bad update ships 4. Submit first binary to stores - iOS: eas submit --platform ios (uses App Store Connect API) - Android: eas submit --platform android (Google Play service account) - First submission needs manual review; updates can be staged 5. Monitor post-deploy - Sentry release health: crash-free users %, new issues per release - Roll back via OTA if crash-free drops below 99.5%

Example

eas.json (production profile): { "build": { "production": { "distribution": "store", "ios": { "applicationArchivePath": "build/*.ipa" } } } } Ship a JS-only hotfix: eas update --branch production --message "fix: null user on cold start" # Users get it on next app open, no store review Ship a native change (new permission): eas build --platform ios --profile production eas submit --platform ios # full store review needed

Common Problems

  • ⚠Shipping an OTA update that uses a new native API — crashes on devices still on the old binary
  • ⚠No staging channel — every update goes straight to all users, no way to catch regressions
  • ⚠Waiting for store review for a crash fix that could have been an OTA update

Best Practices

  • ✓Only OTA-ship JS-only changes; any native change requires a new binary + store review
  • ✓Always update a staging channel first and wait 24h before promoting to production
  • ✓Wire expo-updates rollback into your incident runbook — it's your fastest revert

Store Submission & Growth

List on App Store and Google Play with assets, ASO, and review-driven iteration.

Why

Store listing quality directly drives install conversion — screenshots and the first 3 lines of description decide whether a search user installs. ASO (App Store Optimization) is the mobile equivalent of SEO: done once, it compounds for the app's lifetime. Review response is your public support channel.

When

After the first production binary is built and tested. Don't rush listing assets — a great screenshot set can 2x install conversion.

How

1. Prepare store assets - Icon: 1024x1024, no alpha, no rounded corners (stores add them) - Screenshots: 6.7" iPhone + 5.5" for max coverage; show real UI, not marketing fluff - Description: first 3 lines are the hook (visible without expanding) 2. Write ASO-optimized metadata - Title: brand + 1 keyword phrase (30 chars iOS, 50 Android) - Keywords: 100 chars iOS, research with AppFollow / Sensor Tower - Subtitle: benefit, not feature ("Track habits in 30s" not "Habit tracker") 3. Submit for review - iOS: eas submit --platform ios (1-3 day review, faster after first) - Android: eas submit --platform android (usually < 24h) - Fill privacy policy URL, data usage questionnaire honestly 4. Launch and monitor reviews - Reply to every review within 24h — it's public support - Track install conversion in App Store Connect / Play Console - Watch for 1-star reviews mentioning a specific device/OS 5. Iterate with ASO - A/B test icon and screenshots (Play Console experiments) - Update keywords quarterly based on search term reports - Each update is a chance to re-enter "new" sections

Example

App Store Connect listing: Name: "FocusFlow — Habit Tracker" (keyword: Habit Tracker) Subtitle: "Build habits in 30 seconds a day" Keywords: habit,tracker,routine,streak,goal,daily,builder Screenshots: 6.7" iPhone, first = core loop, last = settings Review response template: "Hi {name}, sorry for the trouble. We fixed this in v1.2 — update via the App Store. Email help@focusflow.app for a quick refund." Play Console experiment: Test icon A (abstract) vs B (app UI) for 14 days -> B won +18% install conversion, ship B

Common Problems

  • ⚠Marketing-style screenshots that hide the real UI — users distrust and don't install
  • ⚠Keyword stuffing in the title — stores penalize and ranking drops
  • ⚠Ignoring 1-star reviews — each unanswered negative review costs ~10 installs

Best Practices

  • ✓First screenshot shows the core action, not a splash screen — it's the #1 install driver
  • ✓Reply to every review within 24h; a polite response often turns a 1-star into a 4-star on update
  • ✓Run Play Console icon/screenshot A/B tests continuously — small wins compound over an app's life

Full Developer Lifecycle

A 14-stage developer roadmap from idea to maintenance — visualize dependencies and track progress.

Next Step