React Native Intermediate: Navigation, Data and Device Features
Fetching Data
In this lesson you will
- Fetch JSON with fetch inside useEffect and show loading, error and success states
- Cancel an unfinished request when a screen closes
- Explain why a phone cannot reach localhost the way a browser can
Introduction
Most apps show data that lives on a server: posts, products, weather. React Native includes the same fetch function you know from the web, so loading data works the same way. What changes on a phone is that the network is slower, less reliable and sometimes absent, so you must design for loading, error and empty states from the start.
Fetch once, with three states
Here is a screen that loads posts from a free test API. It tracks the data, whether it is loading, and any error:
React (TSX)
import { useEffect, useState } from "react";
import { ActivityIndicator, FlatList, StyleSheet, Text, View } from "react-native";
type Post = { id: number; title: string };
export default function Posts() {
const [posts, setPosts] = useState<Post[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const controller = new AbortController();
async function load() {
try {
const res = await fetch("https://jsonplaceholder.typicode.com/posts?_limit=20", {
signal: controller.signal,
});
if (!res.ok) throw new Error(`The server answered ${res.status}`);
setPosts((await res.json()) as Post[]);
} catch (e) {
if (controller.signal.aborted) return;
setError(e instanceof Error ? e.message : "Something went wrong");
} finally {
if (!controller.signal.aborted) setLoading(false);
}
}
load();
return () => controller.abort();
}, []);
if (loading) {
return (
<View style={styles.center}>
<ActivityIndicator size="large" />
</View>
);
}
if (error) {
return (
<View style={styles.center}>
<Text style={styles.error}>Could not load posts: {error}</Text>
</View>
);
}
return (
<FlatList
data={posts}
keyExtractor={(post) => String(post.id)}
renderItem={({ item }) => <Text style={styles.row}>{item.title}</Text>}
ListEmptyComponent={<Text style={styles.row}>No posts yet.</Text>}
/>
);
}
const styles = StyleSheet.create({
center: { flex: 1, alignItems: "center", justifyContent: "center", padding: 24 },
error: { color: "#b91c1c", textAlign: "center" },
row: { padding: 16, fontSize: 16 },
});Walk through it:
- State holds the three possible situations:
loading(waiting),error(failed) andposts(the result). fetchreturns a promise.res.okis true for a successful status, andfetchdoes not throw for a 404 or 500, so you must check it yourself, as inif (!res.ok) throw ....res.json()reads the body as JSON. Theas Post[]tells TypeScript the shape you expect. It is a promise, not a check, so the server could still send something else.ActivityIndicatoris the spinning wheel.- The screen returns early for loading and error, and finally draws the list.
Quick quiz
A server replies with status 404. What does fetch do?
Cancelling with AbortController
Look at the effect above. The user might leave the screen before the data arrives. Without care, the request would finish and try to update a screen that is gone. An AbortController solves this:
- You pass
controller.signaltofetch. - The function returned from the effect calls
controller.abort()when the screen closes, which cancels the request. - After an abort,
fetchrejects, so the code checkscontroller.signal.abortedand quietly stops instead of showing an error.
Pull to refresh
You already know the props from the FlatList lesson. Put loading into a function you can call again:
React (TSX)
import { useCallback, useEffect, useState } from "react";
import { FlatList, Text } from "react-native";
type Post = { id: number; title: string };
export default function RefreshablePosts() {
const [posts, setPosts] = useState<Post[]>([]);
const [refreshing, setRefreshing] = useState(false);
const load = useCallback(async () => {
const res = await fetch("https://jsonplaceholder.typicode.com/posts?_limit=10");
if (res.ok) setPosts((await res.json()) as Post[]);
}, []);
useEffect(() => {
load();
}, [load]);
async function refresh() {
setRefreshing(true);
await load();
setRefreshing(false);
}
return (
<FlatList
data={posts}
keyExtractor={(post) => String(post.id)}
renderItem={({ item }) => <Text style={{ padding: 16 }}>{item.title}</Text>}
refreshing={refreshing}
onRefresh={refresh}
/>
);
}useCallback keeps load the same function between renders, so the effect that depends on it runs only once.
Sending data
To send data, use fetch with a method and a JSON body:
React (TSX)
async function createPost(title: string) {
const res = await fetch("https://jsonplaceholder.typicode.com/posts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title, body: "Written in my app", userId: 1 }),
});
if (!res.ok) throw new Error("Could not save");
return res.json();
}Phone networking traps
Phones are stricter than browsers in a few ways:
- Use HTTPS. iPhone and Android block plain
http://addresses by default for real apps. Usehttps://for any server you do not control. - localhost is the phone itself. When you test against a server running on your computer,
localhoston the phone means the phone, not your computer. On an Android emulator the computer is reached at10.0.2.2. On a real phone use your computer's address on your Wi-Fi network, for examplehttp://192.168.1.20:3000, and make sure both are on the same network. - Expect failure. People ride trains and walk into lifts. Always show an error message and a way to try again.
Beyond the basics
Hand-written useEffect fetching is perfect for learning and for simple screens. In larger apps people use a library such as TanStack Query, which adds caching, automatic retries, refetching and shared loading states. Learn the fundamentals first, so you understand what such a library is doing for you.
Quick quiz
On an Android emulator, how can your app reach a server running on your computer?
Try it yourself
Build a screen that loads 20 users from https://jsonplaceholder.typicode.com/users. Show a spinner while loading, an error message with a "Try again" button if it fails (turn on airplane mode to test it), and pull to refresh. Add a search TextInput that filters the names.
Summary
- Use
fetchinsideuseEffect, keeploading,erroranddatain state, and checkres.ok, becausefetchdoes not throw for error statuses. - Cancel unfinished requests with an
AbortControllerwhen the screen closes. - Use HTTPS, remember localhost means the phone itself, and always design for slow or missing networks.
Lesson complete
+20 XP
Nice work. Keep going while it is fresh.