Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 77

Displaying Product Details

Recommended resources

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:

  1. When the ProductDetailPage mounts, we'll trigger a useEffect hook.
  2. Inside the effect, we'll use the productId from useParams to construct an API endpoint URL.
  3. We'll use the fetch API (with async/await, which you've worked with before) to get the product data.
  4. We'll use the useState hook 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.

React Routing Guide - Strapi

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 loading is true, we should show a loading message.
  • If error has 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 the useEffect hook 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), the loading status, and any potential error.
  • 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.