In this lesson, we will build a product listing page that lists a set of products with their thumbnail, title and price. Clicking on a particular product should take us to the product details page and display the corresponding product details. Here is how our application should function after implementing this feature:
Since this feature has a lot of moving parts, let's break it down into smaller requirements:
- Add an API for fetching a list of products.
- Display the product list as cards.
- Add navigation to the
Product component from the product listing page.
- Display the detail of the product clicked.
Let's get started by adding an API connector to fetch the products list from the product listing API available to us. Since we have already set the Axios for managing API requests, we can directly add a function to our productsApi to send a get request to fetch products:
We can use the Home component to display the product list. Let's rename the Home component to ProductList to indicate what it does:
We will display the ProductList component under the "/products" URL. Let's replace the Route for the Home component with the ProductList route. We can also remove the NavLink components added in the previous lesson from App.jsx:
But now, when the user opens the application, they will see the PageNotFound component under the root URL, which is not desirable. Instead, they should be taken to the ProductList component when they visit our application.
We can enable this by redirecting users to the "/products" path from the root URL. The React Router provides the <Redirect> component, which accepts a from and to prop to add redirection. Similar to the <Route> component, we should pass the exact prop to <Redirect> to match the path completely.
Note: All direct children of a must be either or components. Any other components nested inside will result in unexpected behavior or runtime warnings.
Now, if you open the application, you will get redirected to the "/products" URL, and the ProductList component will be rendered. From the ProductList component, we will fetch the products and render them as cards.
Let's import productsApi from the apis directory and add a function to fetch products inside the ProductList component:
We will call the above function inside the useEffect hook with an empty dependency array to fetch products data on the initial render.
As you can see from the console, the product listing API returns an object containing an array of products and the total product count in the database. We will destructure the products array from this object and store it in a state:
We should also handle the loading state while fetching products:
Now, instead of the placeholder text "Home", let's render the product list.
For that, first, create a ProductListItem component to render the product card. Since we need to display the thumbnail, name and offer price of products, let's have a prop for each of them:
Next, map the products array over the ProductListItem.
We have completed the first phase of the feature by listing the product details:
Our next goal is to enable users to view the corresponding product details when they click the product card. To achieve this, we'll utilize a technique called dynamic routing. This technique allows us to define routes that can accept parameters.
We can apply this technique to include the slug of a product as part of the product path like this:
Now, the product component will be displayed when we visit any URL of the form /products/:slug, where :slug can be any string. For the path "/products/mens-casual-slim-fit", mens-casual-slim-fit is the :slug. The : at the beginning of the :slug indicates that it is the dynamic part of the URL.
Next, we can replace the div tag in the ProductListItem component with Link to add navigation to the product URL with the corresponding slug:
At this point, you can navigate to the Product component by clicking the product cards, and the URL will contain the slug of the product clicked.
Now, from the Product component, we can access the slug from the URL using the useParams hook from the react-router-dom. The useParams hook returns an object containing dynamic params from the current URL matched by the <Route path>.
Let's extract the slug from the URL params. We can use this slug to send API requests for the given product using the productsApi.show function.
We can enable the show function of productsApi to send the request to the given slug, replacing the hard-coded slug infinix-inbook-2 with a slug parameter:
With this, users will be able to view product details of the selected product from the list:
Nevertheless, if a user enters a URL with an invalid slug, the backend will throw a 404 error since there's no product corresponding to the given slug. To address this scenario, we will set an isError state inside the catch block while fetching the product data and use it to display a PageNotFound component.
As the final touch, let's add a back button on the product page to navigate back to the product listing page:
To achieve this, we'll make use of the useHistory hook, which provides access to the browser history. The useHistory hook returns a history object with a set of properties and methods for managing browser history. You can refer to the official documentation for a comprehensive list of properties and methods.
For our specific use case, we'll utilize the goBack method from the history object, which helps us navigate to the previous location in the history stack:
While the product listing functionality is complete, there are still opportunities for improving code quality.
Let's clean up the code before committing the changes.
Grouping components into folder
In the current project structure, all components are placed in the src/components directory. However, as the project grows, this approach becomes impractical, making it challenging to locate specific components. To address this, we should organize components into folders based on their functionality.
For example, the Carousel and Product components, used together to display the Product page, should be grouped into a folder named Product. The Product component becomes the entry point of this folder, and therefore, we will rename the Product.jsx file into index.jsx.
Similarly, we can group the ProductListItem and ProductList into ProductList folder and rename the ProductList.jsx into index.jsx:
In both the product list page and the product detail page, the page headers share a similar UI, with the only difference being the presence of a back button on the product detail page:
However, we are writing the same JSX in the ProductList and Product components. To eliminate redundancy, we can extract the common code into a component named Header.jsx. We can add two props, title and shouldShowBackButton, to make it flexible. Since both the Product and ProductList components need to use this, we will keep it inside a separate folder src/components/commons.
Now, you can use the Header component inside the Product and ProductList components by passing the necessary props:
We can also create a separate component for PageLoader to avoid code repetition and use it inside Product and ProductList components:
Similarly, the PageNotFound component, which is not specific to any page, can also be moved into the src/components/commons folder.
Here is how the folder structure will look like after refactoring:
Exporting multiple components from a namespace
Examining the directory structure above, you will notice that we are exporting multiple components from the commons folder, resulting in several imports under the components/commons namespace:
To reduce the number of import statements, we will introduce a file index.js within the components/commons directory to group these exports.
Create an index.js file inside the src/components/commons folder and export all the components from the commons folder as shown:
After adding the above index.js file, we can replace the multiple imports from components/commons with a single import like this:
To learn more about code structuring, check out this lesson in the miscellaneous section.
Ensure that the application works properly as before after refactoring the code.
Let's commit the new changes:
You can verify the changes here.