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.
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.