React Native β Complete Beginner Tutorial
6 modules Β· 17 examples Β· Click βΆ Run Program to see the phone screen output
π‘ How to use this tutorial
Read each explanation, study the code, then click βΆ Run Program to see the phone screen output.
Core Components
React Native does not use HTML β it uses its own components that map to real native UI elements. View is like a div, Text is like a p tag, and Image displays pictures. All styling is done with JavaScript using StyleSheet.create() β similar to CSS but in camelCase (backgroundColor instead of background-color). The SafeAreaView component keeps content away from phone notches and status bars.
Your First React Native Component
Every React Native app is a JavaScript function that returns JSX β a mix of JavaScript and XML-like markup. Instead of HTML tags you use React Native components like View (a container) and Text (for all text). StyleSheet.create() defines your styles as a JavaScript object. Wrap everything in SafeAreaView so content does not sit under the phone notch.
import { View, Text, StyleSheet, SafeAreaView } from 'react-native';
export default function App() {
return (
<SafeAreaView style={styles.safe}>
<View style={styles.container}>
<Text style={styles.heading}>Hello, React Native!</Text>
<Text style={styles.body}>
One codebase β iOS and Android.
</Text>
<Text style={styles.note}>
Built with JavaScript + React
</Text>
</View>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
safe: { flex: 1, backgroundColor: '#060912' },
container: { flex: 1, justifyContent: 'center',
alignItems: 'center', padding: 24 },
heading: { fontSize: 28, fontWeight: '800',
color: '#fff', marginBottom: 12 },
body: { fontSize: 16, color: '#94a3b8',
textAlign: 'center', marginBottom: 8 },
note: { fontSize: 13, color: '#61DAFB' },
});In React Native every unit of size is a "density-independent pixel" (dp) β not a CSS pixel. This means fontSize: 16 looks the same physical size on a small phone and a large tablet. You never write "px" in React Native styles.
View, Text & Image
View is the building block of every React Native UI β it is a container that you nest and style freely. Text must be the parent of all text content. Image displays local or remote images. The resizeMode prop controls how an image fits its container.
import { View, Text, Image, StyleSheet } from 'react-native';
export default function ProfileCard() {
return (
<View style={styles.card}>
<Image
source={{ uri: 'https://placekitten.com/100/100' }}
style={styles.avatar}
resizeMode="cover"
/>
<View style={styles.info}>
<Text style={styles.name}>Alice Smith</Text>
<Text style={styles.role}>React Native Developer</Text>
<View style={styles.badge}>
<Text style={styles.badgeText}>Pro Member</Text>
</View>
</View>
</View>
);
}
const styles = StyleSheet.create({
card: { flexDirection: 'row', padding: 16,
backgroundColor: '#1e293b', borderRadius: 12,
alignItems: 'center', gap: 14 },
avatar: { width: 64, height: 64, borderRadius: 32 },
info: { flex: 1 },
name: { fontSize: 18, fontWeight: '700', color: '#fff' },
role: { fontSize: 13, color: '#94a3b8', marginTop: 2 },
badge: { marginTop: 6, backgroundColor: '#61DAFB20',
paddingHorizontal: 8, paddingVertical: 2,
borderRadius: 4, alignSelf: 'flex-start' },
badgeText: { fontSize: 11, color: '#61DAFB', fontWeight: '700' },
});flexDirection: "row" makes children sit side by side (horizontal). The default is "column" (vertical stack). This is Flexbox β React Native uses it for ALL layout, just like CSS Flexbox but without Grid.
ScrollView & StyleSheet
By default a View does not scroll β if content overflows, it is hidden. Wrap your content in ScrollView to make it scrollable. Use StyleSheet.create() instead of inline styles β it validates your style names, provides autocomplete in editors, and is slightly more performant.
import { ScrollView, View, Text, StyleSheet } from 'react-native';
const ITEMS = [
{ id: 1, title: 'Introduction to React Native', duration: '5 min' },
{ id: 2, title: 'Components & JSX', duration: '8 min' },
{ id: 3, title: 'StyleSheet & Flexbox', duration: '10 min' },
{ id: 4, title: 'State with useState', duration: '7 min' },
{ id: 5, title: 'FlatList & SectionList', duration: '9 min' },
{ id: 6, title: 'Navigation Basics', duration: '12 min' },
{ id: 7, title: 'Fetching Data', duration: '11 min' },
];
export default function LessonList() {
return (
<ScrollView style={styles.scroll}
contentContainerStyle={styles.content}>
<Text style={styles.heading}>Course Lessons</Text>
{ITEMS.map((item) => (
<View key={item.id} style={styles.row}>
<Text style={styles.num}>{item.id}</Text>
<Text style={styles.title}>{item.title}</Text>
<Text style={styles.dur}>{item.duration}</Text>
</View>
))}
</ScrollView>
);
}
const styles = StyleSheet.create({
scroll: { flex: 1, backgroundColor: '#060912' },
content: { padding: 20 },
heading: { fontSize: 22, fontWeight: '800', color: '#fff', marginBottom: 16 },
row: { flexDirection: 'row', alignItems: 'center',
paddingVertical: 12, borderBottomWidth: 1,
borderBottomColor: '#1e293b', gap: 12 },
num: { width: 24, color: '#61DAFB', fontWeight: '700' },
title: { flex: 1, color: '#e2e8f0', fontSize: 14 },
dur: { color: '#64748b', fontSize: 12 },
});Use ScrollView when you have a small, known amount of content. For long lists of dynamic data, use FlatList instead β it only renders items currently visible on screen, which is much more efficient for hundreds or thousands of items.
Props & State
Props (short for properties) are how you pass data from a parent component to a child. State is data that lives inside a component and can change β when state changes, React Native automatically re-renders the component to reflect the new data. The useState hook is the primary way to manage state in modern React Native.
Passing Props to Components
Props make components reusable. Instead of writing a separate Card component for every person, you write one Card that accepts props and renders them. Props flow downward β from parent to child. A child cannot change its props directly.
import { View, Text, StyleSheet } from 'react-native';
// Reusable component that accepts props
function StudentCard({ name, score, city }) {
const passed = score >= 50;
return (
<View style={styles.card}>
<Text style={styles.name}>{name}</Text>
<Text style={styles.city}>{city}</Text>
<View style={[styles.badge,
{ backgroundColor: passed ? '#00C89622' : '#EF444422' }]}>
<Text style={{ color: passed ? '#00C896' : '#EF4444',
fontWeight: '700' }}>
{score}% β {passed ? 'PASSED' : 'FAILED'}
</Text>
</View>
</View>
);
}
// Parent uses the component with different props each time
export default function App() {
return (
<View style={styles.container}>
<StudentCard name="Alice" score={95} city="Cape Town" />
<StudentCard name="Bob" score={72} city="Johannesburg" />
<StudentCard name="Carol" score={45} city="Durban" />
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, padding: 20, gap: 12,
backgroundColor: '#060912' },
card: { backgroundColor: '#0f172a', borderRadius: 12,
padding: 16, borderWidth: 1,
borderColor: '#1e293b' },
name: { fontSize: 17, fontWeight: '700', color: '#fff' },
city: { fontSize: 13, color: '#64748b', marginTop: 2, marginBottom: 10 },
badge: { paddingHorizontal: 10, paddingVertical: 4,
borderRadius: 6, alignSelf: 'flex-start' },
});Destructuring props in the function signature β function StudentCard({ name, score, city }) β is cleaner than function StudentCard(props) and then writing props.name everywhere. Both work, but destructuring is the modern standard.
useState β Making Components Interactive
useState is a hook that adds state to a functional component. It returns two things: the current value, and a function to update it. Every time you call the setter function, React Native re-renders the component with the new value. This is how all interactivity works.
import { useState } from 'react';
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
export default function Counter() {
// useState(0) sets the initial value to 0
const [count, setCount] = useState(0);
const [colour, setColour] = useState('#61DAFB');
const increment = () => setCount(count + 1);
const decrement = () => setCount(count - 1);
const reset = () => { setCount(0); setColour('#61DAFB'); };
// Change colour based on count
const displayColour = count > 0 ? '#00C896'
: count < 0 ? '#EF4444'
: '#61DAFB';
return (
<View style={styles.container}>
<Text style={styles.label}>Count</Text>
<Text style={[styles.count, { color: displayColour }]}>
{count}
</Text>
<View style={styles.row}>
<TouchableOpacity style={styles.btn} onPress={decrement}>
<Text style={styles.btnText}>β</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.btn} onPress={increment}>
<Text style={styles.btnText}>+</Text>
</TouchableOpacity>
</View>
<TouchableOpacity style={styles.reset} onPress={reset}>
<Text style={styles.resetText}>Reset</Text>
</TouchableOpacity>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, justifyContent: 'center',
alignItems: 'center', backgroundColor: '#060912' },
label: { color: '#64748b', fontSize: 14, marginBottom: 8 },
count: { fontSize: 72, fontWeight: '900', marginBottom: 32 },
row: { flexDirection: 'row', gap: 20 },
btn: { width: 64, height: 64, borderRadius: 32,
backgroundColor: '#1e293b', justifyContent: 'center',
alignItems: 'center' },
btnText: { color: '#fff', fontSize: 28, fontWeight: '700' },
reset: { marginTop: 24, paddingHorizontal: 24, paddingVertical: 10,
backgroundColor: '#1e293b', borderRadius: 8 },
resetText: { color: '#64748b', fontSize: 14 },
});Never mutate state directly β always use the setter function. If you write count++ or count = count + 1 React Native will not know the state changed and will not re-render. Always call setCount(newValue).
TextInput β Getting User Input
TextInput is the React Native equivalent of an HTML input field. It fires an onChangeText callback every time the user types a character. Combine it with useState to store what the user typed and use it in your UI.
import { useState } from 'react';
import { View, Text, TextInput, StyleSheet } from 'react-native';
export default function GreetingForm() {
const [name, setName] = useState('');
const [message, setMessage] = useState('');
return (
<View style={styles.container}>
<Text style={styles.label}>Your name</Text>
<TextInput
style={styles.input}
value={name}
onChangeText={(text) => setName(text)}
placeholder="e.g. Alice"
placeholderTextColor="#475569"
autoCapitalize="words"
/>
<Text style={styles.label}>Your message</Text>
<TextInput
style={[styles.input, styles.multiline]}
value={message}
onChangeText={setMessage} // shorthand β same as above
placeholder="Type something..."
placeholderTextColor="#475569"
multiline
numberOfLines={4}
/>
{name.length > 0 && (
<View style={styles.preview}>
<Text style={styles.previewName}>
Hello, {name}! π
</Text>
{message.length > 0 && (
<Text style={styles.previewMsg}>{message}</Text>
)}
</View>
)}
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, padding: 24, backgroundColor: '#060912' },
label: { color: '#94a3b8', fontSize: 13, marginBottom: 6,
marginTop: 16 },
input: { backgroundColor: '#0f172a', borderRadius: 10,
borderWidth: 1, borderColor: '#1e293b',
color: '#fff', padding: 12, fontSize: 15 },
multiline: { height: 100, textAlignVertical: 'top' },
preview: { marginTop: 24, padding: 16, backgroundColor: '#0f172a',
borderRadius: 12, borderWidth: 1,
borderColor: '#61DAFB30' },
previewName: { color: '#61DAFB', fontSize: 18, fontWeight: '700' },
previewMsg: { color: '#94a3b8', marginTop: 8, fontSize: 14 },
});onChangeText gives you the full current text string β not an event object. This is simpler than web React where you write event.target.value. In React Native: onChangeText={(text) => setName(text)} or shorthand: onChangeText={setName}.
Lists & Input
FlatList is the most important component for displaying dynamic lists in React Native. Unlike mapping over an array in ScrollView, FlatList only renders items that are currently visible on screen β this makes it fast even with thousands of items. TouchableOpacity and Pressable turn any component into a tappable button.
FlatList β Efficient Dynamic Lists
FlatList takes a data array and a renderItem function. keyExtractor tells React which item is which (used for efficient updates). You can add separators, headers, footers, pull-to-refresh, and infinite scroll β all built in. It is the React Native equivalent of a virtualised list.
import { FlatList, View, Text, StyleSheet } from 'react-native';
const STUDENTS = [
{ id: '1', name: 'Alice', score: 95, city: 'Cape Town' },
{ id: '2', name: 'Bob', score: 82, city: 'Johannesburg' },
{ id: '3', name: 'Charlie', score: 71, city: 'Durban' },
{ id: '4', name: 'Diana', score: 68, city: 'Pretoria' },
{ id: '5', name: 'Edward', score: 55, city: 'Bloemfontein' },
];
function StudentItem({ name, score, city }) {
return (
<View style={styles.item}>
<View style={styles.left}>
<Text style={styles.name}>{name}</Text>
<Text style={styles.city}>{city}</Text>
</View>
<Text style={[styles.score,
{ color: score >= 70 ? '#00C896' : '#F59E0B' }]}>
{score}%
</Text>
</View>
);
}
export default function App() {
return (
<FlatList
data={STUDENTS}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<StudentItem
name={item.name}
score={item.score}
city={item.city}
/>
)}
ItemSeparatorComponent={() => <View style={styles.sep} />}
ListHeaderComponent={
<Text style={styles.header}>π Student Scores</Text>
}
contentContainerStyle={styles.content}
style={styles.list}
/>
);
}
const styles = StyleSheet.create({
list: { flex: 1, backgroundColor: '#060912' },
content: { padding: 20 },
header: { fontSize: 22, fontWeight: '800', color: '#fff', marginBottom: 16 },
item: { flexDirection: 'row', justifyContent: 'space-between',
alignItems: 'center', paddingVertical: 12 },
left: { flex: 1 },
name: { fontSize: 15, fontWeight: '600', color: '#f1f5f9' },
city: { fontSize: 12, color: '#64748b', marginTop: 2 },
score: { fontSize: 18, fontWeight: '800' },
sep: { height: 1, backgroundColor: '#1e293b' },
});Always provide a keyExtractor that returns a unique string per item. If you do not, React Native warns you and falls back to using array index β which causes bugs when items are added, removed, or reordered.
TouchableOpacity & Pressable
TouchableOpacity dims its children when pressed β giving visual feedback. Pressable is the newer, more flexible alternative with pressed state callbacks. Both use onPress for tap events. Use them to wrap any component and make it tappable.
import { useState } from 'react';
import { View, Text, TouchableOpacity,
Pressable, StyleSheet } from 'react-native';
export default function ButtonDemo() {
const [taps, setTaps] = useState(0);
const [last, setLast] = useState('none');
const [liked, setLiked] = useState(false);
return (
<View style={styles.container}>
<Text style={styles.heading}>Interactive Buttons</Text>
{/* TouchableOpacity β dims on press */}
<TouchableOpacity
style={styles.btn}
onPress={() => { setTaps(t => t + 1); setLast('blue'); }}
activeOpacity={0.7}
>
<Text style={styles.btnText}>Tap me! ({taps})</Text>
</TouchableOpacity>
{/* Pressable with pressed state */}
<Pressable
onPress={() => setLiked(!liked)}
style={({ pressed }) => [
styles.likeBtn,
{ opacity: pressed ? 0.7 : 1,
backgroundColor: liked ? '#EC4899' : '#1e293b' }
]}
>
<Text style={styles.likeText}>
{liked ? 'β€οΈ Liked!' : 'π€ Like'}
</Text>
</Pressable>
<Text style={styles.status}>
Last tapped: {last} button | Total: {taps}
</Text>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, justifyContent: 'center',
alignItems: 'center', gap: 16,
backgroundColor: '#060912', padding: 24 },
heading: { fontSize: 20, fontWeight: '700', color: '#fff', marginBottom: 8 },
btn: { backgroundColor: '#61DAFB', paddingHorizontal: 32,
paddingVertical: 14, borderRadius: 12, width: '100%',
alignItems: 'center' },
btnText: { color: '#060912', fontWeight: '800', fontSize: 15 },
likeBtn: { paddingHorizontal: 32, paddingVertical: 14,
borderRadius: 12, width: '100%', alignItems: 'center' },
likeText: { color: '#fff', fontWeight: '700', fontSize: 15 },
status: { marginTop: 16, color: '#64748b', fontSize: 13 },
});TouchableOpacity is simpler and works great for most buttons. Use Pressable when you need the pressed state (to animate or style differently while the finger is down). Both replace the old TouchableHighlight and TouchableNativeFeedback.
Hooks
Hooks are special functions that give your components superpowers. useState manages data that changes. useEffect runs code after the component renders β perfect for fetching data, setting up subscriptions, or interacting with device APIs. useCallback and useMemo prevent unnecessary re-renders in performance-critical parts of your app.
useEffect β Running Code After Render
useEffect takes a function that runs after the component renders. The dependency array (the second argument) controls when it re-runs. An empty array [] means "run once when the component mounts". Dependencies in the array mean "re-run whenever these values change". Return a cleanup function to cancel timers, subscriptions, etc.
import { useState, useEffect } from 'react';
import { View, Text, StyleSheet } from 'react-native';
export default function Timer() {
const [seconds, setSeconds] = useState(0);
const [running, setRunning] = useState(true);
// useEffect with [] runs ONCE when component mounts
useEffect(() => {
console.log('Component mounted!');
return () => console.log('Component unmounted!');
}, []);
// useEffect re-runs whenever 'running' changes
useEffect(() => {
if (!running) return; // pause
const interval = setInterval(() => {
setSeconds(s => s + 1); // functional update
}, 1000);
// Cleanup: cancel interval when effect re-runs or unmounts
return () => clearInterval(interval);
}, [running]);
const minutes = Math.floor(seconds / 60);
const secs = seconds % 60;
return (
<View style={styles.container}>
<Text style={styles.timer}>
{String(minutes).padStart(2, '0')}:
{String(secs).padStart(2, '0')}
</Text>
<Text
style={[styles.btn,
{ backgroundColor: running ? '#EF4444' : '#00C896' }]}
onPress={() => setRunning(!running)}
>
{running ? 'βΈ Pause' : 'βΆ Resume'}
</Text>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, justifyContent: 'center',
alignItems: 'center', backgroundColor: '#060912' },
timer: { fontSize: 64, fontWeight: '900', color: '#fff',
fontVariant: ['tabular-nums'] },
btn: { marginTop: 32, paddingHorizontal: 32, paddingVertical: 12,
borderRadius: 10, color: '#fff', fontWeight: '700',
fontSize: 16, overflow: 'hidden' },
});The cleanup function returned from useEffect (return () => clearInterval(interval)) is crucial. Without it, the interval keeps running even after the component is removed from the screen β causing memory leaks and errors. Always clean up subscriptions, timers, and event listeners.
Custom Hooks
A custom hook is just a JavaScript function whose name starts with "use" that calls other hooks inside it. Custom hooks let you extract reusable logic from components. Instead of copy-pasting the same useState + useEffect pattern everywhere, put it in a custom hook and reuse it.
import { useState, useEffect } from 'react';
import { View, Text, ActivityIndicator, StyleSheet } from 'react-native';
// Custom hook β reusable data fetching logic
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
setError(null);
fetch(url)
.then(res => {
if (!res.ok) throw new Error('Network error');
return res.json();
})
.then(json => {
setData(json);
setLoading(false);
})
.catch(err => {
setError(err.message);
setLoading(false);
});
}, [url]);
return { data, loading, error };
}
// Component uses the custom hook cleanly
export default function UserProfile() {
const { data, loading, error } = useFetch(
'https://jsonplaceholder.typicode.com/users/1'
);
if (loading) return (
<View style={styles.center}>
<ActivityIndicator size="large" color="#8B5CF6" />
<Text style={styles.loadText}>Loading...</Text>
</View>
);
if (error) return (
<View style={styles.center}>
<Text style={styles.error}>Error: {error}</Text>
</View>
);
return (
<View style={styles.container}>
<Text style={styles.name}>{data.name}</Text>
<Text style={styles.detail}>{data.email}</Text>
<Text style={styles.detail}>{data.phone}</Text>
<Text style={styles.detail}>{data.website}</Text>
</View>
);
}
const styles = StyleSheet.create({
center: { flex: 1, justifyContent: 'center',
alignItems: 'center', backgroundColor: '#060912' },
loadText: { color: '#64748b', marginTop: 12 },
error: { color: '#EF4444', fontSize: 14 },
container: { flex: 1, padding: 24, backgroundColor: '#060912',
justifyContent: 'center' },
name: { fontSize: 24, fontWeight: '800', color: '#fff', marginBottom: 12 },
detail: { fontSize: 14, color: '#94a3b8', marginBottom: 6 },
});Custom hooks must start with "use" β this is not just a convention, it is required. React uses the "use" prefix to know which functions are hooks and to apply its rules (hooks must always be called in the same order, never inside if/loops).
Networking & Storage
React Native apps fetch real data from APIs using the built-in fetch() function β the same API as in web browsers. Since network calls take time, you always handle them asynchronously with async/await. For persisting data between app launches, AsyncStorage is the built-in key-value store.
Fetching Data from an API
fetch() is asynchronous β it returns a Promise. Use async/await inside a useEffect to fetch data when the component mounts. Always handle the three states: loading (spinner), error (error message), and success (render the data).
import { useState, useEffect } from 'react';
import {
View, Text, FlatList,
ActivityIndicator, StyleSheet
} from 'react-native';
export default function PostList() {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function fetchPosts() {
try {
const response = await fetch(
'https://jsonplaceholder.typicode.com/posts?_limit=5'
);
if (!response.ok) {
throw new Error('Failed to fetch posts');
}
const data = await response.json();
setPosts(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false); // always runs
}
}
fetchPosts();
}, []); // [] = fetch once on mount
if (loading) {
return (
<View style={styles.center}>
<ActivityIndicator size="large" color="#EF4444" />
<Text style={styles.loadText}>Fetching postsβ¦</Text>
</View>
);
}
if (error) {
return (
<View style={styles.center}>
<Text style={styles.errorText}>β οΈ {error}</Text>
</View>
);
}
return (
<FlatList
data={posts}
keyExtractor={(item) => String(item.id)}
style={styles.list}
contentContainerStyle={styles.content}
ListHeaderComponent={
<Text style={styles.heading}>Latest Posts</Text>
}
renderItem={({ item }) => (
<View style={styles.card}>
<Text style={styles.postTitle}>{item.title}</Text>
<Text style={styles.postBody} numberOfLines={2}>
{item.body}
</Text>
<Text style={styles.postId}>Post #{item.id}</Text>
</View>
)}
/>
);
}
const styles = StyleSheet.create({
center: { flex: 1, justifyContent: 'center',
alignItems: 'center', backgroundColor: '#060912' },
loadText: { color: '#64748b', marginTop: 12 },
errorText: { color: '#EF4444', fontSize: 15 },
list: { flex: 1, backgroundColor: '#060912' },
content: { padding: 20, gap: 12 },
heading: { fontSize: 22, fontWeight: '800',
color: '#fff', marginBottom: 8 },
card: { backgroundColor: '#0f172a', borderRadius: 12,
padding: 16, borderWidth: 1, borderColor: '#1e293b' },
postTitle: { fontSize: 14, fontWeight: '700', color: '#f1f5f9',
marginBottom: 6, textTransform: 'capitalize' },
postBody: { fontSize: 12, color: '#64748b', lineHeight: 18 },
postId: { marginTop: 8, fontSize: 11, color: '#334155' },
});The finally block runs whether fetch succeeds or fails β it is the right place to call setLoading(false). This prevents a bug where loading stays true forever if an error is thrown before you call setLoading(false) manually.
AsyncStorage β Persisting Data
AsyncStorage saves key-value data to the device and keeps it between app launches β like localStorage on the web but async. Install with: npx expo install @react-native-async-storage/async-storage. All methods return Promises so always use await.
// Install: npx expo install @react-native-async-storage/async-storage
import { useState, useEffect } from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';
import {
View, Text, TextInput,
TouchableOpacity, StyleSheet
} from 'react-native';
const STORAGE_KEY = '@user_profile';
export default function ProfileScreen() {
const [name, setName] = useState('');
const [saved, setSaved] = useState(false);
const [loaded, setLoaded] = useState(false);
// Load saved profile when app opens
useEffect(() => {
async function loadProfile() {
try {
const stored = await AsyncStorage.getItem(STORAGE_KEY);
if (stored !== null) {
const profile = JSON.parse(stored);
setName(profile.name);
}
} catch (e) {
console.error('Failed to load profile');
} finally {
setLoaded(true);
}
}
loadProfile();
}, []);
async function saveProfile() {
try {
const profile = { name, savedAt: new Date().toISOString() };
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(profile));
setSaved(true);
setTimeout(() => setSaved(false), 2000);
} catch (e) {
console.error('Failed to save profile');
}
}
async function clearProfile() {
await AsyncStorage.removeItem(STORAGE_KEY);
setName('');
}
if (!loaded) return null;
return (
<View style={styles.container}>
<Text style={styles.title}>Your Profile</Text>
<Text style={styles.label}>Display Name</Text>
<TextInput
style={styles.input}
value={name}
onChangeText={setName}
placeholder="Enter your nameβ¦"
placeholderTextColor="#475569"
/>
<TouchableOpacity style={styles.saveBtn} onPress={saveProfile}>
<Text style={styles.saveTxt}>
{saved ? 'β Saved!' : 'Save Profile'}
</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.clearBtn} onPress={clearProfile}>
<Text style={styles.clearTxt}>Clear</Text>
</TouchableOpacity>
<Text style={styles.hint}>
Close and reopen the app β your name persists!
</Text>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, padding: 24, backgroundColor: '#060912' },
title: { fontSize: 24, fontWeight: '800', color: '#fff', marginBottom: 24 },
label: { color: '#94a3b8', fontSize: 13, marginBottom: 6 },
input: { backgroundColor: '#0f172a', borderRadius: 10, borderWidth: 1,
borderColor: '#1e293b', color: '#fff', padding: 12,
fontSize: 15, marginBottom: 16 },
saveBtn: { backgroundColor: '#EF4444', paddingVertical: 14,
borderRadius: 10, alignItems: 'center', marginBottom: 10 },
saveTxt: { color: '#fff', fontWeight: '700', fontSize: 15 },
clearBtn: { paddingVertical: 12, alignItems: 'center' },
clearTxt: { color: '#64748b', fontSize: 14 },
hint: { marginTop: 24, color: '#334155', fontSize: 13,
textAlign: 'center' },
});AsyncStorage only stores strings. To save objects, serialize them with JSON.stringify() when saving and parse with JSON.parse() when loading. For complex state management across the whole app, consider Zustand or MMKV (much faster than AsyncStorage).
You finished the React Native tutorial!
You can now build real mobile apps with components, props, state, hooks, navigation, and API calls. These skills work on both iOS and Android from a single codebase.