DEV Community

Hugo Rus
Hugo Rus

Posted on

7 Things That Break When You Convert a Web App to React Native

  • There is no div, no span, and no cascading CSS. Every layout primitive changes.
  • localStorage, window, document, and most browser APIs simply don't exist.
  • flexDirection defaults to column, not row. This one silently breaks every layout you wrote.
  • Routing, image handling, forms, and scrolling all need native equivalents.
  • None of it is hard individually. It's the volume that kills weekend projects.

AI web app builders got very good very fast. You can describe an app to Lovable, v0, or Bolt and have a working web product the same afternoon.

Then you try to put it on the App Store, and you discover the thing nobody mentions: a web app and a native app share a language, not a platform. They both run JavaScript. Almost nothing else transfers.

I've done this conversion enough times to know exactly where it hurts. Here are the seven things that break, in roughly the order you'll hit them.

1. Every HTML element is gone

There is no div. No span. No p, h1, button, ul, or img.

React Native has its own primitives, and the mapping isn't one-to-one:

// Web
<div className="card">
  <h2>Title</h2>
  <p>Body text</p>
  <img src={url} />
</div>

// React Native
<View style={styles.card}>
  <Text style={styles.title}>Title</Text>
  <Text>Body text</Text>
  <Image source={{ uri: url }} />
</View>
Enter fullscreen mode Exit fullscreen mode

The important gotcha: all text must be wrapped in <Text>. A bare string inside a <View> throws. On web you can drop text anywhere; here it's a hard error, and it's the single most common first crash.

2. CSS doesn't cascade — and half of it doesn't exist

React Native uses a JavaScript style object, not a stylesheet. That means:

  • No cascade. A style on a parent doesn't inherit down to children. Set fontSize on a View and the Text inside ignores it.
  • No selectors. No classes, no descendants, no :hover, no media queries.
  • No units. Numbers are density-independent pixels. padding: 16, not padding: '16px'.
const styles = StyleSheet.create({
  card: {
    padding: 16,
    borderRadius: 12,
    backgroundColor: '#fff',
  },
});
Enter fullscreen mode Exit fullscreen mode

Also missing: float, grid, position: fixed, and most of the box model shorthand you're used to. Flexbox is the layout system — essentially the only one.

3. flexDirection defaults to column

This is the one that quietly wrecks everything.

On the web, display: flex defaults to flex-direction: row. In React Native, every View is already flex, and the default direction is column.

So every horizontal row you built on the web stacks vertically after the port, and you won't get an error — just a layout that looks wrong everywhere at once.

// You have to be explicit
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
Enter fullscreen mode Exit fullscreen mode

Budget an afternoon just for this.

4. Browser APIs don't exist

No window. No document. No localStorage. No fetch cookies, no DOM measurement, no alert().

The replacements:

Web React Native
localStorage AsyncStorage (or expo-secure-store for anything sensitive)
document.querySelector refs
window.location Expo Router / React Navigation
alert() Alert.alert() from react-native
CSS media queries useWindowDimensions()

fetch does survive, which is a small mercy — your API layer mostly ports as-is.

5. Routing is a different model

URL-based routing doesn't exist natively. There's no address bar and no history object.

Expo Router gets you closest to a web mental model — it's file-based, so app/profile/[id].tsx maps to /profile/42 — but you still have to think in stacks and tabs rather than pages. Back isn't "previous URL," it's "pop the stack."

Deep links also need real configuration on both platforms (URL schemes, Universal Links on iOS, App Links on Android). That's a genuine chunk of work with no web equivalent.

6. Scrolling and lists are explicit

On the web, content overflows and the browser gives you a scrollbar for free. In React Native, nothing scrolls unless you say so.

<ScrollView>{/* short content */}</ScrollView>

<FlatList
  data={items}
  keyExtractor={(item) => item.id}
  renderItem={({ item }) => <Row item={item} />}
/>
Enter fullscreen mode Exit fullscreen mode

And use FlatList (or FlashList) for anything long — ScrollView renders every child at once and will tank your frame rate on a list of 500 items.

7. Forms and keyboards need real work

<input> becomes <TextInput>, and it needs configuration the browser handled implicitly: keyboardType, autoCapitalize, autoComplete, secureTextEntry, returnKeyType.

Then there's the keyboard itself. On mobile it physically covers the bottom of the screen, so your submit button disappears behind it unless you wrap the form in KeyboardAvoidingView with different behavior values per platform.

There is no web equivalent to this problem, and it catches everyone once.

What this actually adds up to

Individually, none of these are hard. A competent React dev can work through any one of them in an hour.

The problem is that there are seven of them, and they compound — plus certificates, provisioning profiles, screenshots at five device sizes, a privacy nutrition label, and a reviewer who might say no anyway.

That's the honest reason so many AI-built web apps never reach a store. The build was a weekend. The port and the submission are a different project entirely.

If you'd rather not do that project, RapidNative converts Lovable apps into real React Native + Expo builds and handles the full App Store and Play Store submission — signing, screenshots, metadata included. You get the source code and keep ownership of it, and if your app uses Supabase, the native build points at the same project with no migration.


Which of these seven got you first? For me it was flexDirection — I spent two hours convinced my styles weren't applying at all.

Top comments (0)