← Back to Blog
React2026-05-14·73 min read

From web to iOS in 30 days: Expo + AI-assisted code conversion

By Caden Burleson

High-Fidelity Mobile Delivery: The Expo-Native Hybrid Strategy for Solo Engineers

Current Situation Analysis

Solo developers and small teams face a persistent architectural dilemma when expanding from web to mobile: the "Native Tax." Building a pure native iOS application requires a complete rewrite in Swift, demanding significant time investment and language expertise. Conversely, wrapping a web application in a WebView (via Capacitor or similar) offers speed but fails catastrophically when the application requires real-time hardware access, such as camera processing or high-frequency sensor data.

This problem is often misunderstood as a binary choice between "slow native rewrite" and "fast but limited wrapper." Many teams overlook the hybrid capability of modern frameworks like Expo, which allows developers to maintain a TypeScript/React Native codebase while dropping into native Swift for performance-critical paths. This approach preserves code reuse without sacrificing the user experience where it matters most.

Data from recent solo-founder deployments demonstrates the viability of this middle path. Projects utilizing Expo with selective native modules have achieved iOS delivery in approximately 30 days, reusing roughly 85% of existing web business logic, while maintaining camera processing latencies comparable to pure native implementations. This strategy effectively collapses the development timeline while preserving the performance characteristics required for AI-driven or real-time applications.

WOW Moment: Key Findings

The hybrid approach fundamentally shifts the cost-benefit analysis for mobile expansion. By isolating "hot paths" (performance-critical code) and implementing them as native modules, teams can achieve near-native performance metrics with a fraction of the development effort.

Strategy Development Cycle Camera Latency Code Reuse Solo Feasibility
Pure Swift Rewrite 90+ days <10ms 0% Low
WebView Wrapper 10 days >100ms 100% High
Expo + Native Bridge ~30 days <15ms ~85% High

Why this matters: The hybrid model enables solo engineers to ship production-grade mobile apps that interact with device hardware efficiently. It eliminates the need to maintain two separate codebases for business logic while ensuring that performance bottlenecks are handled by the native layer. This is particularly valuable for applications involving computer vision, health data synchronization, or complex gesture handling.

Core Solution

The implementation strategy relies on a "Hot Path Native, Cold Path JS" architecture. The core principle is to identify components where the JavaScript bridge introduces unacceptable latency and implement those specific functions as native Swift modules using the Expo Modules API. All other logic, including state management, UI composition, and business rules, remains in TypeScript.

1. Architecture and AI-Assisted Conversion

The conversion process begins with an audit of the existing web codebase. Pure business logic, data models, and API clients can be translated directly to TypeScript. AI tools are highly effective for this structural translation, handling component conversion and state logic with high accuracy. However, AI should not be trusted with navigation patterns or hardware-specific implementations; these require manual intervention.

Workflow:

  1. Audit: Identify performance-critical paths (e.g., camera frame processing) and isolate them.
  2. Translate: Use AI to convert web components to React Native equivalents, focusing on structure and state.
  3. Refine: Manually adjust navigation, gestures, and platform-specific UI patterns.
  4. Bridge: Implement native modules for isolated hot paths.

2. Native Module Implementation

Native modules are written in Swift and exposed to the React Native layer via the Expo Modules API. This allows TypeScript code to invoke native functions asynchronously.

Example: Vision-Based Pose Analysis Module

This module interfaces with Apple's Vision framework to detect human body poses. It accepts image data, processes it natively, and returns keypoint data to the JavaScript layer.

// modules/pose-analysis/ios/PoseAnalysisModule.swift
import ExpoModulesCore
import Vision

public class PoseAnalysisModule: Module {
    public func definition() -> ModuleDefinition {
        Name("PoseAnalysis")
        
        AsyncFunction("analyzeFrame") { (buffer: Data) -> [String: Any] in
            let request = VNDetectHumanBodyPoseRequest()
            let handler = VNImageRequestHandler(data: buffer, options: [:])
            
            do {
                try handler.perform([request])
                guard let observation = request.results?.first else {
                    return ["status": "no_pose", "keypoints": []]
                }
                return serializePoseObservation(observation)
            } catch {
                return ["status": "error", "message": error.localizedDescription]
            }
        }
    }
    
    private func serializePoseObservation(_ observation: VNHumanBodyPoseObservation) -> [String: Any] {
        var points: [[String: Any]] = []
        for joint in observation.allJointsWithoutRoot {
            let point = joint.location
            points.append([
                "identifier": joint.identifier.rawValue,
                "x": point.x,
                "y": point.y,
                "confidence": point.confidence
            ])
        }
        return ["status": "detected", "keypoints": points]
    }
}

Example: TypeScript Consumer Component

The React Native component consumes the native module. Note that the state machine logic remains in TypeScript, demonstrating the separation of concerns.

// app/components/WorkoutSession.tsx
import { useCallback, useRef } from 'react';
import { usePoseAnalysis } from '@/hooks/usePoseAnalysis';
import { CameraView } from '@/components/CameraView';

interface WorkoutSessionProps {
    exerciseId: string;
    onComplete: (stats: WorkoutStats) => void;
}

export function WorkoutSession({ exerciseId, onComplete }: WorkoutSessionProps) {
    const { processFrame, repCount, formScore } = usePoseAnalysis();
    const sessionRef = useRef<WorkoutSessionState>(newSession());

    const handleFrame = useCallback(async (frameData: Uint8Array) => {
        const result = await processFrame(frameData);
        
        if (result.status === 'detected') {
            const stateUpdate = evaluateRepState(sessionRef.current, result.keypoints);
            sessionRef.current = stateUpdate;
            
            if (stateUpdate.completedRep) {
                onComplete({
                    reps: repCount + 1,
                    score: formScore,
                    exerciseId
                });
            }
        }
    }, [processFrame, repCount, formScore, onComplete]);

    return (
        <CameraView 
            onFrameCapture={handleFrame} 
            fps={30}
        />
    );
}

3. Health Data Synchronization

For applications integrating with system health data, a native module handles the HealthKit interactions. This ensures proper permission handling and data formatting.

// modules/health-sync/ios/HealthSyncModule.swift
import ExpoModulesCore
import HealthKit

public class HealthSyncModule: Module {
    private let store = HKHealthStore()
    
    public func definition() -> ModuleDefinition {
        Name("HealthSync")
        
        AsyncFunction("requestAuthorization") { () -> Bool in
            let types: Set<HKSampleType> = [
                HKObjectType.workoutType(),
                HKQuantityType.quantityType(forIdentifier: .dietaryEnergyConsumed)!
            ]
            do {
                try await store.requestAuthorization(toShare: types, read: types)
                return true
            } catch {
                return false
            }
        }
        
        AsyncFunction("saveWorkoutSummary") { (payload: WorkoutPayload) in
            let activityType = HKWorkoutActivityType(rawValue: payload.activityCode) ?? .other
            let workout = HKWorkout(
                activityType: activityType,
                start: payload.startTime,
                end: payload.endTime,
                duration: payload.duration,
                totalEnergyBurned: HKQuantity(unit: .kilocalorie(), doubleValue: payload.calories),
                metadata: ["app_source": "ExpoHybrid"]
            )
            try await store.save(workout)
        }
    }
}

4. Deployment Strategy

Expo provides cloud-based build and submission tools that eliminate the need for local Xcode configuration. This streamlines the CI/CD pipeline for solo developers.

  • Build: eas build --platform ios --profile production handles signing, archiving, and binary generation in the cloud.
  • Submit: eas submit --platform ios --latest uploads the binary directly to App Store Connect.

This approach reduces deployment friction and ensures consistent build environments.

Pitfall Guide

  1. AI Navigation Hallucination

    • Explanation: AI tools often attempt to map web routing libraries directly to Expo Router, resulting in broken navigation structures.
    • Fix: Manually define the route hierarchy. Use AI only for component content, not routing logic.
  2. Bridge Latency Neglect

    • Explanation: Passing large data payloads (like raw image buffers) over the JavaScript bridge causes frame drops and UI jank.
    • Fix: Implement performance-critical processing in native modules. Pass only minimal result data back to JavaScript.
  3. Native Contract Mismatch

    • Explanation: Building the UI before the native module interface is finalized leads to integration issues and refactoring.
    • Fix: Define the TypeScript interface for native modules first. Build the UI against the contract, then implement the native side.
  4. Privacy Label Oversights

    • Explanation: App Store rejections often occur due to mismatches between the app's data collection practices and the privacy nutrition labels.
    • Fix: Audit all data collection paths before submission. Ensure privacy labels accurately reflect on-device processing vs. server transmission.
  5. Late TestFlight Integration

    • Explanation: Delaying TestFlight distribution until the app is feature-complete misses early feedback opportunities.
    • Fix: Ship an alpha build to TestFlight within the first week. Gather feedback on core interactions early.
  6. Web-Centric UI Patterns

    • Explanation: Web UI patterns (hover states, scroll behaviors) do not translate well to mobile touch interfaces.
    • Fix: Test on physical devices frequently. Adapt layouts for one-handed usage and touch targets.
  7. Ignoring Platform Specifics

    • Explanation: Assuming identical behavior across iOS and Android can lead to subtle bugs.
    • Fix: Use platform-specific conditionals where necessary. Test on both platforms regularly.

Production Bundle

Action Checklist

  • Identify Hot Paths: Audit the web codebase to isolate performance-critical functions requiring native implementation.
  • Define Native Contracts: Create TypeScript interfaces for all native modules before implementation.
  • Configure AI Prompts: Set up AI tools with specific instructions for React Native conversion, excluding navigation and hardware APIs.
  • Implement Native Modules: Write Swift modules for hot paths using the Expo Modules API.
  • Audit Privacy Compliance: Review data collection practices and update App Store privacy labels.
  • Setup EAS Pipeline: Configure eas.json for automated builds and submissions.
  • Early TestFlight: Distribute an alpha build to internal testers within the first week.
  • Device Testing: Validate UI and performance on physical iOS devices throughout development.

Decision Matrix

Scenario Recommended Approach Why Cost Impact
Real-time Camera/AI Processing Expo + Native Module Bridge latency unacceptable; native Vision APIs required. Moderate (Swift dev time)
CRUD / Data Entry Forms Pure Expo/RN High code reuse; no performance constraints. Low
Marketing / Static Content WebView / Capacitor Fastest delivery; no native features needed. Very Low
HealthKit / System Integration Expo + Native Module Requires native permissions and APIs. Moderate
Complex Gestures / Animations Expo + Reanimated JS-based animations may lack fluidity. Low-Moderate

Configuration Template

eas.json

{
  "cli": {
    "version": ">= 3.13.0"
  },
  "build": {
    "development": {
      "developmentClient": true,
      "distribution": "internal",
      "ios": {
        "resourceClass": "m-medium"
      }
    },
    "preview": {
      "distribution": "internal",
      "ios": {
        "simulator": false
      }
    },
    "production": {
      "ios": {
        "autoIncrement": true,
        "resourceClass": "m-medium"
      },
      "android": {
        "buildType": "app-bundle"
      }
    }
  },
  "submit": {
    "production": {
      "ios": {
        "appleId": "developer@example.com",
        "ascAppId": "1234567890",
        "appleTeamId": "ABCDE12345"
      }
    }
  }
}

Quick Start Guide

  1. Initialize Project: Run npx create-expo-app@latest my-mobile-app to scaffold a new Expo project.
  2. Install Dependencies: Add required packages: npx expo install expo-camera expo-modules-core.
  3. Create Native Module: Use npx expo-module init modules/my-module to generate a native module template.
  4. Configure Build: Create eas.json with build profiles for development, preview, and production.
  5. Run Build: Execute eas build --platform ios --profile development to generate a development build.

This hybrid strategy provides a robust path for solo engineers to deliver high-performance iOS applications without the overhead of a full native rewrite. By leveraging Expo's native module capabilities and AI-assisted conversion, teams can achieve rapid delivery while maintaining the quality and performance expected by users.