Load remote data with fetch, handle loading and error states, and render it with FlatList.
Why it matters
Almost every real app shows data that lives on a server: posts, products, messages. Loading it well is what separates a demo from an app people trust. Users on a train go through tunnels, servers are slow and requests fail, so a screen must handle loading, success, empty and error states, not just the happy path.
Fetching in React Native
React Native ships the same fetch API you know from the browser. The usual pattern is an effect that runs when the screen mounts:
import { useEffect, useState } from "react";
import { ActivityIndicator, FlatList, Text, View, Pressable } from "react-native";
export default function UsersScreen() {
const [state, setState] = useState({ status: "idle", items: [], error: null });
async function load() {
setState((s) => ({ ...s, status: "loading", error: null }));
try {
const res = await fetch("https://jsonplaceholder.typicode.com/users");
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const users = await res.json();
setState({ status: "success", items: users.map((u) => ({ id: String(u.id), title: u.name })), error: null });
} catch (err) {
setState((s) => ({ ...s, status: "error", error: err.message }));
}
}
useEffect(() => { load(); }, []);
if (state.status === "loading" && state.items.length === 0) return <ActivityIndicator />;
if (state.status === "error") {
return (
<View>
<Text>Could not load users: {state.error}</Text>
<Pressable onPress={load}><Text>Try again</Text></Pressable>
</View>
);
}
return (
<FlatList
data={state.items}
keyExtractor={(item) => item.id}
renderItem={({ item }) => <Text>{item.title}</Text>}
ListEmptyComponent={<Text>No users yet.</Text>}
refreshing={state.status === "loading"}
onRefresh={load}
/>
);
}
Note res.ok: fetch only rejects on network failure. A 404 or 500 still resolves, so you must check the status yourself.
Why FlatList and not map
On the web you might write items.map(...) inside a scrolling div. In React Native, a ScrollView with map renders every row at once, which is fine for 20 items and painful for 2,000. FlatList renders only the rows near the screen and recycles the rest. It needs:
data: the array,renderItem: a function that receives{ item, index },keyExtractor: a function returning a string key per item (React Native warns about missing or duplicate keys),- optional
ListEmptyComponent,ListHeaderComponent,onRefresh+refreshingfor pull-to-refresh, andonEndReachedfor loading more pages.
Shape the data before the UI sees it
API responses rarely match what a list row needs. Convert them once, in a plain function, so the component stays simple and the conversion is easy to test:
function toListData(users) {
return users
.filter((u) => u && u.id != null && u.name)
.map((u) => ({ id: String(u.id), title: u.name, subtitle: u.email || "" }));
}
The same idea applies to the request state itself. Instead of several useState calls that can disagree (loading is false but data is missing), keep one object and change it through a reducer with start, success and failure actions. Keeping the old items during a refresh means the list does not flash empty while new data loads.
Device networking gotchas
- localhost is not your computer. On a phone,
localhostmeans the phone. Use your computer's LAN address (for examplehttp://192.168.1.20:3000); in the Android emulator,10.0.2.2reaches the host machine. - Plain HTTP is blocked by default. iOS App Transport Security and Android 9+ refuse cleartext
http://in production builds. Use HTTPS for real servers. - Requests can outlive the screen. If the user leaves before a response arrives, ignore the result (keep an
ignoreflag in the effect cleanup, or use anAbortController).
Common mistakes
- Forgetting to check
res.ok, so error pages are parsed as data. - Numeric keys:
keyExtractormust return a string. - Showing a blank screen on error with no way to retry.
- Rendering a long list with
ScrollViewandmap.
Quick recap
- Use
fetchin an effect, checkres.ok, and model loading, success, empty and error states. - Use
FlatListwithkeyExtractorfor lists; add pull-to-refresh withonRefresh. - Convert API data with small pure functions and keep request state in one reducer.
Try it
In the Do tab you will write requestReducer and toListData, the two pure pieces behind the screen above.