CodeWithSabir
HomeAIDevOpsNext.jsMobile DevelopmentWeb Development
CodeWithSabir
  • Home
  • AI
  • DevOps
  • Next.js
  • Mobile Development
  • Web Development
  • About
  • Contact
CodeWithSabir

In-depth articles, tutorials, and guides on web development, React, Next.js, AI, and modern programming practices.

Topics

  • AI
  • DevOps
  • Next.js
  • Mobile Development
  • Web Development

Company

  • About
  • Contact
  • Privacy Policy
  • Terms

© 2026 CodeWithSabir. All rights reserved.

Built with SabirSoft.com

Home/Mobile Development/Building Your First React Native App: A Complete Beginner's Guide
Mobile Development

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.

Sabir KhaloufiSabir KhaloufiJanuary 20, 20263 min read

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:

bash
npx create-expo-app NewsReader --template blank-typescript
cd NewsReader
npx expo start

Install 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():

typescript
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:

typescript
fontSize: 16,    // NOT '16px'
marginBottom: 8, // NOT '8px'

Project Structure

code
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

typescript
// 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

typescript
// 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

tsx
// 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

tsx
// 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:

tsx
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 map in a ScrollView for large datasets
  • Test on real devices early — simulators don't capture touch feel or keyboard behavior accurately
  • shadowColor/shadowOffset for iOS, elevation for Android shadows — they're different APIs
#react native#expo#mobile app#beginner#javascript
Share:
Sabir Khaloufi — author photo

Written by

Sabir Khaloufi

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

On this page

  • Setup with Expo
  • Understanding the Differences from Web React
  • Project Structure
  • Types
  • Data Fetching Hook
  • Article Card Component
  • Home Screen with FlatList
  • Common Mistakes for Web Developers
  • Key Takeaways

Related Articles

Mobile Development

React Native vs Flutter in 2026: Which One Should You Actually Use?

An honest, experience-based comparison of React Native and Flutter in 2026. Covers performance, developer experience, ecosystem, and which one fits different team types.

Sabir KhaloufiJanuary 25, 20264 min read
Mobile Development

State Management in React Native: Zustand vs Redux Toolkit

A practical comparison of Zustand and Redux Toolkit for React Native apps — when to use each, real-world patterns, async actions, and persistence with MMKV storage.

Sabir KhaloufiJanuary 15, 20263 min read
Mobile Development

How to Deploy a React Native App to App Store and Google Play

A step-by-step guide to publishing a React Native (Expo) app to the Apple App Store and Google Play Store — from build configuration to store listing and approval.

Sabir KhaloufiJanuary 10, 20264 min read