- There is no
div, nospan, and no cascading CSS. Every layout primitive changes. -
localStorage,window,document, and most browser APIs simply don't exist. -
flexDirectiondefaults tocolumn, notrow. 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>
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
fontSizeon aViewand theTextinside ignores it. -
No selectors. No classes, no descendants, no
:hover, no media queries. -
No units. Numbers are density-independent pixels.
padding: 16, notpadding: '16px'.
const styles = StyleSheet.create({
card: {
padding: 16,
borderRadius: 12,
backgroundColor: '#fff',
},
});
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' }}>
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} />}
/>
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)