Fetching data from an API
Real apps take data from a server. React Native has the same fetch function as the browser, and, as on the web, the request is sent in useEffect. The phone adds its own details: a system loading indicator and pull-to-refresh on lists.
The post list
The address https://jsonplaceholder.typicode.com/posts returns 100 posts with id, title and body fields. Replace the local array with loading in app/(tabs)/index.tsx:
// app/(tabs)/index.tsx
import { Link } from 'expo-router';
import { useEffect, useState } from 'react';
import { ActivityIndicator, FlatList, Text, View } from 'react-native';
import type { Post } from '@/data/posts';
const API = 'https://jsonplaceholder.typicode.com';
export default function Posts() {
const [posts, setPosts] = useState<Post[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
async function load() {
setLoading(true);
setError(null);
try {
const response = await fetch(`${API}/posts`);
if (!response.ok) throw new Error(`Error ${response.status}`);
setPosts(await response.json());
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setLoading(false);
}
}
useEffect(() => {
load();
}, []);
if (loading && posts.length === 0) {
return <ActivityIndicator size="large" style={{ flex: 1 }} />;
}
if (error) {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center', gap: 12 }}>
<Text>Could not load posts: {error}</Text>
<Text onPress={load} style={{ color: '#2563eb' }}>Retry</Text>
</View>
);
}
return (
<FlatList
data={posts}
keyExtractor={(item) => String(item.id)}
refreshing={loading}
onRefresh={load}
renderItem={({ item }) => (
<Link href={{ pathname: '/posts/[id]', params: { id: item.id } }} style={{ padding: 16 }}>
{item.title}
</Link>
)}
/>
);
}First the system ActivityIndicator spins in the center, then a list of 100 titles appears. The Post type still comes from data/posts.ts.
Pull to refresh
The refreshing and onRefresh props enable pull-to-refresh on a FlatList: pull the list down, an indicator appears, and load fetches the posts again. While refreshing is true, the indicator spins.
Error and retry
Turn on airplane mode and reload the app with the r key: an error message appears instead of the list. Turn airplane mode off and tap "Retry", and the posts load. Connections drop often on a phone, so a retry button is almost always needed.
The post screen
The app/posts/[id].tsx screen loads a single post from ${API}/posts/${id} the same way: post, loading and error state and a useEffect with the [id] dependency. A post's comments are at /posts/1/comments.
Common mistake
Calling localhost from the phone. If you run your own API on the computer at http://localhost:3000, then on the phone localhost is the phone itself, and the request never arrives. Use the computer's IP address on the local network, for example http://192.168.1.10:3000. Also, iOS blocks plain http requests by default, so a real server needs https.