React.js Fundamentals

React and API Integration

18 min Lesson 31 of 40

React and API Integration

Learn how to integrate REST APIs into your React applications, handle asynchronous data fetching, manage loading and error states, and implement advanced features like pagination and infinite scroll.

Fetching Data with Fetch API

The Fetch API is the modern way to make HTTP requests in JavaScript. Let's start with a basic example:

import { useState, useEffect } from 'react'; function UserList() { const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { fetch('https://jsonplaceholder.typicode.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 <div>Loading...</div>; if (error) return <div>Error: {error}</div>; return ( <ul> {users.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> ); }
Note: Always handle loading and error states when fetching data. This provides better user experience and helps debug issues.

Using Async/Await with Fetch

Async/await syntax makes asynchronous code more readable. Here's how to refactor the previous example:

import { useState, useEffect } from 'react'; function UserList() { const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { const fetchUsers = async () => { try { 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 (error) { setError(error.message); } finally { setLoading(false); } }; fetchUsers(); }, []); if (loading) { return <div className="spinner">Loading...</div>; } if (error) { return <div className="error">Error: {error}</div>; } return ( <div className="user-list"> {users.map(user => ( <div key={user.id} className="user-card"> <h3>{user.name}</h3> <p>{user.email}</p> </div> ))} </div> ); }
Tip: Use a finally block to ensure loading state is set to false regardless of success or failure.

Creating a Custom Hook for Data Fetching

Extract data fetching logic into a reusable custom hook:

import { useState, useEffect } from 'react'; function useFetch(url) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { const fetchData = async () => { try { setLoading(true); const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const result = await response.json(); setData(result); setError(null); } catch (error) { setError(error.message); setData(null); } finally { setLoading(false); } }; fetchData(); }, [url]); return { data, loading, error }; } // Usage function Users() { const { data: users, loading, error } = useFetch( 'https://jsonplaceholder.typicode.com/users' ); if (loading) return <div>Loading...</div>; if (error) return <div>Error: {error}</div>; return ( <ul> {users.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> ); }

CRUD Operations with REST APIs

Implement Create, Read, Update, and Delete operations:

import { useState, useEffect } from 'react'; function PostManager() { const [posts, setPosts] = useState([]); const [newPost, setNewPost] = useState({ title: '', body: '' }); const [loading, setLoading] = useState(false); // READ - Fetch all posts useEffect(() => { fetchPosts(); }, []); const fetchPosts = async () => { try { const response = await fetch('https://jsonplaceholder.typicode.com/posts'); const data = await response.json(); setPosts(data.slice(0, 10)); // Limit to 10 posts } catch (error) { console.error('Error fetching posts:', error); } }; // CREATE - Add new post const createPost = async (e) => { e.preventDefault(); setLoading(true); try { const response = await fetch('https://jsonplaceholder.typicode.com/posts', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(newPost), }); const data = await response.json(); setPosts([data, ...posts]); setNewPost({ title: '', body: '' }); } catch (error) { console.error('Error creating post:', error); } finally { setLoading(false); } }; // UPDATE - Edit existing post const updatePost = async (id, updatedData) => { try { const response = await fetch( `https://jsonplaceholder.typicode.com/posts/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(updatedData), } ); const data = await response.json(); setPosts(posts.map(post => (post.id === id ? data : post))); } catch (error) { console.error('Error updating post:', error); } }; // DELETE - Remove post const deletePost = async (id) => { try { await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`, { method: 'DELETE', }); setPosts(posts.filter(post => post.id !== id)); } catch (error) { console.error('Error deleting post:', error); } }; return ( <div> <form onSubmit={createPost}> <input type="text" placeholder="Title" value={newPost.title} onChange={(e) => setNewPost({ ...newPost, title: e.target.value })} required /> <textarea placeholder="Body" value={newPost.body} onChange={(e) => setNewPost({ ...newPost, body: e.target.value })} required /> <button type="submit" disabled={loading}> {loading ? 'Creating...' : 'Create Post'} </button> </form> <div className="posts"> {posts.map(post => ( <div key={post.id} className="post-card"> <h3>{post.title}</h3> <p>{post.body}</p> <button onClick={() => deletePost(post.id)}>Delete</button> </div> ))} </div> </div> ); }
Warning: Always validate user input before sending it to the API. Consider implementing proper error handling and user feedback for all CRUD operations.

Implementing Pagination

Handle large datasets with pagination:

import { useState, useEffect } from 'react'; function PaginatedPosts() { const [posts, setPosts] = useState([]); const [currentPage, setCurrentPage] = useState(1); const [totalPages, setTotalPages] = useState(0); const [loading, setLoading] = useState(false); const postsPerPage = 10; useEffect(() => { fetchPosts(currentPage); }, [currentPage]); const fetchPosts = async (page) => { setLoading(true); try { const response = await fetch( `https://jsonplaceholder.typicode.com/posts?_page=${page}&_limit=${postsPerPage}` ); const data = await response.json(); const total = response.headers.get('X-Total-Count'); setPosts(data); setTotalPages(Math.ceil(total / postsPerPage)); } catch (error) { console.error('Error fetching posts:', error); } finally { setLoading(false); } }; const goToPage = (page) => { if (page >= 1 && page <= totalPages) { setCurrentPage(page); } }; return ( <div> {loading ? ( <div>Loading...</div> ) : ( <> <div className="posts"> {posts.map(post => ( <div key={post.id} className="post-card"> <h3>{post.title}</h3> <p>{post.body}</p> </div> ))} </div> <div className="pagination"> <button onClick={() => goToPage(currentPage - 1)} disabled={currentPage === 1} > Previous </button> <span> Page {currentPage} of {totalPages} </span> <button onClick={() => goToPage(currentPage + 1)} disabled={currentPage === totalPages} > Next </button> </div> </> )} </div> ); }

Infinite Scroll Implementation

Load more data as the user scrolls down:

import { useState, useEffect, useRef, useCallback } from 'react'; function InfiniteScrollPosts() { const [posts, setPosts] = useState([]); const [page, setPage] = useState(1); const [loading, setLoading] = useState(false); const [hasMore, setHasMore] = useState(true); const observer = useRef(); const lastPostRef = useCallback((node) => { if (loading) return; if (observer.current) observer.current.disconnect(); observer.current = new IntersectionObserver((entries) => { if (entries[0].isIntersecting && hasMore) { setPage(prevPage => prevPage + 1); } }); if (node) observer.current.observe(node); }, [loading, hasMore]); useEffect(() => { const fetchPosts = async () => { setLoading(true); try { const response = await fetch( `https://jsonplaceholder.typicode.com/posts?_page=${page}&_limit=10` ); const data = await response.json(); setPosts(prevPosts => [...prevPosts, ...data]); setHasMore(data.length > 0); } catch (error) { console.error('Error fetching posts:', error); } finally { setLoading(false); } }; fetchPosts(); }, [page]); return ( <div> <div className="posts"> {posts.map((post, index) => { if (posts.length === index + 1) { return ( <div ref={lastPostRef} key={post.id} className="post-card"> <h3>{post.title}</h3> <p>{post.body}</p> </div> ); } else { return ( <div key={post.id} className="post-card"> <h3>{post.title}</h3> <p>{post.body}</p> </div> ); } })} </div> {loading && <div className="loading">Loading more posts...</div>} {!hasMore && <div className="end">No more posts to load</div>} </div> ); }
Tip: The Intersection Observer API is more performant than scroll event listeners for infinite scroll implementations.
Exercise 1: Create a product listing component that fetches data from an API, implements search functionality, and displays loading states. Include error handling and retry logic.
Exercise 2: Build a comment system with CRUD operations. Users should be able to add, edit, and delete comments. Implement optimistic UI updates for better user experience.
Exercise 3: Create an infinite scroll image gallery that loads images from the Unsplash API. Implement a loading skeleton and handle rate limiting errors gracefully.