EthicsComputerCourses
Join training +91 90532 10052

Fetching Data

React Native · Lesson 16 of 30 Intermediate 7 min read Updated

By the end of this lesson you will be able to load data from a web API into your app, show a spinner while it loads, handle errors politely, and cancel requests the user no longer needs. You will also set up TanStack Query, which handles caching and refreshing for you.

Almost every real app talks to a server: a train app fetches schedules, a food app fetches menus, a bank app fetches your balance. In React Native, you use the same fetch function you know from the browser.

Step 1: fetch in an effect with three states

A request takes time and can fail. So a screen that loads data has three possible states: loading, error and success. Think of ordering at a dhaba: either the food is being cooked, or something went wrong in the kitchen, or the plate is on your table. Your screen must show something sensible for each.

The logic below is pure React plus fetch, so it runs here in the browser against a free practice API. In your app you would render ActivityIndicator, Text and FlatList instead of web elements.

React JSXTry it yourself
import { useEffect, useState } from "react";
import { createRoot } from "react-dom/client";

function useJson(url) {
  const [state, setState] = useState({ status: "loading", data: null, error: null });

  useEffect(() => {
    const controller = new AbortController();
    setState({ status: "loading", data: null, error: null });

    fetch(url, { signal: controller.signal })
      .then((res) => {
        if (!res.ok) throw new Error("Server replied " + res.status);
        return res.json();
      })
      .then((data) => setState({ status: "success", data, error: null }))
      .catch((err) => {
        if (err.name !== "AbortError") setState({ status: "error", data: null, error: err.message });
      });

    return () => controller.abort();
  }, [url]);

  return state;
}

function Customers() {
  const { status, data, error } = useJson("https://jsonplaceholder.typicode.com/users?_limit=3");

  if (status === "loading") return <p>Loading customers...</p>;
  if (status === "error") return <p>Could not load customers: {error}</p>;
  return (
    <ul>
      {data.map((u) => <li key={u.id}>{u.name} ({u.address.city})</li>)}
    </ul>
  );
}

createRoot(document.getElementById("root")).render(<Customers />);

What you should see: "Loading customers..." for a moment, then three names: Leanne Graham (Gwenborough), Ervin Howell (Wisokyburgh) and Clementine Bauch (McKenziehaven). Change the URL to /userz and you get "Could not load customers: Server replied 404".

Notice the important details:

  • fetch only rejects on network failure. A 404 or 500 still "succeeds", so check res.ok yourself.
  • The AbortController cancels the request when the screen unmounts or the URL changes. Without it, a slow old response could overwrite newer data, or try to update a screen that is gone.
  • The effect lists url as a dependency, so a new URL starts a new request. For a refresher on effects, see useEffect & Side Effects.

Step 2: the same screen in React Native

React JSX
import { ActivityIndicator, FlatList, Text, View } from "react-native";
import { useJson } from "@/hooks/use-json"; // the hook from Step 1

export default function CustomersScreen() {
  const { status, data, error } = useJson(`${process.env.EXPO_PUBLIC_API_URL}/customers`);

  if (status === "loading") {
    return <ActivityIndicator size="large" style={{ flex: 1 }} />;
  }
  if (status === "error") {
    return <Text style={{ padding: 24, color: "crimson" }}>Could not load customers. {error}</Text>;
  }
  return (
    <FlatList
      data={data}
      keyExtractor={(c) => String(c.id)}
      renderItem={({ item }) => <Text style={{ padding: 16 }}>{item.name}</Text>}
      ListEmptyComponent={<Text style={{ padding: 24 }}>No customers yet.</Text>}
    />
  );
}

What you should see: a spinning circle in the middle of the screen, then the list of customers, or a red message if the server is unreachable.

API addresses on a real phone

This trips up almost every beginner. Your phone is a separate device. If your API runs on your laptop, http://localhost:3000 on the phone means "the phone itself", not your laptop.

Where the app runsHow to reach a server on your laptop
Physical phone on the same Wi-FiUse the laptop's local IP, such as http://192.168.1.20:3000
Android emulatorhttp://10.0.2.2:3000 points to the laptop
iOS Simulator (on a Mac)http://localhost:3000 works
Production appYour real HTTPS domain

Release builds also block plain http:// addresses by default on both platforms. Always use HTTPS for real servers.

Step 3: put the API address in an environment variable

Do not hard-code the server address in every file. Expo reads .env files, and any variable whose name starts with EXPO_PUBLIC_ is available as process.env.EXPO_PUBLIC_... in your code.

Terminal
# .env (in the project root)
EXPO_PUBLIC_API_URL=https://api.example.com

Restart npx expo start after creating or changing .env.

Step 4: let TanStack Query do the hard parts

Writing loading, error, caching, retries and refreshing by hand for every screen gets repetitive. TanStack Query (also called React Query) does this for you. It keeps a cache: a store of earlier answers, keyed by a name you choose, so revisiting a screen shows data instantly while it checks for fresh data in the background.

Terminal
npx expo install @tanstack/react-query
React JSX
// src/app/_layout.jsx
import { Stack } from "expo-router";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

const queryClient = new QueryClient();

export default function RootLayout() {
  return (
    <QueryClientProvider client={queryClient}>
      <Stack />
    </QueryClientProvider>
  );
}
React JSX
// src/app/orders.jsx
import { useQuery } from "@tanstack/react-query";
import { ActivityIndicator, FlatList, Text } from "react-native";

async function fetchOrders() {
  const res = await fetch(`${process.env.EXPO_PUBLIC_API_URL}/orders`);
  if (!res.ok) throw new Error("Server replied " + res.status);
  return res.json();
}

export default function Orders() {
  const { data, isPending, isError, error, refetch, isRefetching } = useQuery({
    queryKey: ["orders"],
    queryFn: fetchOrders,
  });

  if (isPending) return <ActivityIndicator style={{ flex: 1 }} />;
  if (isError) return <Text onPress={() => refetch()}>Error: {error.message}. Tap to retry.</Text>;

  return (
    <FlatList
      data={data}
      keyExtractor={(o) => String(o.id)}
      renderItem={({ item }) => <Text style={{ padding: 16 }}>Order #{item.id}: ₹{item.total}</Text>}
      refreshing={isRefetching}
      onRefresh={refetch}
    />
  );
}

What you should see: the first visit shows a spinner, then the orders. Leave the screen and come back: the orders appear instantly from the cache. Pull down to refresh and a fresh request runs. If the request fails, TanStack Query retries a few times before showing the error. The React course covers it in depth in Server State with TanStack Query.

Common mistakes

  • Not checking res.ok. A 404 page is not data. Throw an error when res.ok is false.
  • Calling localhost from a phone. Use your laptop's IP or 10.0.2.2 on the Android emulator.
  • Secrets in EXPO_PUBLIC_ variables. Everything in the bundle is public.
  • No cleanup in effects. Abort requests when the screen unmounts or inputs change.
  • Showing a blank screen while loading. Always show a spinner or placeholder, and a message on error.

Key points

  • fetch works the same as on the web; check res.ok because HTTP errors do not reject.
  • Every data screen needs loading, error and success states.
  • Cancel stale requests with AbortController in the effect cleanup.
  • On a phone, localhost is the phone; use your laptop's IP or 10.0.2.2 on the Android emulator.
  • EXPO_PUBLIC_ variables configure the API address but are visible to everyone.
  • TanStack Query adds caching, retries and refreshing with useQuery.

Exercise

Write a custom hook usePosts(userId) that fetches https://jsonplaceholder.typicode.com/posts?userId=ID, with loading, error and success states and an AbortController cleanup. Then write a screen that shows the number of posts and the first post's title, plus a "Next user" button that increases userId (stop at 10).

Show solution
React JSX
import { useEffect, useState } from "react";
import { View, Text, Pressable, ActivityIndicator } from "react-native";

export function usePosts(userId) {
  const [state, setState] = useState({ status: "loading", posts: [] });
  useEffect(() => {
    const controller = new AbortController();
    setState({ status: "loading", posts: [] });
    fetch(`https://jsonplaceholder.typicode.com/posts?userId=${userId}`, { signal: controller.signal })
      .then((r) => { if (!r.ok) throw new Error("HTTP " + r.status); return r.json(); })
      .then((posts) => setState({ status: "success", posts }))
      .catch((e) => { if (e.name !== "AbortError") setState({ status: "error", posts: [] }); });
    return () => controller.abort();
  }, [userId]);
  return state;
}

export default function PostsScreen() {
  const [userId, setUserId] = useState(1);
  const { status, posts } = usePosts(userId);

  return (
    <View style={{ padding: 24, gap: 12 }}>
      <Text>User {userId}</Text>
      {status === "loading" && <ActivityIndicator />}
      {status === "error" && <Text>Could not load posts.</Text>}
      {status === "success" && <Text>{posts.length} posts. First: {posts[0]?.title}</Text>}
      <Pressable onPress={() => setUserId((id) => Math.min(10, id + 1))}><Text>Next user</Text></Pressable>
    </View>
  );
}

Because userId is in the dependency list, each tap starts a new request and the cleanup aborts the previous one if it is still running.

Quick Quiz

1. fetch gets a 404 response. What happens by default?

2. Your API runs on your laptop at port 3000. What URL should the Android emulator use?

3. Is it safe to store a payment gateway secret key in EXPO_PUBLIC_PAY_SECRET?

4. What does TanStack Query's cache give you?

Learn React Native with a mentor

Free lessons get you started. Our Mobile App Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call