April 1, 2026|8 min read

Building for Mobile Without Losing Your Mind

React NativeMobile DevelopmentCross-PlatformExpo

As a web-first engineer, mobile development always felt like a different world. Native iOS and Android development have their own languages, paradigms, build systems, and deployment pipelines. For years, I avoided it.

Then I started building products that needed to be on phones. Real products, not just responsive websites.

The Cross-Platform Question

The first decision is always: native or cross-platform? After evaluating the options (Swift/Kotlin, Flutter, React Native), I landed on React Native. Not because it's perfect, but because the tradeoffs made sense for my situation:

  • I already think in React. The mental model transfers directly.
  • The ecosystem is mature enough that most common problems have solutions.
  • Hot reloading and Expo make the development cycle fast.
  • One codebase for iOS and Android means I can actually ship to both platforms.

Getting Started: It Feels Like React (Until It Doesn't)

If you know React, your first React Native component will feel familiar:

import { View, Text, StyleSheet } from "react-native"

export default function WelcomeCard({ name }: { name: string }) {
  return (
    <View style={styles.card}>
      <Text style={styles.title}>Welcome, {name}</Text>
      <Text style={styles.subtitle}>Let's build something.</Text>
    </View>
  )
}

const styles = StyleSheet.create({
  card: {
    padding: 24,
    backgroundColor: "#1a1a2e",
    borderRadius: 16,
    shadowColor: "#000",
    shadowOffset: { width: 0, height: 4 },
    shadowOpacity: 0.3,
    shadowRadius: 8,
    elevation: 5,
  },
  title: {
    fontSize: 24,
    fontWeight: "700",
    color: "#fff",
  },
  subtitle: {
    fontSize: 16,
    color: "#8892b0",
    marginTop: 8,
  },
})

No div, no p, no CSS files. View replaces div, Text replaces p and span, and StyleSheet.create replaces your stylesheet. The properties look like CSS but use camelCase and accept only a subset of what the web supports. That's the first adjustment: you can't just copy your Tailwind classes over.

Navigation: The Biggest Mental Shift

On the web, routing is URL-based. You navigate to /dashboard and a component renders. In React Native, navigation is stack-based. Screens push onto a stack, and you pop back. It's closer to how native iOS and Android apps work.

import { createNativeStackNavigator } from "@react-navigation/native-stack"
import { NavigationContainer } from "@react-navigation/native"

const Stack = createNativeStackNavigator()

export default function App() {
  return (
    <NavigationContainer>
      <Stack.Navigator initialRouteName="Home">
        <Stack.Screen name="Home" component={HomeScreen} />
        <Stack.Screen name="Profile" component={ProfileScreen} />
        <Stack.Screen
          name="Settings"
          component={SettingsScreen}
          options={{ headerShown: false }}
        />
      </Stack.Navigator>
    </NavigationContainer>
  )
}

Then inside any screen, you navigate by name, not by URL:

function HomeScreen({ navigation }) {
  return (
    <View style={styles.container}>
      <Pressable
        style={styles.button}
        onPress={() => navigation.navigate("Profile", { userId: "123" })}
      >
        <Text style={styles.buttonText}>View Profile</Text>
      </Pressable>
    </View>
  )
}

No <Link href="/profile/123">. You call navigation.navigate() with the screen name and params. Getting used to stacks, tabs, and drawers takes time, but once it clicks, the pattern is clean.

Platform-Specific Code

Even with cross-platform code, iOS and Android behave differently in places. React Native gives you a clean way to handle this:

import { Platform, StyleSheet } from "react-native"

const styles = StyleSheet.create({
  header: {
    paddingTop: Platform.OS === "ios" ? 44 : 24,
    backgroundColor: Platform.select({
      ios: "#1a1a2e",
      android: "#0f0f23",
    }),
  },
  shadow: Platform.select({
    ios: {
      shadowColor: "#000",
      shadowOffset: { width: 0, height: 2 },
      shadowOpacity: 0.25,
      shadowRadius: 4,
    },
    android: {
      elevation: 4,
    },
  }),
})

iOS uses shadow properties. Android uses elevation. Status bar heights differ. Keyboard behavior differs. These are small things individually, but they accumulate. Having Platform.select() built into the framework makes it manageable.

Lists and Performance

On the web, you render a list with .map() and call it a day. In mobile, long lists need virtualization out of the box because you're working with constrained memory and smaller screens.

import { FlatList, View, Text } from "react-native"

interface Project {
  id: string
  title: string
  status: string
}

function ProjectList({ projects }: { projects: Project[] }) {
  return (
    <FlatList
      data={projects}
      keyExtractor={(item) => item.id}
      renderItem={({ item }) => (
        <View style={styles.row}>
          <Text style={styles.title}>{item.title}</Text>
          <Text style={styles.status}>{item.status}</Text>
        </View>
      )}
      ItemSeparatorComponent={() => <View style={styles.separator} />}
      ListEmptyComponent={
        <Text style={styles.empty}>No projects yet.</Text>
      }
    />
  )
}

FlatList only renders what's visible on screen. It handles scroll performance, recycling views, and memory management. On the web, you'd need to bring in react-window or react-virtualized. Here, it's a core component.

Expo: The Build System That Makes It Possible

Expo is what makes React Native approachable for a web developer. Without it, you're managing Xcode, Android Studio, Gradle configs, and CocoaPods. With Expo, the development loop looks like:

# Start the dev server
npx expo start

# Build for both platforms
eas build --platform all

# Deploy an over-the-air update (no app store review)
eas update --branch production

The over-the-air updates via EAS are a game changer. You push a JS bundle update and users get it without downloading a new version from the App Store. For bug fixes and small changes, you skip the review process entirely.

What Surprised Me

  • Navigation is its own beast. React Navigation works, but the mental model is different from web routing. Stacks, tabs, drawers. It takes time to internalize.
  • Platform differences are real. Even with cross-platform code, you'll hit moments where iOS and Android behave differently. Keyboard handling, status bars, gestures. The details matter.
  • Performance is good enough. For the majority of applications, React Native performance is not the bottleneck. Your architecture decisions matter more than the framework choice.
  • Debugging is different. React DevTools work, but you'll also use Flipper, platform-specific logs, and occasionally Xcode's console. The tooling ecosystem is more fragmented than the web.

My Approach

I'm not trying to become a mobile specialist. I'm building a toolkit that lets me ship to mobile when the product requires it. That means:

  • Keeping shared business logic in TypeScript packages
  • Using Expo for build and deployment tooling
  • Leaning on well-maintained community libraries instead of building from scratch
  • Writing platform-specific code only when the default behavior isn't right

The Takeaway

Mobile development doesn't have to be a separate career path. With the right tools and the right expectations, a web engineer can build quality mobile experiences. The key is being honest about what you know, what you don't, and where the framework can carry you.

If you're a React developer considering mobile, start with Expo and build something small. The gap between web and mobile is smaller than it's ever been.