API Integration and Data Fetching

Connect your React app to backend services and handle asynchronous operations.

Understanding APIs and React: Connecting to the Outside World

Every modern web application needs to communicate with servers to get data, save information, or interact with other services. In React, we do this through APIs (Application Programming Interfaces). Think of an API as a waiter in a restaurant - you tell the waiter what you want, they go to the kitchen (server), and bring back your food (data).

What is an API? An API is like a menu at a restaurant:

  • It tells you what's available (endpoints)
  • How to order it (request methods)
  • What you'll get back (response format)
  • Any special requirements (authentication, headers)

Why Do We Need APIs?

  • Get fresh data: User profiles, product listings, news articles
  • Save information: Form submissions, user preferences, file uploads
  • Real-time updates: Chat messages, notifications, live scores
  • Third-party services: Maps, payment processing, social media

The Journey of Data

  1. Your React app needs some data (like a list of products)
  2. It makes a request to an API endpoint (like ordering from a menu)
  3. The server processes the request (kitchen prepares the order)
  4. Server sends back the data (waiter brings your food)
  5. React displays the data to the user (you enjoy your meal)

Common API Operations (CRUD)

  • Create (POST): Add new data (create a new user account)
  • Read (GET): Fetch existing data (get list of products)
  • Update (PUT/PATCH): Modify existing data (update user profile)
  • Delete (DELETE): Remove data (delete a comment)

What Makes API Calls Special in React?

  • They're asynchronous (don't block the UI)
  • They need proper state management
  • They require loading indicators
  • They need error handling
  • They often happen in useEffect
1// 🌟 YOUR FIRST API CALL IN REACT
2
3import React, { useState, useEffect } from 'react';
4
5// Basic API call example
6function MyFirstAPICall() {
7 const [data, setData] = useState(null);
8 const [loading, setLoading] = useState(false);
9 const [error, setError] = useState(null);
10
11 const fetchData = async () => {
12 setLoading(true);
13 setError(null);
14
15 try {
16 const response = await fetch('https://jsonplaceholder.typicode.com/posts/1');
17
18 if (!response.ok) {
19 throw new Error('Failed to fetch data');
20 }
21
22 const json = await response.json();
23 setData(json);
24 } catch (err) {
25 setError(err.message);
26 } finally {
27 setLoading(false);
28 }
29 };
30
31 return (
32 <div>
33 <button onClick={fetchData} disabled={loading}>
34 {loading ? 'Loading...' : 'Fetch Data'}
35 </button>
36
37 {error && <p style={{ color: 'red' }}>Error: {error}</p>}
38
39 {data && (
40 <div>
41 <h3>{data.title}</h3>
42 <p>{data.body}</p>
43 </div>
44 )}
45 </div>
46 );
47}
48
49// Using useEffect to fetch on mount
50function AutoFetchExample() {
51 const [users, setUsers] = useState([]);
52 const [loading, setLoading] = useState(true);
53
54 useEffect(() => {
55 // Fetch data when component mounts
56 fetch('https://jsonplaceholder.typicode.com/users')
57 .then(res => res.json())
58 .then(data => {
59 setUsers(data.slice(0, 5)); // Get first 5 users
60 setLoading(false);
61 })
62 .catch(err => {
63 console.error('Error:', err);
64 setLoading(false);
65 });
66 }, []); // Empty dependency array = run once on mount
67
68 if (loading) return <div>Loading users...</div>;
69
70 return (
71 <ul>
72 {users.map(user => (
73 <li key={user.id}>{user.name} - {user.email}</li>
74 ))}
75 </ul>
76 );
77}
78
79// Async/await vs Promises comparison
80// Promise approach:
81fetch(url)
82 .then(response => response.json())
83 .then(data => console.log(data))
84 .catch(error => console.error(error));
85
86// Async/await approach (cleaner):
87async function fetchData() {
88 try {
89 const response = await fetch(url);
90 const data = await response.json();
91 console.log(data);
92 } catch (error) {
93 console.error(error);
94 }
95}

Fetch API Basics: Making HTTP Requests

The Fetch API is the modern way to make HTTP requests in JavaScript. React applications commonly use it to communicate with backend APIs for data retrieval and submission.

Key Concepts:

  • Promises: Fetch returns promises for asynchronous operations
  • Response handling: Check status codes and parse response data
  • Error handling: Handle network errors and API errors
  • Request configuration: Set headers, methods, and body data

The Anatomy of a Fetch Request

fetch(url, options)
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error(error));

Understanding HTTP Methods

  • GET: Retrieve data (like viewing a webpage)
  • POST: Create new data (like submitting a form)
  • PUT: Update entire resource (like editing a profile)
  • PATCH: Update part of resource (like changing just email)
  • DELETE: Remove data (like deleting a comment)

Response Status Codes

  • 200-299: Success! Everything worked
  • 300-399: Redirection - resource moved
  • 400-499: Client error - you did something wrong
  • 500-599: Server error - server had a problem

Headers: Extra Information Headers are like metadata for your request:

  • Content-Type: What format is the data? (JSON, form data, etc.)
  • Authorization: Who are you? (API keys, tokens)
  • Accept: What format do you want back?
1import { useState, useEffect } from 'react';
2
3// 🌟 FETCH API EXAMPLES
4
5// GET Request
6function GetExample() {
7 const [users, setUsers] = useState([]);
8 const [loading, setLoading] = useState(true);
9
10 useEffect(() => {
11 fetch('https://jsonplaceholder.typicode.com/users')
12 .then(response => {
13 if (!response.ok) {
14 throw new Error(`HTTP error! status: ${response.status}`);
15 }
16 return response.json();
17 })
18 .then(data => setUsers(data))
19 .catch(error => console.error('Error:', error))
20 .finally(() => setLoading(false));
21 }, []);
22
23 if (loading) return <div>Loading...</div>;
24
25 return (
26 <ul>
27 {users.map(user => (
28 <li key={user.id}>{user.name}</li>
29 ))}
30 </ul>
31 );
32}
33
34// POST Request
35async function createPost(title, body) {
36 const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
37 method: 'POST',
38 headers: {
39 'Content-Type': 'application/json',
40 },
41 body: JSON.stringify({
42 title,
43 body,
44 userId: 1
45 })
46 });
47
48 if (!response.ok) {
49 throw new Error('Failed to create post');
50 }
51
52 return response.json();
53}
54
55// PUT Request (Update)
56async function updatePost(id, updates) {
57 const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`, {
58 method: 'PUT',
59 headers: {
60 'Content-Type': 'application/json',
61 },
62 body: JSON.stringify(updates)
63 });
64
65 return response.json();
66}
67
68// DELETE Request
69async function deletePost(id) {
70 const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`, {
71 method: 'DELETE'
72 });
73
74 if (!response.ok) {
75 throw new Error('Failed to delete');
76 }
77}
78
79// Working with Headers
80async function fetchWithAuth(token) {
81 const response = await fetch('https://api.example.com/protected', {
82 headers: {
83 'Authorization': `Bearer ${token}`,
84 'Accept': 'application/json'
85 }
86 });
87
88 return response.json();
89}
90
91// Query Parameters
92function buildUrl(base, params) {
93 const url = new URL(base);
94 Object.keys(params).forEach(key =>
95 url.searchParams.append(key, params[key])
96 );
97 return url.toString();
98}
99
100// Usage:
101const url = buildUrl('https://api.example.com/users', {
102 page: 1,
103 limit: 10,
104 sort: 'name'
105});
106// Result: https://api.example.com/users?page=1&limit=10&sort=name
107
108// Abort Controller (Cancel requests)
109function CancelableRequest() {
110 const [data, setData] = useState(null);
111
112 useEffect(() => {
113 const controller = new AbortController();
114
115 fetch('https://api.example.com/data', {
116 signal: controller.signal
117 })
118 .then(res => res.json())
119 .then(data => setData(data))
120 .catch(err => {
121 if (err.name === 'AbortError') {
122 console.log('Request was cancelled');
123 }
124 });
125
126 // Cleanup: cancel request if component unmounts
127 return () => controller.abort();
128 }, []);
129
130 return <div>{data ? JSON.stringify(data) : 'Loading...'}</div>;
131}

Loading States and Error Handling: Creating Great User Experience

Proper loading states and error handling create a better user experience by providing feedback during API operations and gracefully handling failures.

Why Loading States Matter Users need to know something is happening! Without loading states:

  • Users might click buttons multiple times
  • They might think the app is broken
  • They might leave your site
  • It looks unprofessional

Types of Loading States

  1. Initial load: First time loading data
  2. Refresh: Updating existing data
  3. Action feedback: Response to user actions
  4. Skeleton screens: Show UI structure while loading
  5. Progressive loading: Load critical data first

Error Handling Best Practices

  • Always expect things to go wrong
  • Provide clear error messages
  • Offer ways to recover (retry buttons)
  • Log errors for debugging
  • Don't expose sensitive information

Common Error Scenarios

  • Network failures (no internet)
  • Server errors (500 errors)
  • Invalid data (400 errors)
  • Timeout errors
  • Permission errors (401, 403)
1import { useState, useEffect } from 'react';
2
3// 🌟 LOADING STATES AND ERROR HANDLING
4
5// Custom hook for API calls
6function useApi(url) {
7 const [data, setData] = useState(null);
8 const [loading, setLoading] = useState(true);
9 const [error, setError] = useState(null);
10
11 useEffect(() => {
12 const fetchData = async () => {
13 try {
14 setLoading(true);
15 const response = await fetch(url);
16
17 if (!response.ok) {
18 throw new Error(`HTTP error! status: ${response.status}`);
19 }
20
21 const json = await response.json();
22 setData(json);
23 } catch (err) {
24 setError(err.message);
25 } finally {
26 setLoading(false);
27 }
28 };
29
30 fetchData();
31 }, [url]);
32
33 return { data, loading, error };
34}
35
36// Example component using the hook
37function UserProfile({ userId }) {
38 const { data: user, loading, error } = useApi(
39 `https://jsonplaceholder.typicode.com/users/${userId}`
40 );
41
42 if (loading) {
43 return <div>Loading user data...</div>;
44 }
45
46 if (error) {
47 return (
48 <div style={{ color: 'red' }}>
49 Error loading user: {error}
50 </div>
51 );
52 }
53
54 return (
55 <div>
56 <h2>{user.name}</h2>
57 <p>Email: {user.email}</p>
58 <p>Phone: {user.phone}</p>
59 </div>
60 );
61}
62
63// Loading spinner component
64function LoadingSpinner() {
65 return (
66 <div className="spinner">
67 <div className="spinner-circle"></div>
68 <style jsx>{`
69 .spinner {
70 display: flex;
71 justify-content: center;
72 padding: 20px;
73 }
74 .spinner-circle {
75 width: 40px;
76 height: 40px;
77 border: 4px solid #f3f3f3;
78 border-top: 4px solid #3498db;
79 border-radius: 50%;
80 animation: spin 1s linear infinite;
81 }
82 @keyframes spin {
83 0% { transform: rotate(0deg); }
84 100% { transform: rotate(360deg); }
85 }
86 `}</style>
87 </div>
88 );
89}
90
91// Error handling patterns
92function ErrorHandling() {
93 const [data, setData] = useState(null);
94 const [error, setError] = useState(null);
95 const [retryCount, setRetryCount] = useState(0);
96
97 const fetchData = async () => {
98 try {
99 setError(null);
100 const response = await fetch('/api/data');
101
102 if (!response.ok) {
103 // Handle different error codes
104 if (response.status === 404) {
105 throw new Error('Data not found');
106 } else if (response.status === 401) {
107 throw new Error('Please login to continue');
108 } else if (response.status >= 500) {
109 throw new Error('Server error. Please try again later.');
110 } else {
111 throw new Error('Something went wrong');
112 }
113 }
114
115 const data = await response.json();
116 setData(data);
117 } catch (err) {
118 setError(err.message);
119 // Log to error tracking service
120 console.error('API Error:', err);
121 }
122 };
123
124 const retry = () => {
125 setRetryCount(count => count + 1);
126 fetchData();
127 };
128
129 if (error) {
130 return (
131 <div className="error-container">
132 <h3>⚠️ Error</h3>
133 <p>{error}</p>
134 <button onClick={retry}>
135 Try Again ({retryCount})
136 </button>
137 </div>
138 );
139 }
140
141 return <div>{/* Your content */}</div>;
142}
143
144// Different loading states
145function LoadingStates() {
146 const [loadingType, setLoadingType] = useState('spinner');
147
148 const loadingComponents = {
149 spinner: <LoadingSpinner />,
150 skeleton: (
151 <div className="skeleton">
152 <div className="skeleton-line"></div>
153 <div className="skeleton-line short"></div>
154 <style jsx>{`
155 .skeleton-line {
156 height: 20px;
157 background: #e0e0e0;
158 margin: 10px 0;
159 border-radius: 4px;
160 animation: pulse 1.5s ease-in-out infinite;
161 }
162 .skeleton-line.short {
163 width: 60%;
164 }
165 @keyframes pulse {
166 0% { opacity: 0.6; }
167 50% { opacity: 1; }
168 100% { opacity: 0.6; }
169 }
170 `}</style>
171 </div>
172 ),
173 message: <p>Loading, please wait...</p>
174 };
175
176 return loadingComponents[loadingType];
177}

Data Caching and Optimization: Making Your App Fast

Data caching improves performance by storing frequently accessed data and reducing unnecessary API calls. This is especially important for data that doesn't change often.

What is Caching? Caching is like keeping a notebook of answers to common questions. Instead of asking the same question repeatedly, you check your notebook first. If the answer is there and still fresh, you use it. Otherwise, you ask again and update your notebook.

Benefits of Caching

  • Faster load times: Instant data from memory
  • Reduced server load: Fewer API calls
  • Better offline experience: Show cached data when offline
  • Lower costs: Less bandwidth usage
  • Improved user experience: No waiting for repeated requests

Caching Strategies

  1. Time-based (TTL): Cache expires after certain time
  2. Event-based: Clear cache on specific actions
  3. Size-based: Limit cache size, remove oldest
  4. Conditional: Cache only certain types of data

When to Cache

  • Data that rarely changes (user profiles, settings)
  • Expensive calculations
  • Frequently accessed data
  • Reference data (countries, categories)

When NOT to Cache

  • Real-time data (stock prices, live scores)
  • Sensitive data (passwords, tokens)
  • User-specific temporary data
  • Data that changes frequently
1import { useState, useEffect, useCallback, useRef } from 'react';
2
3// 🌟 SIMPLE CACHE IMPLEMENTATION
4
5class SimpleCache {
6 constructor() {
7 this.cache = new Map();
8 }
9
10 set(key, value, ttl = 5 * 60 * 1000) { // Default 5 minutes
11 this.cache.set(key, {
12 value,
13 expiry: Date.now() + ttl
14 });
15 }
16
17 get(key) {
18 const item = this.cache.get(key);
19 if (!item) return null;
20
21 // Check if expired
22 if (Date.now() > item.expiry) {
23 this.cache.delete(key);
24 return null;
25 }
26
27 return item.value;
28 }
29
30 clear() {
31 this.cache.clear();
32 }
33}
34
35const apiCache = new SimpleCache();
36
37// 🚀 CUSTOM CACHING HOOK
38
39function useCachedApi(url, options = {}) {
40 const [data, setData] = useState(null);
41 const [loading, setLoading] = useState(true);
42 const [error, setError] = useState(null);
43 const [isFromCache, setIsFromCache] = useState(false);
44
45 const {
46 cacheKey = url,
47 cacheTTL = 5 * 60 * 1000, // 5 minutes default
48 skipCache = false
49 } = options;
50
51 const fetchData = useCallback(async (forceRefresh = false) => {
52 try {
53 // Check cache first
54 if (!forceRefresh && !skipCache) {
55 const cachedData = apiCache.get(cacheKey);
56 if (cachedData) {
57 setData(cachedData);
58 setIsFromCache(true);
59 setLoading(false);
60 return cachedData;
61 }
62 }
63
64 setLoading(true);
65 setError(null);
66 setIsFromCache(false);
67
68 const response = await fetch(url);
69
70 if (!response.ok) {
71 throw new Error(`HTTP error! status: ${response.status}`);
72 }
73
74 const jsonData = await response.json();
75
76 // Cache the successful response
77 if (!skipCache) {
78 apiCache.set(cacheKey, jsonData, cacheTTL);
79 }
80
81 setData(jsonData);
82 return jsonData;
83 } catch (err) {
84 setError(err);
85
86 // Try to use stale cache data on error
87 const staleData = apiCache.get(cacheKey);
88 if (staleData) {
89 setData(staleData);
90 setIsFromCache(true);
91 }
92 } finally {
93 setLoading(false);
94 }
95 }, [url, cacheKey, cacheTTL, skipCache]);
96
97 useEffect(() => {
98 fetchData();
99 }, [fetchData]);
100
101 const refresh = () => fetchData(true);
102
103 return { data, loading, error, isFromCache, refresh };
104}
105
106// 🛍️ EXAMPLE: PRODUCT LIST WITH CACHING
107
108function ProductList() {
109 const [selectedCategory, setSelectedCategory] = useState('all');
110
111 const url = selectedCategory === 'all'
112 ? 'https://fakestoreapi.com/products?limit=6'
113 : `https://fakestoreapi.com/products/category/${selectedCategory}?limit=6`;
114
115 const {
116 data: products,
117 loading,
118 error,
119 isFromCache,
120 refresh
121 } = useCachedApi(url, {
122 cacheKey: `products-${selectedCategory}`,
123 cacheTTL: 10 * 60 * 1000 // Cache for 10 minutes
124 });
125
126 return (
127 <div>
128 <h2>Product Catalog</h2>
129
130 {/* Cache indicator */}
131 <div style={{
132 padding: '10px',
133 marginBottom: '20px',
134 backgroundColor: isFromCache ? '#d4edda' : '#cce5ff',
135 borderRadius: '5px'
136 }}>
137 {isFromCache ? '📦 Cached data' : '🌐 Fresh data'}
138 </div>
139
140 {/* Category filter */}
141 <select
142 value={selectedCategory}
143 onChange={(e) => setSelectedCategory(e.target.value)}
144 >
145 <option value="all">All Products</option>
146 <option value="electronics">Electronics</option>
147 <option value="jewelery">Jewelery</option>
148 </select>
149
150 <button onClick={refresh} style={{ marginLeft: '10px' }}>
151 🔄 Refresh
152 </button>
153
154 {loading && !products && <div>Loading...</div>}
155 {error && <div style={{ color: 'red' }}>Error: {error.message}</div>}
156
157 <div style={{
158 display: 'grid',
159 gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))',
160 gap: '20px',
161 marginTop: '20px'
162 }}>
163 {products?.map(product => (
164 <div key={product.id} style={{
165 border: '1px solid #ddd',
166 padding: '10px',
167 borderRadius: '8px'
168 }}>
169 <h4>{product.title.substring(0, 50)}...</h4>
170 <p style={{ fontWeight: 'bold', color: '#28a745' }}>
171 ${product.price}
172 </p>
173 </div>
174 ))}
175 </div>
176 </div>
177 );
178}
179
180// 🚀 OPTIMISTIC UPDATES EXAMPLE
181
182function TodoListOptimistic() {
183 const [todos, setTodos] = useState([
184 { id: 1, title: 'Learn React', completed: false },
185 { id: 2, title: 'Build an app', completed: false }
186 ]);
187
188 const toggleTodo = async (id) => {
189 // Optimistically update UI
190 setTodos(prev => prev.map(todo =>
191 todo.id === id ? { ...todo, completed: !todo.completed } : todo
192 ));
193
194 try {
195 // Simulate API call
196 await new Promise(resolve => setTimeout(resolve, 1000));
197 // In real app, you'd make actual API call here
198 } catch (error) {
199 // Revert on error
200 setTodos(prev => prev.map(todo =>
201 todo.id === id ? { ...todo, completed: !todo.completed } : todo
202 ));
203 }
204 };
205
206 return (
207 <div>
208 <h3>Todo List (Optimistic Updates)</h3>
209 <ul style={{ listStyle: 'none', padding: 0 }}>
210 {todos.map(todo => (
211 <li key={todo.id} style={{ marginBottom: '10px' }}>
212 <label style={{ cursor: 'pointer' }}>
213 <input
214 type="checkbox"
215 checked={todo.completed}
216 onChange={() => toggleTodo(todo.id)}
217 style={{ marginRight: '10px' }}
218 />
219 <span style={{
220 textDecoration: todo.completed ? 'line-through' : 'none'
221 }}>
222 {todo.title}
223 </span>
224 </label>
225 </li>
226 ))}
227 </ul>
228 </div>
229 );
230}
231
232// 💡 CACHING BEST PRACTICES:
233// 1. Set appropriate TTL (Time To Live)
234// 2. Clear cache on user actions (logout, data updates)
235// 3. Handle stale data gracefully
236// 4. Use meaningful cache keys
237// 5. Consider using React Query or SWR for production apps