Chapter 14: Fetching Data & APIs
Up until now, we’ve been using fake or static data. Today we’ll learn how to actually fetch data from APIs, handle loading spinners, show errors gracefully, and even get a taste of the modern way with TanStack Query (formerly React Query).
We’ll go very slowly and clearly, like I’m sitting next to you in Mumbai with our laptops open, chai on the table, and I’m explaining everything live with complete, copy-paste-ready examples.
1. Fetch API vs Axios – Which One to Use?
| Feature | Fetch API (built-in) | Axios (library) |
|---|---|---|
| Included in browser | Yes – no extra install | No – need to install (npm install axios) |
| Promise-based | Yes | Yes |
| Automatic JSON parsing | No – you must do response.json() | Yes – automatic |
| Error handling | Only rejects on network error (not on 404/500) | Rejects on any non-2xx status too (easier) |
| Cancel requests | Yes (with AbortController) | Yes (with CancelToken) |
| Interceptors | No | Yes (great for auth tokens, logging) |
| Progress tracking | Yes (via ReadableStream) | Yes (onUploadProgress, onDownloadProgress) |
| Community & popularity | Built-in – always available | Extremely popular in React world |
2026 Recommendation:
- Small/simple projects or learning → use Fetch API (no extra dependencies)
- Medium to large projects → use Axios (or better → TanStack Query which we’ll see soon)
- Modern best practice → TanStack Query (handles caching, loading, errors, retries, background updates…)
2. Basic Fetching with Fetch API + Loading/Error States
Let’s build a real user list from JSONPlaceholder API.
Create src/components/UserList.tsx
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 |
import { useState, useEffect } from 'react'; interface User { id: number; name: string; email: string; company: { name: string }; } function UserList() { const [users, setUsers] = useState<User[]>([]); const [loading, setLoading] = useState(true); const [error, setError] = useState<string | null>(null); useEffect(() => { const fetchUsers = async () => { try { setLoading(true); setError(null); const response = await fetch('https://jsonplaceholder.typicode.com/users'); if (!response.ok) { throw new Error(`HTTP error! Status: {response.status}`); } const data = await response.json(); setUsers(data); } catch (err) { setError(err instanceof Error ? err.message : 'Something went wrong'); } finally { setLoading(false); } }; fetchUsers(); }, []); // empty array → run only once on mount if (loading) { return ( <div style={{ padding: '60px', textAlign: 'center', fontSize: '24px' }}> Loading users... ⏳ </div> ); } if (error) { return ( <div style={{ padding: '60px', color: '#ff6b6b', textAlign: 'center', fontSize: '20px' }}> Error: {error} 😔 <br /> <button onClick={() => window.location.reload()} style={{ marginTop: '20px', padding: '12px 24px', background: '#646cff', color: 'white', border: 'none', borderRadius: '8px' }} > Try Again </button> </div> ); } return ( <div style={{ padding: '40px', maxWidth: '900px', margin: '0 auto' }}> <h2 style={{ color: '#646cff', textAlign: 'center', marginBottom: '30px' }}> Users List (Fetched from API) </h2> <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))', gap: '20px' }}> {users.map(user => ( <div key={user.id} style={{ border: '1px solid #ddd', borderRadius: '12px', padding: '20px', background: '#f8f9ff', boxShadow: '0 4px 12px rgba(0,0,0,0.1)' }} > <h3 style={{ margin: '0 0 10px 0', color: '#646cff' }}>{user.name}</h3> <p><strong>Email:</strong> {user.email}</p> <p><strong>Company:</strong> {user.company.name}</p> </div> ))} </div> </div> ); } export default UserList; |
Add to App.tsx:
|
0 1 2 3 4 5 6 |
<Route path="/users" element={<UserList />} /> |
3. Fetching with Axios (Cleaner Syntax)
Install Axios:
|
0 1 2 3 4 5 6 |
npm install axios |
Same example with Axios:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import axios from 'axios'; // Inside useEffect: const fetchUsers = async () => { try { setLoading(true); setError(null); const response = await axios.get('https://jsonplaceholder.typicode.com/users'); setUsers(response.data); } catch (err) { setError(axios.isAxiosError(err) ? err.message : 'Something went wrong'); } finally { setLoading(false); } }; |
Advantages:
- response.data is already JSON
- Better error handling (catches 404, 500 automatically)
4. Introduction to TanStack Query (React Query) – The Modern Way
TanStack Query is the best way to fetch data in 2026 React apps. It handles:
- Loading states
- Error states
- Caching
- Automatic retries
- Background refetching
- Pagination, infinite scroll, mutations…
Install:
|
0 1 2 3 4 5 6 |
npm install @tanstack/react-query |
Wrap your app in QueryClientProvider (in main.tsx):
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; const queryClient = new QueryClient(); ReactDOM.createRoot(document.getElementById('root')!).render( <React.StrictMode> <QueryClientProvider client={queryClient}> <BrowserRouter> <App /> </BrowserRouter> </QueryClientProvider> </React.StrictMode>, ) |
Now create src/components/UserListQuery.tsx
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
import { useQuery } from '@tanstack/react-query'; import axios from 'axios'; interface User { id: number; name: string; email: string; company: { name: string }; } function UserListQuery() { const { data, isLoading, error, isError } = useQuery<User[]>({ queryKey: ['users'], // unique key for caching queryFn: async () => { const { data } = await axios.get('https://jsonplaceholder.typicode.com/users'); return data; }, staleTime: 5 * 60 * 1000, // 5 minutes cache }); if (isLoading) { return <div style={{ padding: '60px', textAlign: 'center', fontSize: '24px' }}>Loading users... ⏳</div>; } if (isError) { return ( <div style={{ padding: '60px', color: '#ff6b6b', textAlign: 'center' }}> Error: {error?.message || 'Something went wrong'} 😔 </div> ); } return ( <div style={{ padding: '40px', maxWidth: '900px', margin: '0 auto' }}> <h2 style={{ color: '#646cff', textAlign: 'center' }}>Users (with TanStack Query)</h2> <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))', gap: '20px' }}> {data?.map(user => ( <div key={user.id} style={{ border: '1px solid #ddd', borderRadius: '12px', padding: '20px', background: '#f8f9ff' }}> <h3 style={{ color: '#646cff' }}>{user.name}</h3> <p><strong>Email:</strong> {user.email}</p> <p><strong>Company:</strong> {user.company.name}</p> </div> ))} </div> </div> ); } export default UserListQuery; |
Why TanStack Query wins:
- No manual useState for loading/error/data
- Automatic caching (data stays fresh)
- Refetch on window focus, network reconnect
- Mutations (POST/PUT/DELETE) are easy
- Devtools for debugging
Summary – Chapter 14 Key Takeaways
- Fetch API → built-in, good for learning
- Axios → cleaner syntax, better errors
- Always handle loading, error, success states
- Use useEffect for basic fetching
- TanStack Query = modern gold standard (caching, retries, background updates)
- Never forget error boundaries in real apps
Mini Homework
- Create a /posts page that fetches posts from https://jsonplaceholder.typicode.com/posts
- Use TanStack Query + show loading skeleton
- Bonus: Click a post title → navigate to /posts/:id and fetch single post
