Building Your First React Native App: A Complete Beginner's Guide
Step-by-step guide to building your first React Native app — setup, navigation, data fetching, styling, and deploying to a physical device for testing.
React Native lets JavaScript developers build native mobile apps without learning Swift or Kotlin. The learning curve is real — mobile is different from the web — but the fundamentals are familiar if you know React.
This guide builds a real news reader app from scratch. You'll learn navigation, data fetching, and how React Native's layout system works.
Setup with Expo
Expo is the fastest way to start. It handles the native build tools so you can focus on JavaScript:
npx create-expo-app NewsReader --template blank-typescript
cd NewsReader
npx expo startInstall the Expo Go app on your phone, scan the QR code, and your app runs on your device. Live reload is instant.
Understanding the Differences from Web React
Before writing code, internalize these key differences:
No HTML elements — use RN primitives:
<div>→<View><p>,<span>→<Text><img>→<Image><button>→<TouchableOpacity>or<Pressable><input>→<TextInput><ul>/<li>→<FlatList>
No CSS classes — use StyleSheet.create():
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
paddingHorizontal: 16,
},
})Flexbox by default — but with flexDirection: 'column' as the default (opposite of web).
No px, em, rem — numbers are density-independent pixels:
fontSize: 16, // NOT '16px'
marginBottom: 8, // NOT '8px'Project Structure
NewsReader/
├── app/ # Expo Router pages
│ ├── _layout.tsx # Root layout
│ ├── index.tsx # Home screen
│ └── article/[id].tsx # Article detail
├── components/
│ ├── ArticleCard.tsx
│ └── LoadingSkeletons.tsx
├── hooks/
│ └── useNews.ts
└── types/
└── index.ts
Types
// types/index.ts
export interface Article {
id: string
title: string
description: string
url: string
urlToImage: string | null
publishedAt: string
source: {
name: string
}
}Data Fetching Hook
// hooks/useNews.ts
import { useState, useEffect, useCallback } from 'react'
import type { Article } from '@/types'
const API_KEY = process.env.EXPO_PUBLIC_NEWS_API_KEY
const BASE_URL = 'https://newsapi.org/v2'
export function useNews(category = 'technology') {
const [articles, setArticles] = useState<Article[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [refreshing, setRefreshing] = useState(false)
const fetchNews = useCallback(async (isRefresh = false) => {
if (isRefresh) setRefreshing(true)
else setLoading(true)
setError(null)
try {
const res = await fetch(
`${BASE_URL}/top-headlines?country=us&category=${category}&apiKey=${API_KEY}`
)
if (!res.ok) throw new Error('Failed to fetch news')
const data = await res.json()
setArticles(data.articles)
} catch (e) {
setError(e instanceof Error ? e.message : 'Something went wrong')
} finally {
setLoading(false)
setRefreshing(false)
}
}, [category])
useEffect(() => {
fetchNews()
}, [fetchNews])
return { articles, loading, error, refreshing, refresh: () => fetchNews(true) }
}Article Card Component
// components/ArticleCard.tsx
import { View, Text, Image, TouchableOpacity, StyleSheet } from 'react-native'
import { useRouter } from 'expo-router'
import type { Article } from '@/types'
interface Props {
article: Article
}
export default function ArticleCard({ article }: Props) {
const router = useRouter()
return (
<TouchableOpacity
style={styles.card}
onPress={() => router.push(`/article/${encodeURIComponent(article.url)}`)}
activeOpacity={0.7}
>
{article.urlToImage && (
<Image
source={{ uri: article.urlToImage }}
style={styles.image}
resizeMode="cover"
/>
)}
<View style={styles.content}>
<Text style={styles.source}>{article.source.name}</Text>
<Text style={styles.title} numberOfLines={2}>
{article.title}
</Text>
<Text style={styles.description} numberOfLines={3}>
{article.description}
</Text>
<Text style={styles.date}>
{new Date(article.publishedAt).toLocaleDateString()}
</Text>
</View>
</TouchableOpacity>
)
}
const styles = StyleSheet.create({
card: {
backgroundColor: '#fff',
borderRadius: 12,
marginBottom: 16,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.08,
shadowRadius: 8,
elevation: 3, // Android shadow
overflow: 'hidden',
},
image: {
width: '100%',
height: 200,
},
content: {
padding: 16,
},
source: {
fontSize: 11,
fontWeight: '700',
color: '#3858F6',
textTransform: 'uppercase',
letterSpacing: 0.5,
marginBottom: 6,
},
title: {
fontSize: 16,
fontWeight: '700',
color: '#1a1a1a',
lineHeight: 22,
marginBottom: 8,
},
description: {
fontSize: 14,
color: '#666',
lineHeight: 20,
marginBottom: 8,
},
date: {
fontSize: 12,
color: '#999',
},
})Home Screen with FlatList
// app/index.tsx
import { View, FlatList, StyleSheet, Text, ActivityIndicator } from 'react-native'
import { StatusBar } from 'expo-status-bar'
import ArticleCard from '@/components/ArticleCard'
import { useNews } from '@/hooks/useNews'
export default function HomeScreen() {
const { articles, loading, error, refreshing, refresh } = useNews()
if (loading) {
return (
<View style={styles.centered}>
<ActivityIndicator size="large" color="#3858F6" />
</View>
)
}
if (error) {
return (
<View style={styles.centered}>
<Text style={styles.errorText}>{error}</Text>
</View>
)
}
return (
<View style={styles.container}>
<StatusBar style="dark" />
<FlatList
data={articles}
keyExtractor={item => item.url}
renderItem={({ item }) => <ArticleCard article={item} />}
contentContainerStyle={styles.list}
refreshing={refreshing}
onRefresh={refresh}
showsVerticalScrollIndicator={false}
ListHeaderComponent={
<Text style={styles.header}>Tech News</Text>
}
/>
</View>
)
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#f5f5f5' },
centered: { flex: 1, alignItems: 'center', justifyContent: 'center' },
list: { padding: 16 },
header: {
fontSize: 28,
fontWeight: '800',
color: '#1a1a1a',
marginBottom: 20,
},
errorText: { fontSize: 16, color: '#e53e3e' },
})Common Mistakes for Web Developers
1. Forgetting flex: 1 on containers. Unlike web where divs have natural height, RN Views need flex: 1 to fill their parent.
2. Using % dimensions. Percentages work but cause inconsistencies across screen sizes. Use Dimensions.get('window') or the useWindowDimensions hook for responsive sizes.
3. Not handling the keyboard. On forms, the keyboard covers inputs. Wrap forms in KeyboardAvoidingView:
import { KeyboardAvoidingView, Platform } from 'react-native'
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={{ flex: 1 }}
>
{/* form content */}
</KeyboardAvoidingView>4. Testing only on one platform. iOS and Android have real differences in shadows, fonts, gesture handling, and keyboard behavior. Test on both.
Key Takeaways
- Use Expo for new projects — it eliminates native toolchain complexity
- React Native uses View, Text, Image — not HTML elements
- FlatList is the correct component for long scrollable lists — never use
mapin a ScrollView for large datasets - Test on real devices early — simulators don't capture touch feel or keyboard behavior accurately
shadowColor/shadowOffsetfor iOS,elevationfor Android shadows — they're different APIs

Written by
Sabir KhaloufiFull-stack developer and tech blogger sharing in-depth tutorials on React, Next.js, AI, and modern web development.