Skip to main content
Back to module
Mobile Development Basics · Your First Real App

Wire up the screens

Reading36mLesson 2 of 3 in this module

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 + refreshing for pull-to-refresh, and onEndReached for 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, localhost means the phone. Use your computer's LAN address (for example http://192.168.1.20:3000); in the Android emulator, 10.0.2.2 reaches 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 ignore flag in the effect cleanup, or use an AbortController).

Common mistakes

  • Forgetting to check res.ok, so error pages are parsed as data.
  • Numeric keys: keyExtractor must return a string.
  • Showing a blank screen on error with no way to retry.
  • Rendering a long list with ScrollView and map.

Quick recap

  • Use fetch in an effect, check res.ok, and model loading, success, empty and error states.
  • Use FlatList with keyExtractor for lists; add pull-to-refresh with onRefresh.
  • 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.

Also in this lesson: