
Working with APIs is a fundamental aspect of modern React applications. This chapter covers various techniques for fetching, caching, and managing API data, from basic fetch operations to advanced patterns using modern libraries and custom hooks.
Understanding APIs in React Context
APIs (Application Programming Interfaces) allow React applications to communicate with backend services, third-party services, and external data sources. Common API operations include:
- GET: Retrieving data
- POST: Creating new resources
- PUT/PATCH: Updating existing resources
- DELETE: Removing resources
Basic API Integration with Fetch
Simple Data Fetching
import React, { useState, useEffect } from 'react';
const UserList = () => {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchUsers = async () => {
try {
setLoading(true);
const response = await fetch('https://jsonplaceholder.typicode.com/users');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const userData = await response.json();
setUsers(userData);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchUsers();
}, []);
if (loading) return <div>Loading users...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div>
<h2>Users</h2>
<ul>
{users.map(user => (
<li key={user.id}>
{user.name} - {user.email}
</li>
))}
</ul>
</div>
);
};
export default UserList;
CRUD Operations
import React, { useState, useEffect } from 'react';
const PostManager = () => {
const [posts, setPosts] = useState([]);
const [newPost, setNewPost] = useState({ title: '', body: '' });
const [editingPost, setEditingPost] = useState(null);
const [loading, setLoading] = useState(false);
const API_BASE = 'https://jsonplaceholder.typicode.com';
// Fetch all posts
const fetchPosts = async () => {
setLoading(true);
try {
const response = await fetch(`${API_BASE}/posts`);
const postsData = await response.json();
setPosts(postsData.slice(0, 10)); // Limit to 10 posts for demo
} catch (error) {
console.error('Error fetching posts:', error);
} finally {
setLoading(false);
}
};
// Create new post
const createPost = async (postData) => {
try {
const response = await fetch(`${API_BASE}/posts`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(postData),
});
const newPostData = await response.json();
setPosts(prev => [newPostData, ...prev]);
setNewPost({ title: '', body: '' });
} catch (error) {
console.error('Error creating post:', error);
}
};
// Update post
const updatePost = async (id, postData) => {
try {
const response = await fetch(`${API_BASE}/posts/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(postData),
});
const updatedPost = await response.json();
setPosts(prev => prev.map(post =>
post.id === id ? updatedPost : post
));
setEditingPost(null);
} catch (error) {
console.error('Error updating post:', error);
}
};
// Delete post
const deletePost = async (id) => {
try {
await fetch(`${API_BASE}/posts/${id}`, {
method: 'DELETE',
});
setPosts(prev => prev.filter(post => post.id !== id));
} catch (error) {
console.error('Error deleting post:', error);
}
};
useEffect(() => {
fetchPosts();
}, []);
const handleSubmit = (e) => {
e.preventDefault();
if (newPost.title && newPost.body) {
createPost(newPost);
}
};
const handleUpdate = (e) => {
e.preventDefault();
if (editingPost) {
updatePost(editingPost.id, editingPost);
}
};
return (
<div className="post-manager">
<h2>Post Manager</h2>
{/* Create new post form */}
<form onSubmit={handleSubmit} className="post-form">
<h3>Create New Post</h3>
<input
type="text"
placeholder="Title"
value={newPost.title}
onChange={(e) => setNewPost({...newPost, title: e.target.value})}
/>
<textarea
placeholder="Body"
value={newPost.body}
onChange={(e) => setNewPost({...newPost, body: e.target.value})}
/>
<button type="submit">Create Post</button>
</form>
{/* Edit post form */}
{editingPost && (
<form onSubmit={handleUpdate} className="post-form">
<h3>Edit Post</h3>
<input
type="text"
value={editingPost.title}
onChange={(e) => setEditingPost({...editingPost, title: e.target.value})}
/>
<textarea
value={editingPost.body}
onChange={(e) => setEditingPost({...editingPost, body: e.target.value})}
/>
<button type="submit">Update Post</button>
<button type="button" onClick={() => setEditingPost(null)}>
Cancel
</button>
</form>
)}
{/* Posts list */}
{loading ? (
<div>Loading posts...</div>
) : (
<div className="posts-list">
{posts.map(post => (
<div key={post.id} className="post-item">
<h4>{post.title}</h4>
<p>{post.body}</p>
<div className="post-actions">
<button onClick={() => setEditingPost(post)}>
Edit
</button>
<button onClick={() => deletePost(post.id)}>
Delete
</button>
</div>
</div>
))}
</div>
)}
</div>
);
};
Custom Hooks for API Management
Basic API Hook
import { useState, useEffect } from 'react';
const useApi = (url, options = {}) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
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);
}
};
fetchData();
}, [url]);
const refetch = () => {
fetchData();
};
return { data, loading, error, refetch };
};
// Usage
const UserProfile = ({ userId }) => {
const { data: user, loading, error } = useApi(`/api/users/${userId}`);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
};
Advanced API Hook with Cache
import { useState, useEffect, useRef } from 'react';
// Simple cache implementation
const cache = new Map();
const useApiWithCache = (url, options = {}) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const abortControllerRef = useRef();
useEffect(() => {
const fetchData = async () => {
// Check cache first
if (cache.has(url)) {
setData(cache.get(url));
setLoading(false);
return;
}
try {
setLoading(true);
setError(null);
// Create abort controller for request cancellation
abortControllerRef.current = new AbortController();
const response = await fetch(url, {
...options,
signal: abortControllerRef.current.signal,
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
// Cache the result
cache.set(url, result);
setData(result);
} catch (err) {
if (err.name !== 'AbortError') {
setError(err.message);
}
} finally {
setLoading(false);
}
};
fetchData();
// Cleanup function to abort request if component unmounts
return () => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
};
}, [url]);
const clearCache = () => {
cache.delete(url);
};
const refetch = () => {
clearCache();
// Trigger useEffect by updating a dependency
};
return { data, loading, error, refetch, clearCache };
};
CRUD Hook
import { useState, useCallback } from 'react';
const useCrud = (baseUrl) => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const makeRequest = useCallback(async (url, options = {}) => {
setLoading(true);
setError(null);
try {
const response = await fetch(url, {
headers: {
'Content-Type': 'application/json',
...options.headers,
},
...options,
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (err) {
setError(err.message);
throw err;
} finally {
setLoading(false);
}
}, []);
const create = useCallback(async (data) => {
return makeRequest(baseUrl, {
method: 'POST',
body: JSON.stringify(data),
});
}, [baseUrl, makeRequest]);
const read = useCallback(async (id) => {
const url = id ? `${baseUrl}/${id}` : baseUrl;
return makeRequest(url);
}, [baseUrl, makeRequest]);
const update = useCallback(async (id, data) => {
return makeRequest(`${baseUrl}/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
});
}, [baseUrl, makeRequest]);
const remove = useCallback(async (id) => {
return makeRequest(`${baseUrl}/${id}`, {
method: 'DELETE',
});
}, [baseUrl, makeRequest]);
return {
create,
read,
update,
remove,
loading,
error,
};
};
// Usage
const TodoManager = () => {
const [todos, setTodos] = useState([]);
const { create, read, update, remove, loading, error } = useCrud('/api/todos');
const loadTodos = async () => {
try {
const todosData = await read();
setTodos(todosData);
} catch (err) {
console.error('Failed to load todos:', err);
}
};
const addTodo = async (todoData) => {
try {
const newTodo = await create(todoData);
setTodos(prev => [...prev, newTodo]);
} catch (err) {
console.error('Failed to create todo:', err);
}
};
return (
<div>
{loading && <div>Loading...</div>}
{error && <div>Error: {error}</div>}
{/* Todo interface */}
</div>
);
};
Error Handling Strategies
Comprehensive Error Handler
import React, { useState } from 'react';
const ErrorBoundary = ({ children, fallback }) => {
const [hasError, setHasError] = useState(false);
if (hasError) {
return fallback || <div>Something went wrong.</div>;
}
return children;
};
const ApiErrorHandler = {
handle: (error, context = '') => {
console.error(`API Error in ${context}:`, error);
// Different handling based on error type
if (error.name === 'TypeError') {
return 'Network error. Please check your connection.';
}
if (error.message.includes('404')) {
return 'Resource not found.';
}
if (error.message.includes('401')) {
return 'Unauthorized. Please log in again.';
}
if (error.message.includes('403')) {
return 'Access forbidden.';
}
if (error.message.includes('500')) {
return 'Server error. Please try again later.';
}
return error.message || 'An unexpected error occurred.';
}
};
const useApiWithErrorHandling = (url) => {
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);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const result = await response.json();
setData(result);
} catch (err) {
const errorMessage = ApiErrorHandler.handle(err, 'Data Fetch');
setError(errorMessage);
} finally {
setLoading(false);
}
};
React.useEffect(() => {
fetchData();
}, [url]);
return { data, loading, error, refetch: fetchData };
};
Retry Logic
import { useState, useEffect } from 'react';
const useApiWithRetry = (url, maxRetries = 3, retryDelay = 1000) => {
 const [data, setData] = useState(null);
 const [loading, setLoading] = useState(true);
 const [error, setError] = useState(null);
 const [retryCount, setRetryCount] = useState(0);
 const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
 const fetchWithRetry = async (attempt = 0) => {
  try {
   setLoading(true);
   setError(null);
  Â
   const response = await fetch(url);
  Â
   if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
   }
  Â
   const result = await response.json();
   setData(result);
   setRetryCount(0);
  } catch (err) {
   if (attempt < maxRetries) {
    setRetryCount(attempt + 1);
    await delay(retryDelay * Math.pow(2, attempt)); // Exponential backoff
    return fetchWithRetry(attempt + 1);
   } else {
    setError(err.message);
   }
  } finally {
   setLoading(false);
  }
 };
 useEffect(() => {
  fetchWithRetry();
 }, [url]);
 return { data, loading, error, retryCount };
};
Axios Integration
Basic Axios Setup
import axios from 'axios';
// Create axios instance with base configuration
const api = axios.create({
baseURL: process.env.REACT_APP_API_BASE_URL || 'http://localhost:3001/api',
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});
// Request interceptor for adding auth tokens
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem('authToken');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// Response interceptor for handling common errors
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
// Handle unauthorized access
localStorage.removeItem('authToken');
window.location.href = '/login';
}
return Promise.reject(error);
}
);
export default api;
Axios Hook
import { useState, useEffect } from 'react';
import api from './axiosConfig';
const useAxios = (config, dependencies = []) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const source = axios.CancelToken.source();
const fetchData = async () => {
try {
setLoading(true);
setError(null);
const response = await api({
...config,
cancelToken: source.token,
});
setData(response.data);
} catch (err) {
if (!axios.isCancel(err)) {
setError(err.response?.data?.message || err.message);
}
} finally {
setLoading(false);
}
};
fetchData();
return () => {
source.cancel('Component unmounted');
};
}, dependencies);
return { data, loading, error };
};
// Usage
const UserProfile = ({ userId }) => {
const { data: user, loading, error } = useAxios({
method: 'GET',
url: `/users/${userId}`,
}, [userId]);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
};
React Query Integration
Basic React Query Setup
import { QueryClient, QueryClientProvider, useQuery, useMutation, useQueryClient } from 'react-query';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 10 * 60 * 1000, // 10 minutes
retry: 3,
},
},
});
const App = () => {
return (
<QueryClientProvider client={queryClient}>
<UserList />
</QueryClientProvider>
);
};
// API functions
const fetchUsers = async () => {
const response = await fetch('/api/users');
if (!response.ok) {
throw new Error('Failed to fetch users');
}
return response.json();
};
const createUser = async (userData) => {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData),
});
if (!response.ok) {
throw new Error('Failed to create user');
}
return response.json();
};
const UserList = () => {
const queryClient = useQueryClient();
const {
data: users,
isLoading,
error,
refetch
} = useQuery('users', fetchUsers);
const createUserMutation = useMutation(createUser, {
onSuccess: () => {
// Invalidate and refetch users query
queryClient.invalidateQueries('users');
},
});
const handleCreateUser = (userData) => {
createUserMutation.mutate(userData);
};
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h2>Users</h2>
<button onClick={() => handleCreateUser({ name: 'New User' })}>
Add User
</button>
{users.map(user => (
<div key={user.id}>{user.name}</div>
))}
</div>
);
};
API Authentication
JWT Token Management
import React, { createContext, useContext, useState, useEffect } from 'react';
const AuthContext = createContext();
export const useAuth = () => {
 const context = useContext(AuthContext);
 if (!context) {
  throw new Error('useAuth must be used within AuthProvider');
 }
 return context;
};
export const AuthProvider = ({ children }) => {
 const [token, setToken] = useState(localStorage.getItem('authToken'));
 const [user, setUser] = useState(null);
 const [loading, setLoading] = useState(true);
 useEffect(() => {
  if (token) {
   // Verify token and get user info
   fetchUserProfile();
  } else {
   setLoading(false);
  }
 }, [token]);
 const fetchUserProfile = async () => {
  try {
   const response = await fetch('/api/user/profile', {
    headers: {
     'Authorization': `Bearer ${token}`,
    },
   });
  Â
   if (response.ok) {
    const userData = await response.json();
    setUser(userData);
   } else {
    // Token is invalid
    logout();
   }
  } catch (error) {
   console.error('Failed to fetch user profile:', error);
   logout();
  } finally {
   setLoading(false);
  }
 };
 const login = async (credentials) => {
  try {
   const response = await fetch('/api/auth/login', {
    method: 'POST',
    headers: {
     'Content-Type': 'application/json',
    },
    body: JSON.stringify(credentials),
   });
   if (response.ok) {
    const { token: newToken, user: userData } = await response.json();
    setToken(newToken);
    setUser(userData);
    localStorage.setItem('authToken', newToken);
    return { success: true };
   } else {
    const error = await response.json();
    return { success: false, error: error.message };
   }
  } catch (error) {
   return { success: false, error: error.message };
  }
 };
 const logout = () => {
  setToken(null);
  setUser(null);
  localStorage.removeItem('authToken');
 };
 const value = {
  token,
  user,
  loading,
  login,
  logout,
  isAuthenticated: !!token && !!user,
 };
 return (
  <AuthContext.Provider value={value}>
   {children}
  </AuthContext.Provider>
 );
};
// Authenticated API hook
const useAuthenticatedApi = () => {
 const { token } = useAuth();
 const makeAuthenticatedRequest = async (url, options = {}) => {
  const config = {
   ...options,
   headers: {
    'Content-Type': 'application/json',
    ...(token && { Authorization: `Bearer ${token}` }),
    ...options.headers,
   },
  };
  const response = await fetch(url, config);
 Â
  if (!response.ok) {
   throw new Error(`HTTP error! status: ${response.status}`);
  }
 Â
  return response.json();
 };
 return { makeAuthenticatedRequest };
};
Performance Optimization
Request Deduplication
mport { useRef, useEffect, useState } from 'react';
// Cache for ongoing requests
const ongoingRequests = new Map();
const useDedupedApi = (url, options = {}) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const isMountedRef = useRef(true);
useEffect(() => {
const requestKey = `${url}-${JSON.stringify(options)}`;
// Check if request is already ongoing
if (ongoingRequests.has(requestKey)) {
ongoingRequests.get(requestKey).then(result => {
if (isMountedRef.current) {
setData(result);
setLoading(false);
}
}).catch(err => {
if (isMountedRef.current) {
setError(err.message);
setLoading(false);
}
});
return;
}
const fetchData = async () => {
try {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
return result;
} catch (err) {
throw err;
} finally {
ongoingRequests.delete(requestKey);
}
};
const requestPromise = fetchData();
ongoingRequests.set(requestKey, requestPromise);
requestPromise
.then(result => {
if (isMountedRef.current) {
setData(result);
setLoading(false);
}
})
.catch(err => {
if (isMountedRef.current) {
setError(err.message);
setLoading(false);
}
});
return () => {
isMountedRef.current = false;
};
}, [url, JSON.stringify(options)]);
return { data, loading, error };
};
Pagination Hook
const usePagination = (fetchFunction, initialPage = 1, pageSize = 10) => {
 const [data, setData] = useState([]);
 const [currentPage, setCurrentPage] = useState(initialPage);
 const [totalPages, setTotalPages] = useState(0);
 const [loading, setLoading] = useState(false);
 const [error, setError] = useState(null);
 const fetchPage = async (page) => {
  setLoading(true);
  setError(null);
 Â
  try {
   const result = await fetchFunction(page, pageSize);
   setData(result.data);
   setTotalPages(result.totalPages);
   setCurrentPage(page);
  } catch (err) {
   setError(err.message);
  } finally {
   setLoading(false);
  }
 };
 useEffect(() => {
  fetchPage(currentPage);
 }, []);
 const goToPage = (page) => {
  if (page >= 1 && page <= totalPages) {
   fetchPage(page);
  }
 };
 const nextPage = () => {
  if (currentPage < totalPages) {
   goToPage(currentPage + 1);
  }
 };
 const prevPage = () => {
  if (currentPage > 1) {
   goToPage(currentPage - 1);
  }
 };
 return {
  data,
  loading,
  error,
  currentPage,
  totalPages,
  goToPage,
  nextPage,
  prevPage,
  hasNext: currentPage < totalPages,
  hasPrev: currentPage > 1,
 };
};
// Usage
const ProductList = () => {
 const fetchProducts = async (page, pageSize) => {
  const response = await fetch(`/api/products?page=${page}&limit=${pageSize}`);
  return response.json();
 };
 const {
  data: products,
  loading,
  error,
  currentPage,
  totalPages,
  nextPage,
  prevPage,
  hasNext,
  hasPrev,
 } = usePagination(fetchProducts);
 if (loading) return <div>Loading...</div>;
 if (error) return <div>Error: {error}</div>;
 return (
  <div>
   <div className="products">
    {products.map(product => (
     <div key={product.id}>{product.name}</div>
    ))}
   </div>
  Â
   <div className="pagination">
    <button onClick={prevPage} disabled={!hasPrev}>
     Previous
    </button>
    <span>Page {currentPage} of {totalPages}</span>
    <button onClick={nextPage} disabled={!hasNext}>
     Next
    </button>
   </div>
  </div>
 );
};
Testing API Integration
Mocking API Calls
// __tests__/UserList.test.js
import { render, screen, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom';
import UserList from '../UserList';
// Mock fetch
global.fetch = jest.fn();
const mockUsers = [
{ id: 1, name: 'John Doe', email: 'john@example.com' },
{ id: 2, name: 'Jane Smith', email: 'jane@example.com' },
];
beforeEach(() => {
fetch.mockClear();
});
test('displays users after successful fetch', async () => {
fetch.mockResolvedValueOnce({
ok: true,
json: async () => mockUsers,
});
render(<UserList />);
expect(screen.getByText('Loading users...')).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText('John Doe - john@example.com')).toBeInTheDocument();
expect(screen.getByText('Jane Smith - jane@example.com')).toBeInTheDocument();
});
expect(fetch).toHaveBeenCalledTimes(1);
});
test('displays error message on fetch failure', async () => {
fetch.mockRejectedValueOnce(new Error('Network error'));
render(<UserList />);
await waitFor(() => {
expect(screen.getByText('Error: Network error')).toBeInTheDocument();
});
});
test('handles HTTP error responses', async () => {
fetch.mockResolvedValueOnce({
ok: false,
status: 404,
});
render(<UserList />);
await waitFor(() => {
expect(screen.getByText(/Error: HTTP error! status: 404/)).toBeInTheDocument();
});
});
Testing with Mock Service Worker (MSW)
// setupTests.js
import { setupServer } from 'msw/node';
import { rest } from 'msw';
const server = setupServer(
rest.get('/api/users', (req, res, ctx) => {
return res(
ctx.json([
{ id: 1, name: 'John Doe', email: 'john@example.com' },
{ id: 2, name: 'Jane Smith', email: 'jane@example.com' },
])
);
}),
rest.post('/api/users', (req, res, ctx) => {
return res(
ctx.json({ id: 3, name: 'New User', email: 'new@example.com' })
);
}),
rest.delete('/api/users/:id', (req, res, ctx) => {
return res(ctx.status(204));
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
export { server };
Integration Test Example
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import PostManager from '../PostManager';
import { server } from '../setupTests';
import { rest } from 'msw';
test('creates a new post successfully', async () => {
const user = userEvent.setup();
render(<PostManager />);
// Wait for initial posts to load
await waitFor(() => {
expect(screen.getByText(/Post Manager/)).toBeInTheDocument();
});
// Fill out the form
const titleInput = screen.getByPlaceholderText('Title');
const bodyInput = screen.getByPlaceholderText('Body');
const submitButton = screen.getByText('Create Post');
await user.type(titleInput, 'Test Post');
await user.type(bodyInput, 'This is a test post body');
await user.click(submitButton);
// Verify the new post appears
await waitFor(() => {
expect(screen.getByText('Test Post')).toBeInTheDocument();
expect(screen.getByText('This is a test post body')).toBeInTheDocument();
});
});
test('handles API error gracefully', async () => {
// Override the handler to return an error
server.use(
rest.get('/api/posts', (req, res, ctx) => {
return res(ctx.status(500));
})
);
render(<PostManager />);
await waitFor(() => {
expect(screen.getByText(/Error/)).toBeInTheDocument();
});
});
GraphQL Integration
Basic GraphQL Setup
import { ApolloClient, InMemoryCache, ApolloProvider, gql, useQuery, useMutation } from '@apollo/client';
const client = new ApolloClient({
uri: 'http://localhost:4000/graphql',
cache: new InMemoryCache(),
});
const GET_USERS = gql`
query GetUsers {
users {
id
name
email
posts {
id
title
}
}
}
`;
const CREATE_USER = gql`
mutation CreateUser($input: UserInput!) {
createUser(input: $input) {
id
name
email
}
}
`;
const UserListGraphQL = () => {
const { loading, error, data, refetch } = useQuery(GET_USERS);
const [createUser] = useMutation(CREATE_USER, {
refetchQueries: [{ query: GET_USERS }],
});
const handleCreateUser = async () => {
try {
await createUser({
variables: {
input: {
name: 'New User',
email: 'new@example.com',
},
},
});
} catch (err) {
console.error('Error creating user:', err);
}
};
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<div>
<h2>Users (GraphQL)</h2>
<button onClick={handleCreateUser}>Add User</button>
<ul>
{data.users.map(user => (
<li key={user.id}>
{user.name} - {user.email}
<ul>
{user.posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</li>
))}
</ul>
</div>
);
};
const App = () => {
return (
<ApolloProvider client={client}>
<UserListGraphQL />
</ApolloProvider>
);
};
API Configuration and Environment Management
Environment-based API Configuration
// config/api.js
const API_CONFIG = {
development: {
baseURL: 'http://localhost:3001/api',
timeout: 10000,
retries: 3,
},
staging: {
baseURL: 'https://staging-api.example.com/api',
timeout: 15000,
retries: 2,
},
production: {
baseURL: 'https://api.example.com/api',
timeout: 20000,
retries: 1,
},
};
const getApiConfig = () => {
const env = process.env.NODE_ENV || 'development';
return API_CONFIG[env];
};
export default getApiConfig();
API Service Class
/ services/ApiService.js
import apiConfig from '../config/api';
class ApiService {
constructor() {
this.baseURL = apiConfig.baseURL;
this.timeout = apiConfig.timeout;
this.defaultHeaders = {
'Content-Type': 'application/json',
};
}
setAuthToken(token) {
this.defaultHeaders.Authorization = `Bearer ${token}`;
}
removeAuthToken() {
delete this.defaultHeaders.Authorization;
}
async request(endpoint, options = {}) {
const url = `${this.baseURL}${endpoint}`;
const config = {
headers: {
...this.defaultHeaders,
...options.headers,
},
...options,
};
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
try {
const response = await fetch(url, {
...config,
signal: controller.signal,
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new Error('Request timeout');
}
throw error;
}
}
// CRUD methods
async get(endpoint, params = {}) {
const queryString = new URLSearchParams(params).toString();
const url = queryString ? `${endpoint}?${queryString}` : endpoint;
return this.request(url);
}
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 patch(endpoint, data) {
return this.request(endpoint, {
method: 'PATCH',
body: JSON.stringify(data),
});
}
async delete(endpoint) {
return this.request(endpoint, {
method: 'DELETE',
});
}
}
export default new ApiService();
Real-World API Integration Example
Complete User Management System
import React, { useState, useEffect } from 'react';
import ApiService from '../services/ApiService';
const UserManagementSystem = () => {
 const [users, setUsers] = useState([]);
 const [loading, setLoading] = useState(false);
 const [error, setError] = useState(null);
 const [selectedUser, setSelectedUser] = useState(null);
 const [isEditing, setIsEditing] = useState(false);
 const [searchTerm, setSearchTerm] = useState('');
 const [currentPage, setCurrentPage] = useState(1);
 const [totalPages, setTotalPages] = useState(1);
 const [formData, setFormData] = useState({
  name: '',
  email: '',
  role: 'user',
 });
 // Load users with pagination and search
 const loadUsers = async (page = 1, search = '') => {
  try {
   setLoading(true);
   setError(null);
  Â
   const params = {
    page,
    limit: 10,
    ...(search && { search }),
   };
  Â
   const response = await ApiService.get('/users', params);
  Â
   setUsers(response.data);
   setCurrentPage(response.currentPage);
   setTotalPages(response.totalPages);
  } catch (err) {
   setError(err.message);
  } finally {
   setLoading(false);
  }
 };
 // Create user
 const createUser = async (userData) => {
  try {
   setLoading(true);
   const newUser = await ApiService.post('/users', userData);
   setUsers(prev => [newUser, ...prev]);
   setFormData({ name: '', email: '', role: 'user' });
  } catch (err) {
   setError(err.message);
  } finally {
   setLoading(false);
  }
 };
 // Update user
 const updateUser = async (id, userData) => {
  try {
   setLoading(true);
   const updatedUser = await ApiService.put(`/users/${id}`, userData);
   setUsers(prev => prev.map(user =>
    user.id === id ? updatedUser : user
   ));
   setSelectedUser(null);
   setIsEditing(false);
  } catch (err) {
   setError(err.message);
  } finally {
   setLoading(false);
  }
 };
 // Delete user
 const deleteUser = async (id) => {
  if (!window.confirm('Are you sure you want to delete this user?')) {
   return;
  }
  try {
   setLoading(true);
   await ApiService.delete(`/users/${id}`);
   setUsers(prev => prev.filter(user => user.id !== id));
  } catch (err) {
   setError(err.message);
  } finally {
   setLoading(false);
  }
 };
 // Handle form submission
 const handleSubmit = (e) => {
  e.preventDefault();
 Â
  if (isEditing && selectedUser) {
   updateUser(selectedUser.id, formData);
  } else {
   createUser(formData);
  }
 };
 // Handle search
 const handleSearch = (e) => {
  e.preventDefault();
  loadUsers(1, searchTerm);
 };
 // Load initial data
 useEffect(() => {
  loadUsers();
 }, []);
 // Handle edit user
 const handleEditUser = (user) => {
  setSelectedUser(user);
  setFormData({
   name: user.name,
   email: user.email,
   role: user.role,
  });
  setIsEditing(true);
 };
 // Cancel editing
 const handleCancelEdit = () => {
  setSelectedUser(null);
  setIsEditing(false);
  setFormData({ name: '', email: '', role: 'user' });
 };
 return (
  <div className="user-management">
   <h1>User Management System</h1>
   {error && (
    <div className="error-message">
     Error: {error}
     <button onClick={() => setError(null)}>×</button>
    </div>
   )}
   {/* Search Form */}
   <form onSubmit={handleSearch} className="search-form">
    <input
     type="text"
     placeholder="Search users..."
     value={searchTerm}
     onChange={(e) => setSearchTerm(e.target.value)}
    />
    <button type="submit">Search</button>
    <button
     type="button"
     onClick={() => {
      setSearchTerm('');
      loadUsers();
     }}
    >
     Clear
    </button>
   </form>
   {/* User Form */}
   <form onSubmit={handleSubmit} className="user-form">
    <h3>{isEditing ? 'Edit User' : 'Create New User'}</h3>
   Â
    <input
     type="text"
     placeholder="Name"
     value={formData.name}
     onChange={(e) => setFormData({...formData, name: e.target.value})}
     required
    />
   Â
    <input
     type="email"
     placeholder="Email"
     value={formData.email}
     onChange={(e) => setFormData({...formData, email: e.target.value})}
     required
    />
   Â
    <select
     value={formData.role}
     onChange={(e) => setFormData({...formData, role: e.target.value})}
    >
     <option value="user">User</option>
     <option value="admin">Admin</option>
     <option value="moderator">Moderator</option>
    </select>
   Â
    <div className="form-actions">
     <button type="submit" disabled={loading}>
      {loading ? 'Processing...' : (isEditing ? 'Update User' : 'Create User')}
     </button>
     {isEditing && (
      <button type="button" onClick={handleCancelEdit}>
       Cancel
      </button>
     )}
    </div>
   </form>
   {/* Users List */}
   <div className="users-list">
    <h3>Users ({users.length})</h3>
   Â
    {loading && <div className="loading">Loading users...</div>}
   Â
    {users.length === 0 && !loading && (
     <div className="no-users">No users found.</div>
    )}
   Â
    <div className="users-grid">
     {users.map(user => (
      <div key={user.id} className="user-card">
       <div className="user-info">
        <h4>{user.name}</h4>
        <p>{user.email}</p>
        <span className={`role role-${user.role}`}>{user.role}</span>
       </div>
      Â
       <div className="user-actions">
        <button onClick={() => handleEditUser(user)}>
         Edit
        </button>
        <button
         onClick={() => deleteUser(user.id)}
         className="delete-btn"
         disabled={loading}
        >
         Delete
        </button>
       </div>
      </div>
     ))}
    </div>
    {/* Pagination */}
    {totalPages > 1 && (
     <div className="pagination">
      <button
       onClick={() => loadUsers(currentPage - 1, searchTerm)}
       disabled={currentPage === 1 || loading}
      >
       Previous
      </button>
     Â
      <span>
       Page {currentPage} of {totalPages}
      </span>
     Â
      <button
       onClick={() => loadUsers(currentPage + 1, searchTerm)}
       disabled={currentPage === totalPages || loading}
      >
       Next
      </button>
     </div>
    )}
   </div>
  </div>
 );
};
export default UserManagementSystem;
Best Practices
- Error Handling: Always implement comprehensive error handling for all API calls
- Loading States: Provide clear loading indicators for better user experience
- Caching: Implement appropriate caching strategies to reduce unnecessary requests
- Request Cancellation: Cancel ongoing requests when components unmount
- Retry Logic: Implement retry mechanisms for failed requests
- Authentication: Secure API calls with proper authentication tokens
- Validation: Validate data both on client and server sides
- Performance: Use pagination, debouncing, and request deduplication
- Testing: Write comprehensive tests for API integration
- Environment Configuration: Use different API configurations for different environments
Summary
Working with APIs in React involves several key concepts and patterns:
- Basic fetch operations for simple CRUD functionality
- Custom hooks for reusable API logic
- Error handling and retry mechanisms for robust applications
- Authentication and security considerations
- Performance optimization through caching and request management
- Testing strategies for reliable API integration
- Advanced libraries like Axios, React Query, and Apollo Client for enhanced functionality
The key to successful API integration is building reusable, maintainable, and robust solutions that handle edge cases gracefully while providing excellent user experience. Start with simple implementations and gradually add complexity based on your application’s needs.