API Integration
Fetching data from APIs with fetch and React Query
Fetching Data in React
Most React applications need to fetch data from APIs. You can use the built-in fetch API, axios, or specialized libraries like React Query that handle caching, loading states, and more.
Basic Fetch with useEffect
import { useState, useEffect } from 'react';
function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch('https://api.example.com/users')
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
setUsers(data);
setLoading(false);
})
.catch(error => {
setError(error.message);
setLoading(false);
});
}, []);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
Async/Await Pattern
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchUser = async () => {
try {
setLoading(true);
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error('User not found');
}
const data = await response.json();
setUser(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchUser();
}, [userId]);
if (loading) return <Spinner />;
if (error) return <Error message={error} />;
if (!user) return null;
return <div>{user.name}</div>;
}
Custom useFetch Hook
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let isMounted = true; // Prevent state update on unmounted component
const fetchData = async () => {
try {
setLoading(true);
const response = await fetch(url);
const json = await response.json();
if (isMounted) {
setData(json);
setLoading(false);
}
} catch (err) {
if (isMounted) {
setError(err.message);
setLoading(false);
}
}
};
fetchData();
return () => {
isMounted = false;
};
}, [url]);
return { data, loading, error };
}
// Usage
function Products() {
const { data: products, loading, error } = useFetch('/api/products');
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return (
<ul>
{products.map(p => <li key={p.id}>{p.name}</li>)}
</ul>
);
}
POST, PUT, DELETE Requests
function CreateUser() {
const [name, setName] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
setIsSubmitting(true);
try {
const response = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ name }),
});
if (!response.ok) {
throw new Error('Failed to create user');
}
const newUser = await response.json();
console.log('Created:', newUser);
setName('');
} catch (error) {
console.error(error);
} finally {
setIsSubmitting(false);
}
};
return (
<form onSubmit={handleSubmit}>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Name"
/>
<button disabled={isSubmitting}>
{isSubmitting ? 'Creating...' : 'Create User'}
</button>
</form>
);
}
// Update and Delete
const updateUser = async (id, data) => {
const response = await fetch(`/api/users/${id}`, {
method: 'PUT', // or 'PATCH' for partial updates
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
return response.json();
};
const deleteUser = async (id) => {
await fetch(`/api/users/${id}`, {
method: 'DELETE',
});
};
TanStack Query (React Query)
The best solution for server state management:
// npm install @tanstack/react-query
import { QueryClient, QueryClientProvider, useQuery, useMutation } from '@tanstack/react-query';
// Setup
const queryClient = new QueryClient();
function App() {
return (
<QueryClientProvider client={queryClient}>
<UserList />
</QueryClientProvider>
);
}
// Fetching data
function UserList() {
const { data: users, isLoading, error } = useQuery({
queryKey: ['users'],
queryFn: () => fetch('/api/users').then(res => res.json()),
});
if (isLoading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<ul>
{users.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
);
}
// With parameters
function UserProfile({ userId }) {
const { data: user } = useQuery({
queryKey: ['users', userId],
queryFn: () => fetch(`/api/users/${userId}`).then(res => res.json()),
enabled: !!userId, // Only fetch if userId exists
});
return <div>{user?.name}</div>;
}
Mutations with React Query
function CreateUser() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (newUser) => {
return fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newUser),
}).then(res => res.json());
},
onSuccess: () => {
// Invalidate and refetch users list
queryClient.invalidateQueries({ queryKey: ['users'] });
},
});
const handleSubmit = (e) => {
e.preventDefault();
mutation.mutate({ name: 'New User' });
};
return (
<form onSubmit={handleSubmit}>
<button disabled={mutation.isPending}>
{mutation.isPending ? 'Creating...' : 'Create User'}
</button>
{mutation.isError && <p>Error: {mutation.error.message}</p>}
{mutation.isSuccess && <p>User created!</p>}
</form>
);
}
Error Handling Patterns
// API wrapper with error handling
async function apiRequest(url, options = {}) {
const response = await fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
...options.headers,
},
});
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(error.message || 'Something went wrong');
}
return response.json();
}
// Usage
const users = await apiRequest('/api/users');
const newUser = await apiRequest('/api/users', {
method: 'POST',
body: JSON.stringify({ name: 'John' }),
});
// Error boundary for API errors
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div>
<h2>Something went wrong</h2>
<p>{error.message}</p>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
Loading and Error States
function DataDisplay() {
const { data, loading, error } = useFetch('/api/data');
// Skeleton loading
if (loading) {
return (
<div className="animate-pulse">
<div className="h-4 bg-gray-200 rounded w-3/4 mb-4"></div>
<div className="h-4 bg-gray-200 rounded w-1/2"></div>
</div>
);
}
// Error with retry
if (error) {
return (
<div className="text-red-500">
<p>Failed to load data</p>
<button onClick={() => window.location.reload()}>
Retry
</button>
</div>
);
}
return <div>{JSON.stringify(data)}</div>;
}
🎯 API Integration Best Practices
- ✓ Use React Query or SWR for production apps
- ✓ Always handle loading and error states
- ✓ Create reusable API functions/hooks
- ✓ Handle race conditions in useEffect
- ✓ Use AbortController to cancel requests
- ✓ Store API base URL in environment variables
- ✓ Implement proper error messages for users
- ✓ Add retry logic for failed requests