Hello! Let's continue building our e-commerce application.
In the last lesson, you did an excellent job setting up a dynamic route for the product detail page. You can now navigate from the product list to a unique URL for each product (e.g., /products/1), and the ProductDetailPage component correctly captures the product's ID using the useParams hook.
Today, we will bring that page to life. Our goal is to fetch and display detailed information for a single product on its page. We'll use the ID we captured to make a specific API request, manage the fetching process, and render the product's data. This process is a cornerstone of building data-driven applications.
From your experience with Laravel, this is analogous to a controller method that finds a specific record by its ID (Product::find($id)) and passes that data to a Blade template for rendering. Here, we'll be doing the client-side equivalent using React hooks.
1. The Data Fetching Strategy
Our ProductDetailPage needs to perform a "side effect": communicating with an external API. In React, the primary tool for managing side effects in functional components is the useEffect hook.
Here’s the plan:
- When the
ProductDetailPagemounts, we'll trigger auseEffecthook. - Inside the effect, we'll use the
productIdfromuseParamsto construct an API endpoint URL. - We'll use the
fetchAPI (withasync/await, which you've worked with before) to get the product data. - We'll use the
useStatehook to store the product data, as well as loading and error states.
Crucially, we need our useEffect to re-run if the productId in the URL changes (for example, if a user navigates from one product page directly to another). To do this, we'll add productId to the hook's dependency array.
2. Setting Up State and the useEffect Hook
Let's modify src/pages/ProductDetailPage.jsx. First, we'll set up the necessary state variables to manage the data, loading status, and any potential errors.
// src/pages/ProductDetailPage.jsx
import React, { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
const ProductDetailPage = () => {
const { productId } = useParams();
// State for the product data, loading status, and errors
const [product, setProduct] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
return (
<div>
{/* We will add conditional rendering here soon */}
</div>
);
};
export default ProductDetailPage;
Now, let's add the useEffect hook to fetch the data. We will define an async function inside the effect and call it immediately. The API endpoint for a single product from Fake Store API is https://fakestoreapi.com/products/ID.
// Add this inside the ProductDetailPage component, after the state declarations
useEffect(() => {
const fetchProduct = async () => {
try {
// Set loading to true before the fetch
setLoading(true);
setError(null); // Clear previous errors
const response = await fetch(`https://fakestoreapi.com/products/${productId}`);
if (!response.ok) {
// If response is not 2xx, throw an error
throw new Error('Sorry, something went wrong.');
}
const data = await response.json();
setProduct(data);
} catch (err) {
setError(err.message);
} finally {
// Set loading to false after the fetch completes (either success or fail)
setLoading(false);
}
};
fetchProduct();
}, [productId]); // The effect depends on productId and will re-run if it changes
This pattern is extremely common. The following article provides an excellent practical example of this exact workflow: defining a dynamic route, using useParams, and fetching data within useEffect.
The 'React Routing Guide' from Strapi has a section that perfectly illustrates the pattern we're implementing. It shows fetching an article by its 'slug' (similar to our 'productId') within a useEffect hook.
In the article, find the sub-heading 'Practical Examples with Strapi and React Routing'. Focus on the code snippets for pages/ArticlePage.js and api/strapi.js. Notice how useParams is used to get the slug, and how that slug is used in the useEffect dependency array and the fetchArticleBySlug function.
3. Conditional Rendering
With our state management and data fetching in place, the final step is to render the UI based on the current state.
- If
loadingistrue, we should show a loading message. - If
errorhas a value, we should display the error. - If the data has been fetched successfully, we render the product details.
Let's complete the ProductDetailPage.jsx component by adding this logic to the return statement.
// src/pages/ProductDetailPage.jsx
import React, { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
const ProductDetailPage = () => {
const { productId } = useParams();
const [product, setProduct] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchProduct = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch(`https://fakestoreapi.com/products/${productId}`);
if (!response.ok) {
throw new Error('Sorry, product not found or an error occurred.');
}
const data = await response.json();
setProduct(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchProduct();
}, [productId]);
// Conditional rendering logic
if (loading) {
return <div style={{ textAlign: 'center', marginTop: '2rem' }}>Loading...</div>;
}
if (error) {
return <div style={{ textAlign: 'center', marginTop: '2rem', color: 'red' }}>Error: {error}</div>;
}
if (!product) {
// This case handles if loading is false but product is still null
return <div style={{ textAlign: 'center', marginTop: '2rem' }}>Product not found.</div>;
}
// Render the product details
return (
<div style={{ display: 'flex', padding: '2rem', maxWidth: '900px', margin: 'auto' }}>
<img src={product.image} alt={product.title} style={{ width: '40%', objectFit: 'contain', marginRight: '2rem' }} />
<div>
<h2 style={{ fontSize: '1.75rem' }}>{product.title}</h2>
<p style={{ background: '#eee', display: 'inline-block', padding: '0.25rem 0.5rem', borderRadius: '4px', textTransform: 'capitalize' }}>{product.category}</p>
<p style={{ fontSize: '1.1rem', margin: '1rem 0' }}>{product.description}</p>
<p style={{ fontSize: '2rem', fontWeight: 'bold' }}>${product.price}</p>
{/* We'll add an "Add to Cart" button here in a future lesson */}
</div>
</div>
);
};
export default ProductDetailPage;
For another perspective on building a component that fetches and displays data, the following resource provides a step-by-step tutorial. While it fetches a list instead of a single item, the core principles of using useEffect and handling a loading state are identical.
Consuming REST APIs In React With Fetch And Axios
The Smashing Magazine article 'Consuming REST APIs In React With Fetch And Axios' builds a small app that fetches data from the GitHub API. It's a great practical walkthrough of the concepts we've just applied.
Read the section 'Let’s Consume A REST API With Fetch Method'. Pay close attention to the App.js file explanation. See how it uses useEffect to fetch data and useState to manage loading and repos states. This reinforces the pattern of separating your fetching logic and your UI state.
Now, run your application. Click on any product from the main page, and you should see a "Loading..." message briefly before the full product details appear. Try navigating to an invalid ID (e.g., /products/999) to see your error handling in action.
Conclusion
Fantastic work! You have successfully connected your dynamic route to a real API, creating a fully functional product detail page.
Key Takeaways:
- Fetching Data with
useEffect: You used theuseEffecthook to perform an API call when the component loads or when a dependency (productId) changes. - State Management for API Calls: You managed three distinct states with
useState: the data itself (product), theloadingstatus, and any potentialerror. - Conditional Rendering: You used the state variables to conditionally render different UI outputs, providing a smooth user experience for loading, error, and success scenarios.
Preview of the Next Lesson:
Now that users can view product details, the next logical step is to let them add items to a shopping cart. In the next lesson, we will set up a global CartContext using useReducer to manage the shopping cart state. This will introduce you to more advanced state management techniques in React, allowing different components across the application to share and modify the cart's contents.