Modern mobile applications rarely operate in isolation. Whether you're loading user data from a backend service, syncing state with a cloud database, or fetching real-time updates from an API, the ability to communicate with remote servers is fundamental to building compelling mobile experiences. React Native provides robust networking capabilities that leverage JavaScript's modern asynchronous patterns, enabling developers to build apps that feel responsive and connected.
Network quality directly shapes how users perceive your entire application. A slow or unreliable data layer can make even the most beautifully designed app feel broken, while smooth data synchronization builds trust and keeps users engaged with your content. This guide explores how to make Ajax-style requests in React Native, covering the built-in Fetch API, popular third-party libraries like Axios, and best practices for handling network operations efficiently and securely. Whether you're building a simple data display or a complex real-time application, understanding these patterns is essential for delivering the seamless experiences users expect from modern mobile apps.
For teams building comprehensive React applications, our web development services provide expertise in designing robust data layers that scale across mobile and desktop platforms.
Fetch API Basics
Built-in networking with Promise-based async patterns
Axios Integration
Enhanced request handling with interceptors and global config
Error Handling
Robust strategies for loading, errors, and retry logic
Performance Optimization
Request cancellation, deduplication, and caching
Security Best Practices
ATS configuration and secure communication patterns
Real-Time Communication
WebSocket connections for live data updates
Why Network Requests Matter in React Native
Every production React Native application eventually needs to exchange data with external services. This might involve authenticating users against a backend server, retrieving product catalogs from an e-commerce API, submitting form data to a CRM system, or pulling real-time updates from a websocket connection. The quality of these network interactions directly impacts how users perceive your application.
React Native runs JavaScript code in a separate thread from the native UI, which means network operations don't block the main thread or cause the interface to freeze. This architectural advantage allows you to build applications that remain responsive while handling potentially slow network operations in the background. However, this also means you need to carefully manage loading states, error conditions, and retry logic to provide a smooth user experience. Without proper state management, users might stare at frozen screens wondering whether the app is working or has crashed.
The framework provides multiple approaches to making HTTP requests, from the native Fetch API that mirrors web standards to more feature-rich libraries like Axios that offer additional capabilities like automatic JSON transformation, request interceptors, and built-in timeout handling. Understanding the strengths of each approach helps you make informed decisions about which tool to use for different scenarios in your application. For complex applications with advanced caching needs, libraries like TanStack Query (React Query) provide sophisticated data synchronization out of the box.
If you're building a comprehensive React ecosystem, exploring top React dashboard libraries can help you choose the right tools for your data visualization and management needs.
Using the Built-In Fetch API
React Native includes the Fetch API as a global function, making it the most straightforward option for making network requests. This API follows the same patterns you'd encounter when building for the web, which makes it particularly accessible if you have prior experience with web development. The Fetch API returns Promises, enabling clean asynchronous code through either .then() chains or the more modern async/await syntax.
Basic GET Requests
The simplest use of Fetch involves calling fetch() with a URL string to retrieve data from an endpoint. By default, this makes a GET request, which is appropriate for retrieving data without modifying server state. The returned Promise resolves with a Response object containing information about the HTTP response, including status codes, headers, and the response body.
// Basic GET request with async/await
const fetchUserData = async () => {
try {
const response = await fetch('https://api.example.com/users/123');
const userData = await response.json();
return userData;
} catch (error) {
console.error('Failed to fetch user:', error);
}
};
The response.json() method parses the response body as JSON, returning another Promise that resolves with the parsed data. This two-step process reflects the asynchronous nature of network operations where both the HTTP response and the data parsing take non-zero time to complete.
Making POST Requests with Custom Headers
For operations that modify data on the server, you'll need to specify additional configuration options. The Fetch API accepts an optional second argument that allows you to customize the HTTP method, headers, request body, and other request parameters.
const submitFormData = async (formData) => {
try {
const response = await fetch('https://api.example.com/forms/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': 'Bearer your-token-here'
},
body: JSON.stringify(formData)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
return result;
} catch (error) {
console.error('Form submission failed:', error);
throw error;
}
};
The Content-Type header tells the server what format you're sending, typically application/json for JSON payloads. Including appropriate headers ensures the server correctly interprets your request and returns the expected response format.
1const handleApiResponse = async (url, options = {}) => {2 const response = await fetch(url, options);3 4 if (!response.ok) {5 const errorBody = await response.json().catch(() => ({}));6 throw new Error(errorBody.message || `Request failed with status ${response.status}`);7 }8 9 // Only parse JSON if there's content10 const contentType = response.headers.get('content-type');11 if (contentType && contentType.includes('application/json')) {12 return await response.json();13 }14 15 return await response.text();16};Using Axios for Enhanced Request Handling
While the Fetch API provides everything needed for basic network operations, Axios offers a more feature-rich alternative that many developers prefer for production applications. This promise-based HTTP client includes automatic JSON transformation, request and response interceptors, and built-in timeout handling that can simplify your code and reduce boilerplate.
Installing and Configuring Axios
Adding Axios to your React Native project is straightforward through npm or yarn. Once installed, you can configure default settings that apply to all requests, such as a base URL for your API and common headers that should be included with every request.
import axios from 'axios';
// Configure global defaults
axios.defaults.baseURL = 'https://api.yourdomain.com';
axios.defaults.timeout = 10000;
axios.defaults.headers.common['Authorization'] = 'Bearer your-auth-token';
axios.defaults.headers.post['Content-Type'] = 'application/json';
Creating Reusable API Services
Organizing your API calls into dedicated service modules improves maintainability and makes it easier to manage different endpoints across your application. This pattern centralizes API logic, making it simple to update authentication mechanisms, change base URLs, or add logging without modifying multiple components.
// api/userService.js
import axios from 'axios';
const apiClient = axios.create({
baseURL: 'https://api.example.com',
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
});
export const userService = {
async getUsers() {
const response = await apiClient.get('/users');
return response.data;
},
async getUserById(id) {
const response = await apiClient.get(`/users/${id}`);
return response.data;
},
async createUser(userData) {
const response = await apiClient.post('/users', userData);
return response.data;
},
async updateUser(id, userData) {
const response = await apiClient.put(`/users/${id}`, userData);
return response.data;
},
async deleteUser(id) {
const response = await apiClient.delete(`/users/${id}`);
return response.data;
}
};
This service-oriented approach keeps your components clean and focused on UI concerns while providing a clear interface for data operations.
1// Request interceptor - runs before each request is sent2apiClient.interceptors.request.use(3 (config) => {4 config.metadata = { startTime: Date.now() };5 const token = getValidToken();6 if (token) {7 config.headers.Authorization = `Bearer ${token}`;8 }9 return config;10 },11 (error) => Promise.reject(error)12);13 14// Response interceptor - runs before responses reach calling code15apiClient.interceptors.response.use(16 (response) => {17 const duration = Date.now() - response.config.metadata.startTime;18 console.log(`Request completed in ${duration}ms`);19 return response;20 },21 (error) => {22 if (error.response?.status === 401) {23 authStore.logout();24 }25 return Promise.reject(error);26 }27);Managing Loading and Error States
Robust error handling and loading state management are essential for creating professional-quality mobile experiences. Users expect to see clear feedback when data is being loaded and helpful messages when something goes wrong. React's state management capabilities make it straightforward to implement these patterns consistently.
Implementing Loading States
Displaying appropriate loading indicators prevents users from wondering whether the application is working and sets clear expectations about wait times. Loading states should be managed at the component level but can be coordinated with global state management solutions for complex applications.
import React, { useState, useEffect } from 'react';
import { ActivityIndicator, Text, View, FlatList, TouchableOpacity } from 'react-native';
const UserList = () => {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
loadUsers();
}, []);
const loadUsers = async () => {
setLoading(true);
setError(null);
try {
const data = await userService.getUsers();
setUsers(data);
} catch (err) {
setError('Unable to load users. Please try again.');
} finally {
setLoading(false);
}
};
if (loading) {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<ActivityIndicator size="large" color="#0066cc" />
<Text style={{ marginTop: 16, color: '#666' }}>Loading users...</Text>
</View>
);
}
if (error) {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center', padding: 20 }}>
<Text style={{ color: '#cc0000', marginBottom: 16 }}>{error}</Text>
<TouchableOpacity onPress={loadUsers}>
<Text style={{ color: '#0066cc' }}>Try Again</Text>
</TouchableOpacity>
</View>
);
}
return (
<FlatList
data={users}
keyExtractor={(item) => item.id.toString()}
renderItem={({ item }) => <UserItem user={item} />}
/>
);
};
Handling Network Errors Gracefully
Network errors can occur for many reasons: poor connectivity, server unavailability, timeout issues, or invalid responses. Your error handling should distinguish between different error types and provide appropriate responses for each scenario.
const handleApiCall = async (apiFunction, setError) => {
try {
return await apiFunction();
} catch (error) {
if (error.code === 'ECONNABORTED') {
setError('The request timed out. Please check your connection and try again.');
} else if (!navigator.onLine) {
setError('You appear to be offline. Please check your internet connection.');
} else if (error.response) {
const status = error.response.status;
if (status === 404) {
setError('The requested resource was not found.');
} else if (status === 500) {
setError('A server error occurred. Please try again later.');
} else {
setError('An unexpected error occurred. Please try again.');
}
} else if (error.request) {
setError('Unable to reach the server. Please check your connection.');
} else {
setError('An unexpected error occurred.');
}
return null;
}
};
Optimizing Performance for Network Operations
Network performance directly affects how users perceive your application's speed and responsiveness. Even with fast servers, poor network request patterns can create sluggish user experiences. Several techniques can help minimize the perceived and actual latency of network operations.
Request Cancellation and Race Condition Prevention
When users navigate quickly between screens or trigger multiple searches in sequence, earlier requests might complete after later ones, causing incorrect data to appear. Implementing request cancellation prevents these race conditions and ensures your UI always displays the most recent data.
import { useRef, useEffect } from 'react';
const useCancellableRequests = () => {
const cancelSourceRef = useRef(null);
const createCancelToken = () => {
if (cancelSourceRef.current) {
cancelSourceRef.current.cancel('Request cancelled due to new request');
}
cancelSourceRef.current = axios.CancelToken.source();
return cancelSourceRef.current.token;
};
const cancelCurrentRequest = () => {
if (cancelSourceRef.current) {
cancelSourceRef.current.cancel('Component unmounted');
cancelSourceRef.current = null;
}
};
return { createCancelToken, cancelCurrentRequest };
};
// In your component
const SearchComponent = () => {
const { createCancelToken, cancelCurrentRequest } = useCancellableRequests();
const search = async (query) => {
cancelCurrentRequest();
try {
const response = await axios.get(`/search?q=${query}`, {
cancelToken: createCancelToken()
});
return response.data;
} catch (error) {
if (axios.isCancel(error)) {
return null;
}
throw error;
}
};
useEffect(() => {
return () => cancelCurrentRequest();
}, []);
};
Implementing Request Deduplication
Multiple components might request the same data simultaneously. Request deduplication prevents redundant network calls by tracking in-flight requests and returning the same Promise to all callers. For production applications, consider using established libraries like TanStack Query (React Query), which provide sophisticated caching, deduplication, background refetching, and optimistic updates out of the box.
Understanding how to optimize React performance through proper state management and memoization complements these networking techniques, ensuring your entire application runs smoothly.
Security Considerations for Network Requests
Mobile applications often handle sensitive user data and must communicate securely with backend services. Both iOS and Android have security features that affect how your application can make network requests, and understanding these helps you configure your app correctly.
App Transport Security on iOS
Since iOS 9, Apple requires apps to use HTTPS for all network connections by default, a feature called App Transport Security (ATS). This security measure prevents man-in-the-middle attacks and ensures data is transmitted encrypted. However, there are legitimate reasons to make HTTP requests to local development servers or specific APIs that haven't yet migrated to HTTPS. For local development, you can add ATS exceptions in your info.plist:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<false/>
<key>NSExceptionDomains</key>
<dict>
<key>localhost</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
</dict>
</dict>
</dict>
The recommended approach is to allow arbitrary loads only for specific domains that genuinely require it, rather than disabling ATS entirely.
Cleartext Traffic on Android
Similarly, Android 9 (API level 28) and above block cleartext (HTTP) traffic by default. For development purposes or when communicating with local servers, you can enable cleartext traffic through the network security configuration:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="false">
<trust-anchors>
<certificates src="system" />
</trust-anchors>
</base-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">10.0.2.2</domain>
<domain includeSubdomains="true">localhost</domain>
</domain-config>
</network-security-config>
Always prefer HTTPS for production applications to protect user data and comply with platform security requirements.
Alternative Networking Approaches
WebSocket Connections for Real-Time Communication
For applications requiring real-time updates like chat systems, live notifications, or collaborative features, WebSocket connections provide persistent, bidirectional communication channels. React Native includes native WebSocket support that integrates well with modern JavaScript patterns.
const connectToWebSocket = (url) => {
const websocket = new WebSocket(url);
websocket.onopen = () => {
console.log('WebSocket connected');
websocket.send(JSON.stringify({ type: 'subscribe', channel: 'updates' }));
};
websocket.onmessage = (event) => {
const data = JSON.parse(event.data);
messageStore.addMessage(data);
};
websocket.onerror = (error) => {
console.error('WebSocket error:', error);
};
websocket.onclose = (event) => {
console.log('WebSocket closed:', event.code, event.reason);
};
return websocket;
};
WebSockets maintain a persistent connection, reducing the overhead of repeated HTTP requests for real-time data flows.
When to Use Each Approach
- Fetch: Simple, one-off requests without complex requirements
- Axios: Production apps needing interceptors, automatic request cancellation, consistent error handling, or sophisticated configuration
- React Query: Complex apps with caching, background updates, or offline support needs
- WebSocket: Real-time features requiring persistent connections
For applications requiring AI-powered real-time features, exploring our AI automation services can help you integrate intelligent capabilities alongside robust networking infrastructure.
Best Practices Summary
Building reliable network functionality in React Native requires attention to multiple concerns: choosing the right tool for each job, handling the full range of possible outcomes, and architecting code for long-term maintainability.
Choose the Right Tool
- Use the built-in Fetch API for simple, one-off requests where you don't need additional features
- Choose Axios when you need interceptors, automatic request cancellation, consistent error handling, or sophisticated configuration options
- Consider React Query when your application has complex data management requirements including caching, background refetching, and optimistic updates
Handle All Outcomes
- Always implement proper loading and error states to provide clear user feedback
- Handle the full range of error conditions including network failures, timeout issues, and server errors
- Cancel unnecessary requests when components unmount or when new requests supersede old ones
Prioritize Security
- Configure security settings appropriately for development and production environments
- Use HTTPS in production and understand platform-specific requirements for cleartext traffic
- Protect sensitive data with proper authentication and encryption
By following these patterns and practices, you can build React Native applications that communicate reliably with backend services while providing excellent user experiences.
A well-designed data layer also connects seamlessly to your broader web development services, ensuring consistent data flow whether users access your application on mobile or desktop. For applications requiring real-time capabilities, integrating WebSocket connections with your existing API infrastructure creates a unified communication layer that scales with your user base.