Hello! Welcome to the final lesson in our module on handling side effects and API integration in React.
In our last lesson, you mastered one of React's most powerful patterns: creating a custom useFetch hook to encapsulate data-fetching logic. This allowed us to abstract away the complexities of state management (useState), side effects (useEffect), and cleanup functions, resulting in much cleaner and more readable components.
Today, you will leverage that knowledge to achieve this lesson's learning outcome: build a mini-app that fetches and filters items from an API using hooks. This project will be the capstone for this module, bringing together everything you've learned about hooks and data handling. It's a practical exercise that directly mirrors the kind of functionality you'd build for an e-commerce product catalog, a key part of your overall goal.
Let's get started!
1. The Core Tool: A Reusable useFetch Hook
While we built a useFetch hook in the last lesson, it's a concept worth reinforcing. Different developers might implement it in slightly different ways. To solidify your understanding and see another professional approach, we'll review the process of creating one.
This video demonstrates building a useFetch hook using Axios, a popular promise-based HTTP client library. It's a great alternative to the built-in fetch API and is widely used in the industry.
How to Fetch Data in React With A Custom useFetch Hook
Watch this video from PedroTech to see a complete walkthrough of creating a useFetch custom hook. Pay close attention to how he structures the hook to manage data, loading, and error states, and how he adds a refetch function.
Watch the video from 02:57 to 16:10. Focus on these key points: Initial Setup (02:57 - 04:24): Understand why a custom hook is useful and the basic structure. Core Logic (04:24 - 10:04): See how useState and useEffect are used to handle the API call with Axios, including managing loading and error states. Using the Hook (10:04 - 13:56): Observe how the hook is consumed in a component, and how the returned data is destructured and used for conditional rendering. Adding refetch (13:56 - 16:10): Learn how to expose a function from the hook to allow the component to trigger the API call again on demand.
After watching the video, here is the complete code for the useFetch hook that we will use in our project. This version uses the fetch API for consistency with our previous lessons, but the core logic is the same as what you saw in the video.
Create a file at src/hooks/useFetch.js and add the following code:
// src/hooks/useFetch.js
import { useState, useEffect, useCallback } from 'react';
const useFetch = (url) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const fetchData = useCallback(async () => {
setLoading(true);
setError(null);
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
setData(result);
} catch (e) {
setError(e);
} finally {
setLoading(false);
}
}, [url]);
useEffect(() => {
fetchData();
}, [fetchData]);
// We return the state values and the refetch function
return { data, loading, error, refetch: fetchData };
};
export default useFetch;
A quick note: We've wrapped fetchData in a useCallback hook here. This is an optimization that ensures the fetchData function itself doesn't get recreated on every render, which is good practice when it's a dependency of useEffect. You'll learn more about useCallback in a future module.
2. Building the Character Filtering App
Now for the main event. We'll build a component that uses our useFetch hook to fetch characters from the public Rick and Morty API and allows a user to filter them by name.
Step 1: Set up the Component and Fetch Data
First, create a new component file, src/components/CharacterGrid.js.
Inside this component, we'll do two things:
- Call our
useFetchhook to get the character data. - Add a
useStatehook to manage the user's search query.
// src/components/CharacterGrid.js
import React, { useState } from 'react';
import useFetch from '../hooks/useFetch';
const API_URL = 'https://rickandmortyapi.com/api/character';
const CharacterGrid = () => {
const [query, setQuery] = useState('');
const { data, loading, error } = useFetch(API_URL);
if (loading) return <p>Loading characters...</p>;
if (error) return <p>Error fetching data: {error.message}</p>;
return (
<div className="container">
<h1>Rick and Morty Characters</h1>
<input
type="text"
placeholder="Search for a character..."
className="search-input"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
{/* The character grid will go here */}
</div>
);
};
export default CharacterGrid;
At this point, we have a component that fetches data and provides a controlled input for our search query.
Step 2: Filter and Display the Data
This is where your knowledge of array methods from our first module comes back into play. To implement the filter, we need to:
- Take the original
data.resultsarray from the API. - Use the
.filter()method to create a new array containing only the characters whose names match the searchquery. - Use the
.map()method on the filtered array to render each character.
This pattern of filtering before mapping is extremely common in React.
How to Consume REST APIs in React
For a refresher on how to take fetched API data and render it in your application by looping through an array, this article from freeCodeCamp is a great reference.
Review the subsection titled 'How to Perform a GET Request in React With Fetch API'. Focus on the final code block in that section, where it shows how to use posts.map(...) to render the fetched data. This is the same principle we'll apply.
Let's add the filtering and mapping logic to our CharacterGrid component.
// src/components/CharacterGrid.js
import React, { useState } from 'react';
import useFetch from '../hooks/useFetch';
import './CharacterGrid.css'; // We'll add some simple styling
const API_URL = 'https://rickandmortyapi.com/api/character';
const CharacterGrid = () => {
const [query, setQuery] = useState('');
const { data, loading, error } = useFetch(API_URL);
// Filter the characters based on the query
const filteredCharacters = data?.results.filter((character) =>
character.name.toLowerCase().includes(query.toLowerCase())
);
if (loading) return <p>Loading characters...</p>;
if (error) return <p>Error fetching data: {error.message}</p>;
return (
<div className="container">
<h1>Rick and Morty Characters</h1>
<input
type="text"
placeholder="Search for a character..."
className="search-input"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<div className="grid">
{filteredCharacters.map((character) => (
<div className="card" key={character.id}>
<img src={character.image} alt={character.name} />
<h3>{character.name}</h3>
</div>
))}
</div>
</div>
);
};
export default CharacterGrid;
Key things to note in the code above:
- Optional Chaining (
data?.results): We usedata?.resultsbecausedatais initiallynull. The?.prevents an error by stopping the chain ifdatais nullish. - Filtering Logic: We convert both the character's name and the query to lowercase (
.toLowerCase()) to ensure the search is case-insensitive. - Mapping: We map over
filteredCharacters, not the originaldata.results. This ensures only the matching characters are rendered. - The
keyProp: As always when mapping, we provide a uniquekeyto each element, in this casecharacter.id.
Finally, to make it look decent, create a src/components/CharacterGrid.css file:
/* src/components/CharacterGrid.css */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
font-family: sans-serif;
}
.search-input {
width: 100%;
padding: 10px;
font-size: 1.2em;
margin-bottom: 20px;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 20px;
}
.card {
border: 1px solid #ccc;
border-radius: 8px;
overflow: hidden;
text-align: center;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
.card img {
width: 100%;
height: auto;
}
.card h3 {
padding: 10px;
margin: 0;
font-size: 1em;
}
Don't forget to import this CSS file into your App.js or CharacterGrid.js and render the <CharacterGrid /> component in your App.js. You now have a working mini-application!
Conclusion
Congratulations! You have successfully built a small but complete React application that fetches, displays, and filters data from a live API.
Key Takeaways:
- You combined multiple hooks (
useState,useEffectinside a custom hook) to create dynamic and interactive functionality. - You leveraged your custom
useFetchhook to keep your component's code clean and focused on presentation logic. - You reinforced your knowledge of declarative array methods like
.filter()and.map()to transform data into a UI. - The data flow is clear: user input updates state (
query), which causes a re-render, which in turn re-calculates thefilteredCharactersand updates the display.
This project is a significant step. The patterns you used here—fetching a list of items, managing loading/error states, and providing search/filter functionality—are the bedrock of many real-world applications, especially the e-commerce project you aim to build.
Preview of the Next Module:
So far, our application lives on a single "page." To build a full e-commerce site, you'll need a home page, product pages, a cart page, and a checkout page. In our next module, Client-Side Routing with React Router, you'll learn how to create this multi-page experience within a single-page application (SPA), a crucial skill for building complex web apps.