Popular Searches
Popular Course Categories
Popular Courses

Data Fetching, Loading States, and Error States

Data Fetching, Loading States, and Error States

State Management in React Native

Data Fetching, Loading States, and Error States in React Native

Course: React Native Training

Topic: Data Fetching, Loading States, and Error States

Course Link: React Native Training Course

Demo Link: Register for a Free Course Demo

Academy Website: JustAcademy


1. Introduction to Data Fetching in React Native

Data fetching is the process of requesting information from an external source, such as a REST API, backend server, or cloud database, and displaying the response inside a React Native application. It allows mobile applications to show dynamic and up-to-date information instead of relying only on static data.

For example, an e-commerce application may fetch product details from a server, a news application may retrieve the latest articles, and a weather application may request current weather conditions.

During a network request, the application must communicate its current status to the user. This is why loading states and error states are important parts of a reliable user experience.

1.1 What Is Data Fetching?

Data fetching involves sending a request to a data source, receiving a response, processing the returned data, and updating the user interface.

Common data-fetching operations include:

  • Retrieving a list of products or users.
  • Loading a user's profile after login.
  • Fetching categories and dashboard information.
  • Sending search queries to an API.
  • Retrieving records from a database through a backend service.
  • Refreshing data when a user revisits a screen.

1.2 What Are Loading States?

A loading state indicates that the application is currently waiting for an operation to complete. It prevents users from assuming that the screen is broken or empty while data is being retrieved.

Examples of loading indicators include an activity spinner, a progress bar, skeleton placeholders, and a message such as "Loading products...".

1.3 What Are Error States?

An error state represents a situation in which an operation cannot complete successfully. This can happen because of an unavailable server, a network connection problem, an invalid API response, an authentication failure, or an unexpected application error.

A useful error state explains the problem in simple language and, when appropriate, provides a Retry button.

2. Learning Objectives

After studying this topic, learners should be able to:

  • Understand how APIs work with React Native applications.
  • Fetch data using the JavaScript Fetch API.
  • Use async/await and Promises to handle asynchronous operations.
  • Manage loading, success, empty, and error states.
  • Display fetched data using FlatList.
  • Handle HTTP errors and network failures.
  • Implement retry functionality and pull-to-refresh.
  • Prevent unnecessary state updates after a component unmounts.
  • Understand request cancellation using AbortController.
  • Apply best practices for reliable API integration.

3. Understanding APIs and HTTP Requests

An API, or Application Programming Interface, allows two software systems to communicate. A React Native application usually communicates with a backend API through HTTP or HTTPS requests.

3.1 How Data Fetching Works

  1. The user opens a screen in the mobile application.
  2. The application sends an HTTP request to the API.
  3. The server processes the request.
  4. The server returns a response containing data or an error.
  5. The application checks the response status.
  6. The response body is parsed, often as JSON.
  7. The application updates its state and renders the appropriate interface.

3.2 Common HTTP Methods

Method Purpose Example
GET Retrieve information. Fetch a list of products.
POST Create or submit information. Register a new user.
PUT Replace an existing resource. Update a complete profile.
PATCH Update selected fields. Change a user's display name.
DELETE Remove a resource. Delete a saved address.

3.3 Understanding HTTP Status Codes

Status Code Meaning Typical Handling
200 OK Process the successful response.
201 Created Confirm that a resource was created.
204 No Content Handle success without expecting a response body.
400 Bad Request Review request parameters or submitted data.
401 Unauthorized Check authentication credentials.
403 Forbidden Explain that the user lacks permission.
404 Not Found Handle a missing resource.
429 Too Many Requests Respect rate limits and retry guidance.
500 Internal Server Error Display a general server error message.

Important: The Fetch API generally rejects its Promise for network-level failures, but it does not automatically reject for HTTP responses such as 404 or 500. Check the response's ok property or status code.

4. Setting Up a React Native Project

You can use an existing React Native project or create one using your preferred React Native setup.

For a project created with Expo, a common starting command is:

npx create-expo-app DataFetchingApp
cd DataFetchingApp
npx expo start

The examples in these notes use JavaScript, React hooks, and standard React Native components.

4.1 Important Imports

import React, { useEffect, useState } from 'react';
import {
  ActivityIndicator,
  FlatList,
  Pressable,
  RefreshControl,
  StyleSheet,
  Text,
  View,
} from 'react-native';

useState stores information such as the API response and loading status. useEffect runs a side effect, such as fetching data when a screen mounts. ActivityIndicator displays a spinner, while FlatList efficiently renders a list of items.

5. Fetching Data Using the Fetch API

The built-in Fetch API allows a React Native application to make HTTP requests without installing an additional networking library.

5.1 Basic Fetch Example

async function fetchUsers() {
  const response = await fetch(
    'https://jsonplaceholder.typicode.com/users'
  );

  if (!response.ok) {
    throw new Error(`Request failed: ${response.status}`);
  }

  const users = await response.json();
  return users;
}

In this example:

  • fetch() sends the HTTP request.
  • await waits for the request to settle.
  • response.ok checks whether the status code indicates success.
  • response.json() parses the response body as JSON.
  • throw new Error() passes a failure to the caller for handling.

5.2 Understanding Async/Await

Network requests are asynchronous because they may take time to complete. Async/await makes asynchronous code easier to read and allows errors to be handled using try/catch.

async function loadProduct() {
  try {
    const response = await fetch(
      'https://example.com/api/products/1'
    );

    if (!response.ok) {
      throw new Error('Unable to load product.');
    }

    const product = await response.json();
    return product;
  } catch (error) {
    console.error('Product request failed:', error);
    throw error;
  }
}

Replace the example URL with a valid endpoint from your backend. A catch block should not silently swallow errors if the calling component needs to display an error message.

6. Managing Data with useState

The useState hook allows a functional component to store values and update its user interface when those values change.

A typical data-fetching screen may use three pieces of state:

  • data: the retrieved information.
  • loading: whether a request is currently running.
  • error: the error message, if the request fails.
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

For a list screen, an empty array is often a useful initial data value. For a detail screen, the initial data value may be null. Choose initial values that match the data type expected by the component.

7. Implementing Loading States

A loading state communicates that an operation is in progress. Without it, users may see a blank screen or assume that the application has stopped responding.

7.1 Using ActivityIndicator

import React from 'react';
import { ActivityIndicator, Text, View } from 'react-native';

export default function LoadingExample() {
  return (
    <View>
      <ActivityIndicator size="large" color="#2563eb" />
      <Text>Loading data...</Text>
    </View>
  );
}

The ActivityIndicator component provides a native loading spinner. It is useful for initial data loading, button operations, and other tasks that take time.

7.2 Conditional Rendering for Loading

{`if (loading) {
  return (
    
      
      Loading users...
    
  );
}`}

When the loading state becomes false, the component can render the fetched content, an empty state, or an error message.

7.3 Initial Loading vs Background Refreshing

Initial loading happens when data is not yet available. Background refreshing happens when existing data is already displayed and the application requests updated information.

State Recommended UI
Initial loading Spinner, skeleton, or placeholder screen.
Background refresh Keep existing data visible and show a smaller progress indicator.
Button submission Show progress in the button and prevent duplicate submissions.
Pagination Show a footer spinner while the next page loads.

8. Implementing Error States

Error handling makes an application more reliable by providing a clear response when a request fails. An error state should not expose sensitive technical details to users.

8.1 Common Causes of Errors

  • The device has no internet connection.
  • The API server is unavailable.
  • The endpoint URL is incorrect.
  • The server returns an unsuccessful HTTP status code.
  • The response body contains invalid or unexpected JSON.
  • The user is not authenticated or lacks permission.
  • The request times out or is cancelled.
  • The server returns data that does not match the expected format.

8.2 Displaying an Error Message

function ErrorMessage({ message, onRetry }) {
  return (
    <View>
      <Text>Something went wrong.</Text>
      <Text>{message}</Text>
      <Pressable onPress={onRetry}>
        <Text>Try Again</Text>
      </Pressable>
    </View>
  );
}

This reusable component receives an error message and a callback. When the user presses the retry control, the parent component can repeat the failed request.

8.3 User-Friendly Error Messages

Technical Situation Suggested Message
Network failure Check your internet connection and try again.
Server failure We couldn't load this information right now.
Authentication failure Your session may have expired. Please sign in again.
Missing resource The requested information could not be found.
Unexpected response We couldn't process the information received.

9. Complete Example: Fetching Users with Loading and Error States

The following example combines data fetching, loading feedback, error handling, retry functionality, and list rendering. It uses a public demonstration API.

import React, { useCallback, useEffect, useState } from 'react';
import {
  ActivityIndicator,
  FlatList,
  Pressable,
  StyleSheet,
  Text,
  View,
} from 'react-native';

const USERS_URL = 'https://jsonplaceholder.typicode.com/users';

export default function UsersScreen() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [requestVersion, setRequestVersion] = useState(0);

  const loadUsers = useCallback(async () => {
    setLoading(true);
    setError(null);

    try {
      const response = await fetch(USERS_URL);

      if (!response.ok) {
        throw new Error(`Server returned ${response.status}`);
      }

      const result = await response.json();

      if (!Array.isArray(result)) {
        throw new Error('Unexpected data format.');
      }

      setUsers(result);
    } catch (err) {
      setError(
        'Unable to load users. Check your connection and try again.'
      );
    } finally {
      setLoading(false);
    }
  }, []);

  useEffect(() => {
    loadUsers();
  }, [loadUsers, requestVersion]);

  const retry = () => {
    setRequestVersion(version => version + 1);
  };

  if (loading && users.length === 0) {
    return (
      <View style={styles.center}>
        <ActivityIndicator size="large" color="#2563eb" />
        <Text>Loading users...</Text>
      </View>
    );
  }

  if (error && users.length === 0) {
    return (
      <View style={styles.center}>
        <Text style={styles.error}>{error}</Text>
        <Pressable style={styles.button} onPress={retry}>
          <Text style={styles.buttonText}>Try Again</Text>
        </Pressable>
      </View>
    );
  }

  return (
    <View style={styles.container}>
      <Text style={styles.title}>Users</Text>

      {loading && (
        <ActivityIndicator size="small" color="#2563eb" />
      )}

      {error && (
        <View>
          <Text style={styles.error}>{error}</Text>
          <Pressable onPress={retry}>
            <Text>Retry loading users</Text>
          </Pressable>
        </View>
      )}

      {!loading && !error && users.length === 0 ? (
        <Text>No users found.</Text>
      ) : (
        <FlatList
          data={users}
          keyExtractor={item => String(item.id)}
          renderItem={({ item }) => (
            <View style={styles.card}>
              <Text style={styles.name}>{item.name}</Text>
              <Text>{item.email}</Text>
            </View>
          )}
        />
      )}
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 16,
    backgroundColor: '#f8fafc',
  },
  center: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
    padding: 24,
  },
  title: {
    fontSize: 24,
    fontWeight: 'bold',
    marginBottom: 16,
  },
  card: {
    padding: 14,
    marginBottom: 10,
    backgroundColor: '#ffffff',
    borderRadius: 8,
  },
  name: {
    fontSize: 16,
    fontWeight: 'bold',
  },
  error: {
    color: '#b91c1c',
    marginVertical: 12,
  },
  button: {
    padding: 12,
    backgroundColor: '#2563eb',
    borderRadius: 6,
  },
  buttonText: {
    color: '#ffffff',
    fontWeight: 'bold',
  },
});

9.1 How the Example Works

  1. The screen starts with an empty users array and a loading state set to true.
  2. The effect calls loadUsers when the screen mounts.
  3. The function sends a GET request to the users API.
  4. The response status is checked before parsing the JSON body.
  5. The returned value is checked to ensure it is an array.
  6. Successful data is saved in the users state.
  7. If a request fails, an error message is stored.
  8. The finally block turns off the loading state whether the request succeeds or fails.
  9. The Retry button triggers another request.
  10. The screen displays a loading indicator, an error state, an empty state, or the user list as appropriate.

For production use, consider request cancellation and stale-response protection when a screen can unmount or issue overlapping requests. The simple example is intended to demonstrate the main concepts.

10. Understanding the Success, Loading, Empty, and Error States

A robust data-driven screen should distinguish between several possible states instead of treating every situation as either loading or success.

State Condition What to Display
Loading The request is in progress and no usable data is available. Spinner or skeleton placeholder.
Success The request completed and returned usable data. Content, cards, or list items.
Empty The request succeeded but returned no records. A message such as "No products found."
Error The request failed and no usable data is available. Error message and Retry action.
Refreshing A new request is running while previous data is available. Existing content with a refresh indicator.
Partial failure Some data is available but another operation failed. Available content plus a clear warning.

Keeping these states separate avoids confusing behavior. For example, an empty list should not automatically mean that an API request failed, and a failed refresh should not necessarily erase previously loaded data.

11. Using FlatList to Display API Data

FlatList is a React Native component designed to render scrollable lists efficiently. It renders items as needed rather than creating every item at once.

11.1 Basic FlatList Structure

<FlatList
  data={products}
  keyExtractor={item => String(item.id)}
  renderItem={({ item }) => (
    <View>
      <Text>{item.name}</Text>
      <Text>Price: {item.price}</Text>
    </View>
  )}
  ListEmptyComponent={
    <Text>No products available.</Text>
  }
/>

11.2 Important FlatList Props

Prop Purpose
data The array of items to display.
renderItem Defines the UI for each item.
keyExtractor Creates a stable, unique key for each item.
ListEmptyComponent Displays content when the list is empty.
ListHeaderComponent Displays a header before the list items.
ListFooterComponent Displays content after the list items, such as a pagination spinner.
refreshing Controls the pull-to-refresh indicator.
onRefresh Runs a callback when the user pulls to refresh.

12. Implementing Pull-to-Refresh

Pull-to-refresh lets users request updated data by dragging a list downward. React Native's FlatList supports this interaction through the refreshing and onRefresh props.

<FlatList
  data={users}
  keyExtractor={item => String(item.id)}
  renderItem={({ item }) => (
    <Text>{item.name}</Text>
  )}
  refreshing={refreshing}
  onRefresh={refreshUsers}
/>

The refreshUsers function should start a request, set refreshing to true, update the data when successful, and set refreshing back to false in a finally block.

12.1 Example Refresh Function

const [refreshing, setRefreshing] = useState(false);

const refreshUsers = async () => {
  setRefreshing(true);

  try {
    const response = await fetch(USERS_URL);

    if (!response.ok) {
      throw new Error('Refresh failed.');
    }

    const result = await response.json();
    setUsers(result);
    setError(null);
  } catch (err) {
    setError('Unable to refresh users.');
  } finally {
    setRefreshing(false);
  }
};

In this example, USERS_URL, users, setUsers, setError, and the state variables should be defined in the same component. If a refresh fails, keep the existing list visible whenever possible and show a non-blocking error message.

13. Handling Network Failures and Timeouts

Mobile network conditions can change as users move between Wi-Fi and cellular networks. A request may fail because the device is offline, the server cannot be reached, or the connection is too slow.

13.1 Network Failure Handling

try {
  const response = await fetch(USERS_URL);

  if (!response.ok) {
    throw new Error(`HTTP error: ${response.status}`);
  }

  const result = await response.json();
  setUsers(result);
} catch (error) {
  setError('Unable to connect. Please try again.');
}

This catch block handles rejected requests and errors thrown by the code. For production applications, the application may classify failures into network, authentication, validation, and server errors so that each can be handled appropriately.

13.2 Request Timeouts with AbortController

Some requests may take longer than expected. The Fetch API supports cancellation with AbortController in compatible React Native environments. Verify support in the React Native version and runtime used by your project.

async function fetchWithTimeout(url, timeoutMs = 10000) {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => {
    controller.abort();
  }, timeoutMs);

  try {
    const response = await fetch(url, {
      signal: controller.signal,
    });

    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }

    return await response.json();
  } finally {
    clearTimeout(timeoutId);
  }
}

This helper aborts the request after the configured timeout and clears the timer when the request settles. An aborted request rejects, so the caller should catch the error and decide whether to show a timeout message or treat the cancellation as an expected event.

14. Preventing State Updates After a Screen Unmounts

A user may navigate away from a screen while its network request is still running. The response may arrive after the component is no longer active. Cancelling the request when possible can reduce unnecessary work and prevent stale results from being used.

14.1 Cancelling a Fetch Request in useEffect

useEffect(() => {
  const controller = new AbortController();

  async function loadData() {
    try {
      const response = await fetch(USERS_URL, {
        signal: controller.signal,
      });

      if (!response.ok) {
        throw new Error('Request failed.');
      }

      const result = await response.json();
      setUsers(result);
      setError(null);
    } catch (err) {
      if (err.name !== 'AbortError') {
        setError('Unable to load users.');
      }
    } finally {
      if (!controller.signal.aborted) {
        setLoading(false);
      }
    }
  }

  loadData();

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

The cleanup function aborts the request when the effect is cleaned up. The catch block ignores an expected abort instead of presenting it as a normal network error. This pattern is particularly useful when the component owns the request and no other screen needs the result.

15. Avoiding Duplicate and Stale Requests

Duplicate requests can waste bandwidth, while stale responses can overwrite newer data. These problems can occur when users tap controls repeatedly, change search terms quickly, or navigate between screens.

15.1 Practical Techniques

  • Disable a submit or refresh button while its request is running when repeated actions are not appropriate.
  • Cancel an older search request when a newer search begins.
  • Use a request identifier or query key to associate responses with the request that created them.
  • Use caching and deduplication where appropriate.
  • Do not assume that network requests complete in the same order they were started.
  • For shared data, consider a dedicated data-fetching library such as TanStack Query or Redux Toolkit Query.

15.2 Example: Preventing Duplicate Button Submissions

const [submitting, setSubmitting] = useState(false);

async function handleSubmit() {
  if (submitting) return;

  setSubmitting(true);

  try {
    await submitForm();
  } catch (error) {
    setError('Unable to submit the form.');
  } finally {
    setSubmitting(false);
  }
}

In this example, submitForm() represents the application's actual request function. The disabled state of the button should also reflect submitting. For operations that must never be duplicated, the backend should enforce suitable idempotency or duplicate-request protections as well.

16. Handling API Response Data Safely

Successful HTTP status codes do not guarantee that the response contains the exact data structure your screen expects. Validate important fields before using them.

16.1 Basic Data Validation

const response = await fetch(USERS_URL);

if (!response.ok) {
  throw new Error('Unable to retrieve users.');
}

const result = await response.json();

if (!Array.isArray(result)) {
  throw new Error('The API returned an unexpected format.');
}

setUsers(result);

For larger applications, use a schema-validation library or a shared API layer to validate complex response structures. Avoid displaying raw server error bodies to users because they may contain internal details.

16.2 Handle Missing Fields

<Text>{item.name ?? 'Unnamed user'}</Text>
<Text>{item.email ?? 'Email not available'}</Text>

Fallback values help the UI remain stable when optional fields are absent. Do not use fallbacks to hide invalid or incomplete data when those fields are required for the application's logic.

17. Using Axios for Data Fetching

Axios is a popular HTTP client that provides request configuration, interceptors, and convenient JSON handling. It is an alternative to the built-in Fetch API.

17.1 Installing Axios

npm install axios

17.2 Basic Axios Request

import axios from 'axios';

async function getUsers() {
  const response = await axios.get(
    'https://jsonplaceholder.typicode.com/users'
  );

  return response.data;
}

Axios rejects its Promise by default for responses outside its configured successful status range, unlike Fetch, which requires an explicit response status check. Both approaches still require thoughtful loading, error, and cancellation handling.

17.3 Fetch vs Axios

Feature Fetch API Axios
Installation Built in to supported environments. Requires a package.
JSON response Call response.json(). Usually available in response.data.
HTTP errors Check response.ok or status. Non-success responses reject by default.
Interceptors Must be implemented separately. Built-in interceptor support.
Best use Simple requests with minimal dependencies. Applications needing centralized HTTP configuration.

18. Centralizing API Requests

As an application grows, writing the same request logic in every screen becomes difficult to maintain. A centralized API utility can manage the base URL, headers, common errors, and reusable request functions.

18.1 Example API Utility

const API_BASE_URL = 'https://example.com/api';

export async function apiGet(path) {
  const response = await fetch(`${API_BASE_URL}${path}`, {
    method: 'GET',
    headers: {
      Accept: 'application/json',
    },
  });

  if (!response.ok) {
    throw new Error(`Request failed with status ${response.status}`);
  }

  return response.json();
}

A screen can then call the shared function:

const products = await apiGet('/products');

Replace the placeholder domain with your own backend. In a production application, the API layer may also normalize errors, attach authentication headers securely, and validate response data.

19. Organizing Data-Fetching Code

Separating screen components from API logic makes code easier to test and reuse.

src/
  api/
    client.js
    usersApi.js
  components/
    LoadingView.js
    ErrorView.js
    EmptyView.js
    UserCard.js
  screens/
    UsersScreen.js
  hooks/
    useUsers.js

Possible responsibilities:

  • client.js: common HTTP configuration and error handling.
  • usersApi.js: functions that communicate with user endpoints.
  • LoadingView.js: reusable loading UI.
  • ErrorView.js: error text and retry control.
  • EmptyView.js: UI for a successful request with no records.
  • useUsers.js: reusable state and request logic for user data.
  • UsersScreen.js: combines data and UI into a screen.

20. Data Fetching with a Custom Hook

A custom hook can move repeated loading, success, and error logic out of a screen component. This is useful when multiple screens share similar request behavior.

20.1 Example useUsers Hook

import { useCallback, useEffect, useState } from 'react';

const USERS_URL = 'https://jsonplaceholder.typicode.com/users';

export function useUsers() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  const refetch = useCallback(async () => {
    setLoading(true);
    setError(null);

    try {
      const response = await fetch(USERS_URL);

      if (!response.ok) {
        throw new Error('Unable to load users.');
      }

      const result = await response.json();

      if (!Array.isArray(result)) {
        throw new Error('Unexpected response format.');
      }

      setUsers(result);
    } catch (err) {
      setError('Could not load users. Please try again.');
    } finally {
      setLoading(false);
    }
  }, []);

  useEffect(() => {
    refetch();
  }, [refetch]);

  return { users, loading, error, refetch };
}

Use the hook in a screen:

const { users, loading, error, refetch } = useUsers();

This basic hook illustrates separation of concerns. For production, extend it with request cancellation, stale-response protection, and an explicit refresh strategy where needed.

21. Introduction to TanStack Query and Redux Toolkit Query

For applications that make many API requests, a dedicated data-fetching library can reduce repetitive code and provide caching, request deduplication, refetching, and mutation support.

21.1 TanStack Query

TanStack Query manages server state through queries and mutations. It can track fetching status, cache data by query key, and refresh data according to configured rules.

Installation:

npm install @tanstack/react-query

React Native applications need to configure a QueryClient and wrap the relevant component tree in QueryClientProvider. Online and app-focus behavior may require additional integration with React Native's lifecycle and network status.

21.2 Redux Toolkit Query

Redux Toolkit Query (RTK Query) is part of Redux Toolkit. It provides endpoint definitions, generated React hooks, caching, and request-status information.

RTK Query can be useful when an application already uses Redux Toolkit or wants a centralized API layer integrated with its Redux store.

21.3 Choosing an Approach

Approach Best Fit
Fetch API Small projects or straightforward API requests.
Axios Projects needing shared HTTP configuration or interceptors.
Custom hook Reusable fetching logic for a small number of related screens.
TanStack Query Applications needing server-state caching and lifecycle management.
RTK Query Applications using Redux Toolkit that want integrated API management.

22. Security Best Practices for API Requests

API integration must consider data protection and authentication in addition to successful responses.

  • Use HTTPS for network communication.
  • Never hardcode private server credentials or secret API keys in a mobile application.
  • Store authentication tokens using an appropriate secure storage solution when required.
  • Do not log passwords, access tokens, or sensitive personal information.
  • Validate user input on the client and always validate it on the server.
  • Handle 401 and 403 responses deliberately.
  • Avoid displaying internal stack traces or sensitive backend error details.
  • Use only the permissions and data that the application actually needs.
  • Follow the backend's authentication, rate-limit, and retry requirements.

23. Performance Best Practices

Efficient data fetching improves responsiveness, reduces network usage, and helps the application perform well on slower devices.

  • Use FlatList for long lists rather than rendering a large array of items inside a ScrollView.
  • Fetch only the data required for the current screen.
  • Use pagination or infinite scrolling for large datasets.
  • Cache suitable responses when freshness requirements allow it.
  • Debounce search requests to avoid sending a request for every keystroke.
  • Cancel obsolete requests when the user changes screens or search terms.
  • Avoid refetching unchanged data unnecessarily.
  • Keep previous data visible during a background refresh when appropriate.
  • Use stable list keys and avoid unnecessary re-renders.
  • Measure performance before introducing complicated optimizations.

24. Common Mistakes and Troubleshooting

Problem Possible Cause Solution
Data does not appear. Incorrect URL, failed request, or state not updated. Check the endpoint, response, and state update logic.
Loading spinner never stops. Missing cleanup or missing finally block. Set the loading state to false in finally when appropriate.
404 or 500 is treated as success. Fetch response status was not checked. Check response.ok before parsing the body.
JSON parsing fails. Response is not valid JSON or has no body. Inspect the response format and handle empty responses where expected.
List renders incorrectly. Wrong data structure or missing keys. Verify that data is an array and use stable unique keys.
Blank screen during refresh. Existing data is cleared unnecessarily. Keep existing content visible during background refreshes.
Repeated API requests. Incorrect effect dependencies or duplicate user actions. Review hooks, dependencies, and request triggers.
Error appears after leaving a screen. Request is still active after navigation. Cancel the request or ignore obsolete results during cleanup.
Retry button does not work. Retry callback does not start another request. Connect the button to a refetch function and reset the error state.
Old search results replace new results. Requests finish in a different order from when they started. Cancel old requests or check request identifiers before updating state.
Request fails on a device. Server access, certificate, network, or environment configuration issue. Check device connectivity and server configuration; do not rely only on a simulator.

25. Best Practices Checklist

  • Always represent loading, success, empty, and error states appropriately.
  • Use async/await with try/catch/finally for readable asynchronous code.
  • Check HTTP response status codes before assuming a request succeeded.
  • Validate the response shape before rendering it.
  • Show clear, actionable messages when a request fails.
  • Provide a Retry action when repeating the request is appropriate.
  • Keep previously loaded content visible during non-blocking refreshes.
  • Cancel requests that are no longer needed when practical.
  • Prevent duplicate submissions and protect against stale responses.
  • Keep API logic reusable and separate from presentation where useful.
  • Use HTTPS and protect authentication credentials.
  • Test slow networks, empty responses, HTTP errors, and offline behavior.

26. Practical Project: Product Listing Application

Project Objective: Build a React Native screen that fetches products from an API and displays loading, success, empty, error, and refresh states.

26.1 Project Requirements

  • Fetch product information from a working API endpoint.
  • Display product names, descriptions, and prices.
  • Show a loading indicator during the initial request.
  • Show a user-friendly error message if the request fails.
  • Add a Retry button.
  • Show an empty state if the API returns an empty array.
  • Implement pull-to-refresh.
  • Keep the interface responsive while refreshing.
  • Validate the response before updating state.
  • Test the application with successful and unsuccessful requests.

26.2 Suggested Workflow

  1. Create a Products screen.
  2. Identify a suitable products API and inspect its response structure.
  3. Create state variables for products, loading, error, and refreshing.
  4. Write an asynchronous function to request the data.
  5. Check the HTTP response status and validate the JSON response.
  6. Render the initial loading state.
  7. Render product cards with FlatList after a successful request.
  8. Render an empty state when no products are returned.
  9. Render an error message and Retry control after a failed request.
  10. Implement pull-to-refresh and test the different states.

26.3 Expected Result

The application should display a progress indicator while the initial request is running. After a successful response, it should show a scrollable product list. If the response contains no products, it should show an empty-state message. If the request fails, it should display a helpful error and allow the user to retry.

27. Practice Exercises

  1. Fetch a list of users from a public test API and render their names.
  2. Add an ActivityIndicator while the request is in progress.
  3. Simulate an invalid URL and display an error message.
  4. Add a Retry button that repeats the request.
  5. Display a separate message when the API returns an empty array.
  6. Implement pull-to-refresh using FlatList.
  7. Add response validation before storing the data.
  8. Cancel a request when the screen unmounts.
  9. Build a product list with pagination or infinite scrolling.
  10. Refactor repeated request logic into a custom hook.

28. Interview Questions and Answers

Q1. What is data fetching in React Native?

Data fetching is the process of requesting data from an external source, such as a backend API, and displaying the response in a mobile application.

Q2. What is the Fetch API?

The Fetch API is a built-in interface for making HTTP requests and handling asynchronous responses.

Q3. What is a loading state?

A loading state indicates that a request or another asynchronous operation is currently in progress.

Q4. What is an error state?

An error state represents a failed operation and provides feedback or an action that helps the user recover.

Q5. Why do we use async/await?

Async/await makes Promise-based asynchronous operations easier to read and allows errors to be handled using try/catch.

Q6. Why is finally useful when fetching data?

The finally block executes after the try/catch flow settles, making it a suitable place to reset loading indicators for requests that have completed.

Q7. Does Fetch throw an error for HTTP 404 responses?

No. Fetch normally resolves with a Response object for HTTP errors. Check response.ok or response.status to identify unsuccessful HTTP responses.

Q8. What is the purpose of ActivityIndicator?

ActivityIndicator displays a native progress spinner while an operation is running.

Q9. What is FlatList used for?

FlatList efficiently renders scrollable lists of data in React Native.

Q10. What is pull-to-refresh?

Pull-to-refresh allows users to manually request updated data by pulling a list downward.

Q11. How can a Fetch request be cancelled?

In compatible environments, an AbortController can provide an AbortSignal to Fetch. Calling abort() cancels the request.

Q12. Why should API responses be validated?

Validation helps prevent unexpected data structures or missing fields from causing UI errors or incorrect application behavior.

Q13. What is the difference between loading and refreshing?

Loading often refers to the initial request before usable data exists. Refreshing refers to requesting newer data while previously loaded content may remain visible.

Q14. Why use a custom hook for data fetching?

A custom hook encapsulates reusable request and state-management logic so multiple components can share the same behavior.

Q15. What are TanStack Query and RTK Query?

They are data-fetching and server-state tools that provide features such as caching, request status tracking, and refetching. RTK Query is integrated with Redux Toolkit.

Q16. How should a mobile app handle an offline request?

It should handle the failed request gracefully, show a clear message, preserve existing content when appropriate, and provide a way to retry when the connection returns.

Q17. How can duplicate requests be reduced?

Use appropriate request-state guards, debouncing, cancellation, caching, or request deduplication depending on the situation.

Q18. Why should sensitive information not appear in error messages?

Detailed internal errors may expose implementation details, credentials, or private information. User-facing messages should be clear without revealing sensitive data.

29. Quick Revision Notes

  • Data fetching: Requesting information from an API or other external source.
  • Fetch: Built-in API for making HTTP requests.
  • async/await: Syntax for writing asynchronous Promise-based code.
  • useState: Stores fetched data and UI status.
  • useEffect: Runs side effects such as initial data fetching.
  • Loading state: Indicates that a request is in progress.
  • Success state: Displays valid returned data.
  • Empty state: Displays feedback when a successful request returns no records.
  • Error state: Explains that a request failed and may offer retry.
  • ActivityIndicator: Native progress spinner.
  • FlatList: Efficient list rendering.
  • AbortController: Cancels supported asynchronous requests.
  • Retry: Starts a new request after failure.
  • TanStack Query / RTK Query: Tools for managing server data, caching, and request states.

30. Conclusion

Data fetching, loading states, and error states are essential skills for building responsive and dependable React Native applications. A good implementation requests data correctly, validates the response, handles unsuccessful HTTP statuses, and presents clear feedback throughout the request lifecycle.

Start with the Fetch API, use useState and useEffect to manage the request lifecycle, and render distinct loading, success, empty, and error states. As the application grows, consider reusable API utilities, custom hooks, request cancellation, and data-fetching libraries such as TanStack Query or Redux Toolkit Query.

For more learning opportunities, visit JustAcademy's React Native Training Course, register through the course demo registration page, or explore the JustAcademy website.

whatsapp