React Native for Web Developers: A Practical Guide to Mobile UI, Performance & Pixel Density

Why React Native Is a Great Choice for Web Developers Moving from React
React Native is an excellent choice for web developers with React experience who want to build their first mobile app.
Although React and React Native share a lot in common, there are important differences between web platforms and native apps. This article highlights the most common mistakes and questions developers face when transitioning from React to React Native.
Why Choose React Native?
Let’s start by exploring the reasons why React Native is a solid choice for mobile app development.
Familiar Technologies
With React Native, almost the entire app code is written in JavaScript (or more often, TypeScript). This means developers can leverage many of the programming patterns and libraries they already know from web development.
Native Code Under the Hood
Even though most of the app is written in JavaScript, React Native apps are essentially native. React code is mapped to real native primitives for each platform. This is crucial for app performance and appearance.
Cross-Platform Code
React Native allows you to create both iOS and Android apps from the same codebase. Using tools like Expo Router and API routers, you can configure redirects to websites and servers.
Why Choose Expo
Expo is a React Native framework, similar to how Next.js is a framework for React.
React Native lets you run React code directly on iOS and Android platforms. It also provides fundamental components like Text
, View
, and TextInput
. However, many features most apps need — such as navigation, push notifications, device camera access, data persistence between app launches, or publishing to app stores — are not included out of the box. This is intentional to keep the core framework lightweight and maintainable by a small team.
Since key features aren’t bundled with React Native, developers used to rely on numerous community libraries. Over almost 10 years, the React Native ecosystem has grown massively, making it challenging to choose the right libraries — especially when starting a new project without prior experience.
Ideally, you want a set of well-supported, commonly used tools — and that’s exactly what Expo provides.
Expo is a platform built around React Native. It offers a comprehensive set of tools and services bridging the gap between React Native’s core features and everything needed to build production-ready apps. The Expo SDK is an extended standard library that grants access to a wide range of native APIs not included in React Native itself (camera, video, notifications, and more). Expo also provides a CLI for creating projects, an environment for learning and prototyping, file-based navigation, tools for managing builds, publishing apps to stores, configuring updates, and even creating custom native modules. Plus, you can still use most other third-party React Native libraries.
It’s no surprise that React Native officially recommends using frameworks like Expo for new projects. The reasoning is simple: either use a ready-made framework or build your own. And building your own from scratch is rarely the best option for most React Native teams.
UI Primitives in React and React Native
React Native code looks a lot like React code, but there are some differences, especially regarding UI components.
Here’s how you might render text on the web:
export function MyTextComponent() {
return (
<div>Hello, web!</div>
);
}
React Native Core UI Components and Their Web Equivalents
View
Closest web equivalent — div
.
In React Native, View
is equivalent to a div
and is used similarly: to create layouts (containers) and apply styling.
ScrollView
Closest web equivalent — div
.
Web pages scroll by default, but native apps do not. To make a page scrollable in React Native, wrap it in a ScrollView
(or other virtualized lists like FlatList
or SectionList
).
Text
Closest web equivalent — p
.
Any text displayed in React Native must be wrapped in a Text
component.
Image
Closest web equivalent — img
.
The Image
component lets you render both remote and local images. The API differs slightly from the web. For example, loading an image from a URL looks like this:
import { Image } from "react-native";
export function MyImage() {
return <Image source={{ uri: "https://domain.com/static/my-image.png" }} />;
}
Loading from a local file looks like this:
import { Image } from "react-native";
const imageSource = require("../assets/my-image.png");
export function MyImage() {
return <Image source={imageSource} />;
}
In most real-world React Native apps, third-party libraries like FastImage or Expo Image are used instead of this basic component. These provide extra features like styling, caching, and support for image formats.
FlatList Closest web equivalent — array.map().
For rendering large lists of items (like an Instagram feed or email inbox), the web usually maps over an array. On native platforms, this is inefficient and should be avoided. Instead, use FlatList — a virtualized list with built-in optimizations such as deferred rendering of offscreen data and rerendering when data changes.
import { Text, FlatList, View } from "react-native";
const posts = [
{ id: "1", name: "Post 1" },
{ id: "2", name: "Post 2" },
];
export function MyList() {
return (
<FlatList
data={posts}
renderItem={({ item }) => <Text>{item.name}</Text>}
/>
);
}
TextInput Closest web equivalent — .
TextInput is used for any text input. The main difference from is that TextInput only accepts text (and numbers). Although TextInput has an onChange handler, typically onChangeText is used, which returns only the updated text string.
import { useState } from "react";
import { TextInput } from "react-native";
export function MyInput() {
const [value, setValue] = useState("");
return <TextInput value={value} onChangeText={setValue} />;
}
TouchableOpacity Closest web equivalent — button.
To make UI elements respond to presses, wrap them in TouchableOpacity. This component automatically highlights the pressed area, with the opacity of the highlight adjustable via the activeOpacity prop.
import { TouchableOpacity, Text } from "react-native";
export function MyButton() {
const onPress = () => {
console.log("Pressed!");
};
return (
<TouchableOpacity onPress={onPress} activeOpacity={0.8}>
<Text>Button Text</Text>
</TouchableOpacity>
);
}
Additional React Native Components and Styling Basics
Pressable
Closest web equivalent — button
.
Like TouchableOpacity
, Pressable
is used to create buttons. It handles presses and offers more flexibility than TouchableOpacity
, such as separate handling of different press states.
Switch
Closest web equivalent — <input type="radio" />
Switch
is a toggle component that switches a value between true
and false
. Its appearance is unique to each mobile platform (iOS or Android) due to platform-specific implementations.
import { useState } from "react";
import { Switch } from "react-native";
export function MySwitch() {
const [value, setValue] = useState(false);
return (
<Switch
value={value}
onValueChange={(value) => setValue(value)}
trackColor={{ true: "pink" }}
/>
);
}
Styling in React Native
Developers familiar with CSS will find styling in React Native fairly straightforward. Most CSS properties are supported, but some differences exist.
No Global Styles
In React Native, styles are defined directly on components via the style prop. There are no global styles unless you use special styling libraries.
To share styles across components, you can create a separate theme.js file and import styles where needed:
```jsx
import { View, Text, StyleSheet } from "react-native";
export function MyComponent() {
return (
<View style={styles.container}>
<Text style={styles.greeting}>Set Reminder</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
padding: 24,
},
greeting: {
fontSize: 24,
},
});
Flexbox with Nuances React Native uses Flexbox for layout, similar to the web, but with some differences:
All elements default to display: flex
flexDirection defaults to column (not row)
alignContent defaults to flex-start (not stretch)
flexShrink defaults to 0 (not 1)
The flex property accepts only a single numeric value
Styling Libraries React Native’s built-in styling works well for many apps, and many projects avoid third-party styling libraries altogether.
However, some libraries offer convenient alternative approaches:
NativeWind — TailwindCSS for React Native
Styled Components — CSS-in-JS styling
Tamagui — comprehensive cross-platform UI and styling framework
Common Mistakes by React Native Beginners Using array.map() for Rendering Lists This approach is common in web development. For example, rendering a list of posts might look like this:
export function Posts() {
const posts = usePosts();
return (
<div>
{posts.map(post => <div key={post.id}>{post.name}</div>)}
</div>
);
}
However, in native development, this approach is discouraged due to performance issues. Instead, use virtualized list components like FlatList:
import { FlatList, View, Text } from "react-native";
export function Posts() {
const posts = usePosts();
return (
<FlatList
data={posts}
renderItem={({ item }) => (
<View>
<Text>{item.name}</Text>
</View>
)}
/>
);
}
Virtualized lists automatically optimize performance by not rendering items outside the visible viewport.
An even more optimized alternative to FlatList is FlashList.
Using the React Native Button
Component
React Native provides a Button
component, but it fits only about 1% of use cases because it does not allow style customization. It’s often better to just forget about using it.
The recommended way to create buttons in React Native is to wrap the button content inside TouchableOpacity
or Pressable
:
import { TouchableOpacity, Text } from "react-native";
export function MyButton() {
const onPress = () => {
console.log("Pressed!");
};
return (
<TouchableOpacity onPress={onPress} activeOpacity={0.8}>
<Text>Button Text</Text>
</TouchableOpacity>
);
}
Do Not Store Secret Data in the App Code
In web development, there is a clear separation between client and server — frontend and backend. Confidential information must never be stored in client-side code because it is accessible to attackers who can study the source code or intercept network traffic.
A React Native app is a client application! Although access to app code and network traffic is less straightforward than in a web browser, it is still possible. Therefore, it is not recommended to embed any sensitive data in the app code. Data such as Firebase configuration or other client identifiers used for app identification in various backend services can be stored in the app code. However, any secret API keys or other critical credentials should never be included in client code.
If your app needs to interact with APIs requiring sensitive data that cannot be embedded, solve this like with websites: deploy an API server and have the app communicate with it.
When using Expo Router, you can write API code inside your React Native app and deploy it separately using API routes.
New Native-Specific Concepts
When moving from web app development to native mobile apps, some new concepts must be learned.
Build Signing
Both iOS and Android have built-in security mechanisms that prevent installation of apps from unknown sources. To install an app on a device, it must be signed with credentials.
The signing process differs between iOS and Android but is fundamentally the same. Android uses a Keystore created locally; iOS requires a Provisioning Profile and Signing Certificate, available only with a paid Apple developer account. Using the EAS CLI for builds automates credential creation and syncing.
Note: For development, you can use Expo Go, which bypasses signing restrictions since it is a publicly released app. This allows fast development without native configuration setup. However, native code cannot be changed in Expo Go, so for production, use dedicated development builds.
Deep Linking
In web development, linking to a specific page is trivial due to unique URLs. In mobile apps, it’s more complex.
A scheme (e.g., myapp://
) is registered, which the app listens for. A link like myapp://my/page
opens the app and passes my/page
as a deep link instruction. The app processes this and routes the user accordingly. Expo Router supports this natively via filesystem-based routing. React Navigation also supports it but requires more setup.
Refer to official docs for details on linking and deep linking.
Gestures and Touches
Native-feeling apps rely on gesture-based interactions: swipe, long press, pinch zoom, etc.
React Native’s TouchableOpacity
and Pressable
provide props like onLongPress
. For more advanced gestures, use libraries like React Native Gesture Handler.
Haptic Feedback
Haptics are subtle vibrations in response to user actions, enhancing the native feel. Use sparingly for key interactions like liking a post or adding an item to cart.
On-Screen Keyboard
Ensure good interaction with the on-screen keyboard:
- Use
KeyboardAvoidingView
so inputs remain visible when the keyboard appears. - Keyboard appears automatically on input focus but can also be shown/hidden programmatically.
- Use
keyboardShouldPersistTaps
onScrollView
to control if taps outside dismiss the keyboard. - Use
keyboardType
onTextInput
to specify keyboard layout (e.g., numeric). - Other useful props:
autoCapitalize
,autoComplete
,autoCorrect
,returnKeyType
.
Animation
React Native lacks CSS animations. Instead, use Layout Animation (experimental on Android, use with caution) and the built-in Animated
API.
For more complex animations, prefer the Reanimated library, a React hook-based animation solution with a simpler API than Animated
.
Pixel Density
Pixel density is a characteristic that shows how many pixels fall on a unit area displayed on the screen. In React Native, traditional units like px
, em
, or rem
are not used for styling. Instead, a special unit called dp
(display point) is applied, and the actual number of pixels per dp
can vary significantly across devices. You can get information about a device’s pixel density using PixelRatio
.
This is especially important when working with images. You need to ensure high image quality while rendering only images sized appropriately for their display area. For example, if an image has a width and height of 100 dp on a modern iPhone with a pixel density of 3, its actual size should be 300x300 px. On an Android device with a density of 1.5, an image of 150x150 px is sufficient. Rendering a larger image in a smaller area works but is resource-intensive (which can reduce performance and cause crashes). Conversely, displaying a too-small image in a large area leads to pixelation.
Therefore, when using remote images, it is recommended to resize them on the server beforehand. For static images bundled with the app, you can use suffixes like @2x
and @3x
to provide versions for different pixel densities.
Conclusion
Throughout this article, we explored the core principles, structure, and practical aspects of the topic at hand. From foundational concepts to real-world implications, we aimed to provide a clear and accessible overview that bridges theory and practice.
Understanding these ideas is essential for anyone looking to deepen their expertise and apply knowledge effectively in real-world scenarios. Whether you're a beginner or an experienced professional, mastering these fundamentals lays the groundwork for more advanced development and confident decision-making.
As technology continues to evolve, staying informed and continuously learning remains key. We encourage you to experiment, ask questions, and keep building — because progress comes through practice.
- Why Choose React Native?
- Why Choose Expo
- UI Primitives in React and React Native
- React Native Core UI Components and Their Web Equivalents
- Additional React Native Components and Styling Basics
- Using the React Native `Button` Component
- Do Not Store Secret Data in the App Code
- New Native-Specific Concepts
- Pixel Density