A React Native screen is built from components. A few built-in ones cover most needs, and you combine them. This lesson teaches them and the idea of state, then builds your first complete app.
The core components
- View: a box that groups and positions other things.
- Text: all visible text. Text cannot sit directly inside a View.
- TextInput: a box to type in. Use
valueandonChangeText.keyboardTypepicks the keyboard. - Pressable: anything that can be tapped. It replaces the old TouchableOpacity for most uses.
- Image: shows a picture from a file or a web address. Remote images need a width and a height.
- ScrollView and FlatList: ScrollView shows everything at once. FlatList draws only the rows on screen, so use it for long lists.
- Switch, ActivityIndicator, Modal: a toggle, a spinner and a pop-up panel.
- SafeAreaView: keeps content away from notches and system bars.
Styling and Flexbox
Styles are JavaScript objects, written with StyleSheet.create. Layout uses Flexbox, with two differences from the web: the default direction is vertical (column), and sizes are numbers without units. flexDirection: "row" puts children side by side. justifyContent places them along the main direction, alignItems across it, flex: 1 makes an item take the available space, and gap adds space between children.
State: data that changes the screen
useState gives a component a value that can change. When you call the setter, React runs the component again with the new value and updates only what changed.
const [count, setCount] = useState(0);
<Pressable onPress={() => setCount(count + 1)}>
<Text>Clicked {count} times</Text>
</Pressable>
Never change state directly (count++). Always call the setter with a new value. When the new value depends on the old one, use the function form setCount((c) => c + 1).
Project 1: Tip Calculator
The user types a bill, chooses a tip, sets how many people share and sees the totals. All the maths lives in a plain function in its own file, so you can test it without a phone.
No extra libraries are needed for this project.
my-app/
App.tsx the screen
tip.ts the calculation, with no React in it
__tests__/
tip.test.ts
TipScreen.test.tsx
`tip.ts`
export type TipResult = { tip: number; total: number; perPerson: number };
export function calculateTip(bill: number, tipPercent: number, people: number, roundUp: boolean): TipResult {
if (bill < 0) throw new Error("bill must not be negative");
if (people < 1) throw new Error("at least one person");
let tip = (bill * tipPercent) / 100;
if (roundUp) tip = Math.ceil(tip);
const total = bill + tip;
return { tip, total, perPerson: total / people };
}
export const money = (n: number) => n.toFixed(2);
`App.tsx`
import { useState } from "react";
import { SafeAreaView, View, Text, TextInput, Switch, Pressable, StyleSheet } from "react-native";
import { calculateTip, money } from "./tip";
export default function App() {
const [billText, setBillText] = useState("");
const [tipPercent, setTipPercent] = useState(15);
const [people, setPeople] = useState(1);
const [roundUp, setRoundUp] = useState(false);
const bill = parseFloat(billText) || 0;
const result = calculateTip(bill, tipPercent, people, roundUp);
return (
<SafeAreaView style={styles.screen}>
<Text style={styles.title}>Tip Calculator</Text>
<Text style={styles.label}>Bill amount</Text>
<TextInput
style={styles.input}
value={billText}
onChangeText={setBillText}
keyboardType="decimal-pad"
placeholder="0.00"
/>
<Text style={styles.label}>Tip: {tipPercent}%</Text>
<View style={styles.row}>
{[10, 15, 20, 25].map((p) => (
<Pressable
key={p}
onPress={() => setTipPercent(p)}
style={[styles.chip, p === tipPercent && styles.chipOn]}
>
<Text style={p === tipPercent ? styles.chipTextOn : styles.chipText}>{p}%</Text>
</Pressable>
))}
</View>
<View style={styles.row}>
<Text style={[styles.label, styles.grow]}>People: {people}</Text>
<Pressable style={styles.round} onPress={() => setPeople(Math.max(1, people - 1))}>
<Text style={styles.roundText}>-</Text>
</Pressable>
<Pressable style={styles.round} onPress={() => setPeople(people + 1)}>
<Text style={styles.roundText}>+</Text>
</Pressable>
</View>
<View style={styles.row}>
<Text style={[styles.label, styles.grow]}>Round tip up</Text>
<Switch value={roundUp} onValueChange={setRoundUp} />
</View>
<View style={styles.card}>
<Text style={styles.line}>Tip: {money(result.tip)}</Text>
<Text style={styles.line}>Total: {money(result.total)}</Text>
<Text style={styles.big}>Each person pays {money(result.perPerson)}</Text>
</View>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
screen: { flex: 1, padding: 20, backgroundColor: "#f5f6fa" },
title: { fontSize: 28, fontWeight: "700", marginBottom: 16 },
label: { fontSize: 16, marginTop: 12, marginBottom: 6 },
input: { backgroundColor: "#fff", borderRadius: 10, padding: 12, fontSize: 18, borderWidth: 1, borderColor: "#d0d4dc" },
row: { flexDirection: "row", alignItems: "center", gap: 8 },
grow: { flex: 1 },
chip: { paddingVertical: 8, paddingHorizontal: 14, borderRadius: 20, backgroundColor: "#e3e6ee" },
chipOn: { backgroundColor: "#3b5bdb" },
chipText: { color: "#222" },
chipTextOn: { color: "#fff", fontWeight: "600" },
round: { width: 36, height: 36, borderRadius: 18, backgroundColor: "#3b5bdb", alignItems: "center", justifyContent: "center" },
roundText: { color: "#fff", fontSize: 20 },
card: { marginTop: 24, padding: 16, borderRadius: 12, backgroundColor: "#fff" },
line: { fontSize: 16, marginBottom: 4 },
big: { fontSize: 20, fontWeight: "700", marginTop: 8 },
});
How it works
- Four values are in state: the bill text, tip percent, number of people and the round-up switch.
- The bill stays as text because the user types it.
parseFloat(billText) || 0turns empty or invalid text into 0. - The result is calculated on every render, so it can never be out of date.
- The array
[10, 15, 20, 25].map(...)creates one button for each tip choice. Each needs a uniquekey.
Expected output
The screen shows the title, a bill box, four tip buttons with 15% highlighted, a people row showing 1, a round-up switch and a card. Type 100: the card shows Tip 15.00, Total 115.00 and Each person pays 115.00. Press + twice: each person pays 38.33. With a bill of 33 and 15%, the tip is 4.95, and 5.00 when round up is on.
Your task
- Create the project, add the two files and run the app.
- Check the three examples.
- Add a custom tip box where the user types their own percent.
- Run the tests in the testing lesson later and add one for your custom tip.
How to check it worked
- 100 at 15% gives a total of 115.00.
- The numbers change as you tap, with no calculate button.
- You can explain what useState does and why we never change state directly.