DailyDevDiet

logo - dailydevdiet

Learn. Build. Innovate. Elevate your coding skills with dailydevdiet!

Chapter 15: Network Requests and API Integration

Network Requests

Introduction

Network requests and API integration are fundamental aspects of modern mobile app development. Most React Native applications need to communicate with external services to fetch data, authenticate users, or synchronize information. This chapter covers everything you need to know about making HTTP requests, handling responses, managing loading states, and integrating with REST APIs in React Native.

Understanding HTTP Requests

HTTP (HyperText Transfer Protocol) is the foundation of data communication on the web. React Native provides several ways to make HTTP requests, with the built-in fetch API being the most common approach.

Common HTTP Methods

  • GET: Retrieve data from a server
  • POST: Send data to create a new resource
  • PUT: Update an existing resource completely
  • PATCH: Update specific fields of a resource
  • DELETE: Remove a resource

The Fetch API

The fetch API is a modern, promise-based approach to making HTTP requests in React Native. It’s built into React Native and provides a clean interface for network operations.

Basic Fetch Example

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

const BasicFetchExample = () => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch('https://jsonplaceholder.typicode.com/posts/1');
       
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
       
        const result = await response.json();
        setData(result);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, []);

  if (loading) {
    return (
      <View style={styles.centered}>
        <ActivityIndicator size="large" color="#0000ff" />
        <Text>Loading...</Text>
      </View>
    );
  }

  if (error) {
    return (
      <View style={styles.centered}>
        <Text style={styles.error}>Error: {error}</Text>
      </View>
    );
  }

  return (
    <View style={styles.container}>
      <Text style={styles.title}>{data.title}</Text>
      <Text style={styles.body}>{data.body}</Text>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 20,
    backgroundColor: '#fff',
  },
  centered: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  title: {
    fontSize: 18,
    fontWeight: 'bold',
    marginBottom: 10,
  },
  body: {
    fontSize: 16,
    lineHeight: 24,
  },
  error: {
    color: 'red',
    fontSize: 16,
  },
});

export default BasicFetchExample;

Creating a Custom Hook for API Calls

Creating reusable custom hooks for API calls helps maintain clean code and reduces duplication:

import { useState, useEffect } from 'react';

const useApi = (url, options = {}) => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  const fetchData = async () => {
    try {
      setLoading(true);
      setError(null);
     
      const response = await fetch(url, options);
     
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
     
      const result = await response.json();
      setData(result);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    fetchData();
  }, [url]);

  const refetch = () => {
    fetchData();
  };

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

// Usage example
const PostList = () => {
  const { data: posts, loading, error, refetch } = useApi(
    'https://jsonplaceholder.typicode.com/posts'
  );

  if (loading) return <Text>Loading posts...</Text>;
  if (error) return <Text>Error: {error}</Text>;

  return (
    <View>
      <Button title="Refresh" onPress={refetch} />
      {posts.map(post => (
        <View key={post.id} style={styles.postItem}>
          <Text style={styles.postTitle}>{post.title}</Text>
          <Text style={styles.postBody}>{post.body}</Text>
        </View>
      ))}
    </View>
  );
};

Handling Different HTTP Methods

GET Request

const fetchUsers = async () => {
  try {
    const response = await fetch('https://jsonplaceholder.typicode.com/users');
    const users = await response.json();
    return users;
  } catch (error) {
    console.error('Error fetching users:', error);
    throw error;
  }
};

POST Request

const createUser = async (userData) => {
  try {
    const response = await fetch('https://jsonplaceholder.typicode.com/users', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(userData),
    });
   
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
   
    const newUser = await response.json();
    return newUser;
  } catch (error) {
    console.error('Error creating user:', error);
    throw error;
  }
};

PUT Request

const updateUser = async (userId, userData) => {
  try {
    const response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`, {
      method: 'PUT',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(userData),
    });
   
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
   
    const updatedUser = await response.json();
    return updatedUser;
  } catch (error) {
    console.error('Error updating user:', error);
    throw error;
  }
};

DELETE Request

const deleteUser = async (userId) => {
  try {
    const response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`, {
      method: 'DELETE',
    });
   
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
   
    return true;
  } catch (error) {
    console.error('Error deleting user:', error);
    throw error;
  }
};

Complete CRUD Example

Here’s a complete example showing Create, Read, Update, and Delete operations:

import React, { useState, useEffect } from 'react';
import {
  View,
  Text,
  TextInput,
  Button,
  FlatList,
  StyleSheet,
  Alert,
  ActivityIndicator,
} from 'react-native';

const CRUDExample = () => {
  const [posts, setPosts] = useState([]);
  const [loading, setLoading] = useState(true);
  const [formData, setFormData] = useState({ title: '', body: '' });
  const [editingId, setEditingId] = useState(null);

  const API_BASE = 'https://jsonplaceholder.typicode.com/posts';

  // Fetch all posts
  const fetchPosts = async () => {
    try {
      setLoading(true);
      const response = await fetch(API_BASE);
      const data = await response.json();
      setPosts(data.slice(0, 10)); // Limit to 10 posts for demo
    } catch (error) {
      Alert.alert('Error', 'Failed to fetch posts');
    } finally {
      setLoading(false);
    }
  };

  // Create a new post
  const createPost = async () => {
    if (!formData.title || !formData.body) {
      Alert.alert('Error', 'Please fill in all fields');
      return;
    }

    try {
      const response = await fetch(API_BASE, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          title: formData.title,
          body: formData.body,
          userId: 1,
        }),
      });

      const newPost = await response.json();
      setPosts([newPost, ...posts]);
      setFormData({ title: '', body: '' });
      Alert.alert('Success', 'Post created successfully');
    } catch (error) {
      Alert.alert('Error', 'Failed to create post');
    }
  };

  // Update a post
  const updatePost = async () => {
    if (!formData.title || !formData.body) {
      Alert.alert('Error', 'Please fill in all fields');
      return;
    }

    try {
      const response = await fetch(`${API_BASE}/${editingId}`, {
        method: 'PUT',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          id: editingId,
          title: formData.title,
          body: formData.body,
          userId: 1,
        }),
      });

      const updatedPost = await response.json();
      setPosts(posts.map(post =>
        post.id === editingId ? updatedPost : post
      ));
      setFormData({ title: '', body: '' });
      setEditingId(null);
      Alert.alert('Success', 'Post updated successfully');
    } catch (error) {
      Alert.alert('Error', 'Failed to update post');
    }
  };

  // Delete a post
  const deletePost = async (postId) => {
    Alert.alert(
      'Confirm Delete',
      'Are you sure you want to delete this post?',
      [
        { text: 'Cancel', style: 'cancel' },
        {
          text: 'Delete',
          style: 'destructive',
          onPress: async () => {
            try {
              await fetch(`${API_BASE}/${postId}`, {
                method: 'DELETE',
              });
              setPosts(posts.filter(post => post.id !== postId));
              Alert.alert('Success', 'Post deleted successfully');
            } catch (error) {
              Alert.alert('Error', 'Failed to delete post');
            }
          },
        },
      ]
    );
  };

  // Edit a post
  const editPost = (post) => {
    setFormData({ title: post.title, body: post.body });
    setEditingId(post.id);
  };

  const cancelEdit = () => {
    setFormData({ title: '', body: '' });
    setEditingId(null);
  };

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

  const renderPost = ({ item }) => (
    <View style={styles.postItem}>
      <Text style={styles.postTitle}>{item.title}</Text>
      <Text style={styles.postBody}>{item.body}</Text>
      <View style={styles.postActions}>
        <Button title="Edit" onPress={() => editPost(item)} />
        <Button title="Delete" color="red" onPress={() => deletePost(item.id)} />
      </View>
    </View>
  );

  if (loading) {
    return (
      <View style={styles.centered}>
        <ActivityIndicator size="large" color="#0000ff" />
        <Text>Loading posts...</Text>
      </View>
    );
  }

  return (
    <View style={styles.container}>
      <Text style={styles.title}>Posts Manager</Text>
     
      <View style={styles.form}>
        <TextInput
          style={styles.input}
          placeholder="Post title"
          value={formData.title}
          onChangeText={(text) => setFormData({ ...formData, title: text })}
        />
        <TextInput
          style={styles.input}
          placeholder="Post body"
          value={formData.body}
          onChangeText={(text) => setFormData({ ...formData, body: text })}
          multiline
          numberOfLines={3}
        />
        <View style={styles.formActions}>
          <Button
            title={editingId ? 'Update Post' : 'Create Post'}
            onPress={editingId ? updatePost : createPost}
          />
          {editingId && (
            <Button title="Cancel" color="gray" onPress={cancelEdit} />
          )}
        </View>
      </View>

      <FlatList
        data={posts}
        renderItem={renderPost}
        keyExtractor={(item) => item.id.toString()}
        style={styles.list}
      />
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 20,
    backgroundColor: '#fff',
  },
  centered: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  title: {
    fontSize: 24,
    fontWeight: 'bold',
    marginBottom: 20,
    textAlign: 'center',
  },
  form: {
    marginBottom: 20,
    padding: 15,
    backgroundColor: '#f5f5f5',
    borderRadius: 10,
  },
  input: {
    borderWidth: 1,
    borderColor: '#ddd',
    padding: 10,
    marginBottom: 10,
    borderRadius: 5,
    backgroundColor: '#fff',
  },
  formActions: {
    flexDirection: 'row',
    justifyContent: 'space-between',
  },
  list: {
    flex: 1,
  },
  postItem: {
    backgroundColor: '#f9f9f9',
    padding: 15,
    marginBottom: 10,
    borderRadius: 5,
    borderWidth: 1,
    borderColor: '#ddd',
  },
  postTitle: {
    fontSize: 16,
    fontWeight: 'bold',
    marginBottom: 5,
  },
  postBody: {
    fontSize: 14,
    color: '#666',
    marginBottom: 10,
  },
  postActions: {
    flexDirection: 'row',
    justifyContent: 'space-between',
  },
});

export default CRUDExample;

Error Handling and Status Codes

Proper error handling is crucial for a good user experience:

const apiRequest = async (url, options = {}) => {
  try {
    const response = await fetch(url, options);
   
    // Handle different HTTP status codes
    if (response.status === 200 || response.status === 201) {
      return await response.json();
    } else if (response.status === 400) {
      throw new Error('Bad Request - Invalid data sent');
    } else if (response.status === 401) {
      throw new Error('Unauthorized - Please log in');
    } else if (response.status === 403) {
      throw new Error('Forbidden - You don\'t have permission');
    } else if (response.status === 404) {
      throw new Error('Not Found - Resource doesn\'t exist');
    } else if (response.status === 500) {
      throw new Error('Server Error - Please try again later');
    } else {
      throw new Error(`HTTP Error: ${response.status}`);
    }
  } catch (error) {
    if (error.name === 'TypeError') {
      throw new Error('Network Error - Please check your connection');
    }
    throw error;
  }
};

Authentication and Headers

Many APIs require authentication. Here’s how to handle authentication tokens:

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

class ApiService {
  constructor() {
    this.token = null;
  }

  setToken(token) {
    this.token = token;
  }

  getHeaders() {
    const headers = {
      'Content-Type': 'application/json',
    };

    if (this.token) {
      headers.Authorization = `Bearer ${this.token}`;
    }

    return headers;
  }

  async request(endpoint, options = {}) {
    const url = `${API_BASE}${endpoint}`;
    const config = {
      ...options,
      headers: {
        ...this.getHeaders(),
        ...options.headers,
      },
    };

    try {
      const response = await fetch(url, config);
     
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
     
      return await response.json();
    } catch (error) {
      console.error('API request failed:', error);
      throw error;
    }
  }

  async get(endpoint) {
    return this.request(endpoint, { method: 'GET' });
  }

  async post(endpoint, data) {
    return this.request(endpoint, {
      method: 'POST',
      body: JSON.stringify(data),
    });
  }

  async put(endpoint, data) {
    return this.request(endpoint, {
      method: 'PUT',
      body: JSON.stringify(data),
    });
  }

  async delete(endpoint) {
    return this.request(endpoint, { method: 'DELETE' });
  }
}

// Usage
const apiService = new ApiService();

// Login and set token
const login = async (credentials) => {
  try {
    const response = await apiService.post('/auth/login', credentials);
    apiService.setToken(response.token);
    return response;
  } catch (error) {
    throw error;
  }
};

// Make authenticated requests
const fetchUserProfile = async () => {
  try {
    const profile = await apiService.get('/user/profile');
    return profile;
  } catch (error) {
    throw error;
  }
};

Upload Files (Images)

Uploading files requires a different approach using FormData:

const uploadImage = async (imageUri) => {
  try {
    const formData = new FormData();
    formData.append('image', {
      uri: imageUri,
      type: 'image/jpeg',
      name: 'image.jpg',
    });

    const response = await fetch('https://api.example.com/upload', {
      method: 'POST',
      body: formData,
      headers: {
        'Content-Type': 'multipart/form-data',
      },
    });

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

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

Implementing Retry Logic

Sometimes network requests fail temporarily. Implementing retry logic can improve reliability:

const fetchWithRetry = async (url, options = {}, maxRetries = 3) => {
  for (let i = 0; i < maxRetries; i++) {
    try {
      const response = await fetch(url, options);
     
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
     
      return await response.json();
    } catch (error) {
      if (i === maxRetries - 1) {
        throw error;
      }
     
      // Wait before retrying (exponential backoff)
      const delay = Math.pow(2, i) * 1000;
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
};

Request Timeout

Add timeout functionality to prevent hanging requests:

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

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

    clearTimeout(timeoutId);
   
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
   
    return await response.json();
  } catch (error) {
    clearTimeout(timeoutId);
   
    if (error.name === 'AbortError') {
      throw new Error('Request timeout');
    }
   
    throw error;
  }
};

Network State Management

Create a comprehensive hook for managing network state:

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

const useApiCall = (apiFunction, dependencies = []) => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const cancelTokenRef = useRef(null);

  const execute = async (...args) => {
    try {
      setLoading(true);
      setError(null);
     
      const result = await apiFunction(...args);
      setData(result);
      return result;
    } catch (err) {
      setError(err.message);
      throw err;
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    return () => {
      if (cancelTokenRef.current) {
        cancelTokenRef.current.abort();
      }
    };
  }, []);

  const reset = () => {
    setData(null);
    setError(null);
    setLoading(false);
  };

  return {
    data,
    loading,
    error,
    execute,
    reset,
  };
};

// Usage example
const UserProfile = ({ userId }) => {
  const {
    data: user,
    loading,
    error,
    execute: fetchUser,
  } = useApiCall(apiService.get);

  useEffect(() => {
    fetchUser(`/users/${userId}`);
  }, [userId]);

  if (loading) return <ActivityIndicator size="large" />;
  if (error) return <Text>Error: {error}</Text>;
  if (!user) return <Text>No user data</Text>;

  return (
    <View>
      <Text style={styles.name}>{user.name}</Text>
      <Text style={styles.email}>{user.email}</Text>
    </View>
  );
};

Best Practices

1. Use Environment Variables for API URLs

// config.js
export const API_CONFIG = {
  BASE_URL: __DEV__
    ? 'https://api-dev.example.com'
    : 'https://api.example.com',
  TIMEOUT: 10000,
};

2. Implement Proper Loading States

const DataComponent = () => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [refreshing, setRefreshing] = useState(false);

  const fetchData = async (isRefresh = false) => {
    try {
      if (isRefresh) {
        setRefreshing(true);
      } else {
        setLoading(true);
      }

      const response = await fetch('/api/data');
      const result = await response.json();
      setData(result);
    } catch (error) {
      console.error('Error:', error);
    } finally {
      setLoading(false);
      setRefreshing(false);
    }
  };

  return (
    <View>
      {loading && <ActivityIndicator />}
      <FlatList
        data={data}
        refreshing={refreshing}
        onRefresh={() => fetchData(true)}
        renderItem={({ item }) => <Text>{item.title}</Text>}
      />
    </View>
  );
};

3. Cache API Responses

const cache = new Map();

const cachedFetch = async (url, options = {}) => {
  const cacheKey = `${url}${JSON.stringify(options)}`;
 
  if (cache.has(cacheKey)) {
    return cache.get(cacheKey);
  }

  try {
    const response = await fetch(url, options);
    const data = await response.json();
   
    cache.set(cacheKey, data);
   
    // Clear cache after 5 minutes
    setTimeout(() => cache.delete(cacheKey), 5 * 60 * 1000);
   
    return data;
  } catch (error) {
    throw error;
  }
};

4. Handle Offline States

import NetInfo from '@react-native-async-storage/async-storage';

const useNetworkStatus = () => {
  const [isConnected, setIsConnected] = useState(true);

  useEffect(() => {
    const unsubscribe = NetInfo.addEventListener(state => {
      setIsConnected(state.isConnected);
    });

    return unsubscribe;
  }, []);

  return isConnected;
};

// Usage
const DataComponent = () => {
  const isConnected = useNetworkStatus();
  const [data, setData] = useState(null);

  const fetchData = async () => {
    if (!isConnected) {
      Alert.alert('No Internet', 'Please check your connection');
      return;
    }

    try {
      const response = await fetch('/api/data');
      const result = await response.json();
      setData(result);
    } catch (error) {
      console.error('Error:', error);
    }
  };

  return (
    <View>
      {!isConnected && (
        <Text style={styles.offline}>You are offline</Text>
      )}
      {/* Rest of component */}
    </View>
  );
};

Common Pitfalls to Avoid

  1. Not handling errors properly: Always implement proper error handling
  2. Memory leaks: Cancel requests when components unmount
  3. Not showing loading states: Always provide feedback to users
  4. Hardcoded URLs: Use environment variables for different environments
  5. Not implementing retry logic: Network requests can fail temporarily
  6. Blocking the UI: Use proper async/await patterns
  7. Not validating responses: Always check response structure

Summary

Network requests and API integration are essential skills for React Native development. Key takeaways from this chapter:

  1. Use the Fetch API for making HTTP requests
  2. Handle errors gracefully with proper error handling
  3. Implement loading states for better UX
  4. Create reusable hooks for common API patterns
  5. Use authentication headers for secure APIs
  6. Implement retry logic for reliability
  7. Handle offline states appropriately
  8. Cache responses when appropriate
  9. Use environment variables for configuration
  10. Test error scenarios thoroughly

Understanding these concepts will help you build robust React Native applications that can effectively communicate with external services and handle various network conditions gracefully.

Next Steps

In the next chapter, we’ll explore local storage and data persistence, which complements network requests by allowing you to store data locally on the device for offline access and improved performance.

Related Articles:

Scroll to Top