Learn how to build a Magento 2 category page using ReactJs in a headless development environment.
1. Introduction
2. Setting Up Your Category Page with ReactJs
3. Creating Your Category Page with ReactJs
4. Conclusion
By leveraging Magento 2 headless architecture, this guide will help you integrate the Magento 2 backend with React.
Unlocking a more flexible and dynamic user experience and harnessing the full potential of headless commerce.
Introduction
Creating a category page in React for your Magento 2 store can significantly impact user engagement and encourage visitors to explore various product listings.
For a comprehensive guide on setting up product pages, check out our blog, How to Create a Magento 2 Product Page in React.
This guide will walk you through building an efficient and user-friendly category page, complete with code examples and detailed explanations.
And, In this blog you are going to know about Magento 2 React Development.
You’ll gain valuable insights into React.js development, learning how to:
- Fetch data from the Magento 2 GraphQL API
- Manage application state effectively
- Design a responsive layout that enhances the overall shopping experience
By the end of this guide, you’ll have the skills to create a dynamic and engaging category page that elevates your e-commerce platform.
Setting Up Your Category Page with ReactJs
1. Create Your ReactJs Project:
Open your terminal and run the following command to create a React.Js Project
npx create-react-app my-magento-store cd my-magento-store
2. Navigate to root directory:
cd my-magento-store
3. Install Necessary Packages In Your ReactJs:
//As, I created this with Tailwind so: npm install -D tailwindcss postcss autoprefixer npx tailwindcss init -p
Creating Your Category Page with ReactJs
1. Set Up GraphQL Queries:
As we are using Magento 2 GraphQl API. So lets, create a file for your GraphQL queries. This keeps your code organized.
mkdir src/graphql touch src/graphql/queries.js
export const GET_PRODUCT_BY_CATEGORY = ` query( $filter: ProductAttributeFilterInput $pageSize: Int = 12 $currentPage: Int = 1 $sort: ProductAttributeSortInput ) { products( filter: $filter pageSize: $pageSize currentPage: $currentPage sort: $sort ) { page_info { current_page page_size total_pages } total_count aggregations { attribute_code label count options { count label value } } sort_fields { default options { label value } } items { media_gallery { disabled label url position } review_count rating_summary url_key uid sku name thumbnail { id: url url label } price_range { maximum_price { final_price { currency value } regular_price { currency value } } minimum_price { final_price { currency value } regular_price { currency value } } } } suggestions { search } } } `;
2. Created a fetch handler in reactjs:
For reusability created a fetch handler function in file called FetchHandler.ts.
// src/api/graphql.js export const fetchGraphQL = (query, variables) => { return fetch("https://m243.winterroot.com/graphql", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ query, variables }), }) .then((response) => response.json()) .then((result) => { if (result.errors) { throw new Error(result.errors.map((err) => err.message).join(", ")); } return result.data; }); };
3. Render Category Page In Your React Application
Create Your Category Page Component
mkdir src/components/category touch src/components/category/index.jsx
Next, add the following code to the index.jsx
component:
import { useEffect, useState } from "react"; import SidebarFilter from "./SidebarFilter"; import { GET_PRODUCT_BY_CATEGORY } from "../../graphql/queries"; import Product from "./Product"; import { ArrowDownIcon, ArrowUpIcon, GridView, ListView, XMarkIcon } from "./Icons"; import { fetchGraphQL } from "./FetchHandler"; import Pagination from "./Pagination"; import SelectedList from "./SelectedFilter"; import Accordion from "./Accordion"; export default function Category() { const [product, setProduct] = useState(); const [data, setData] = useState({}); const [productView, setProductView] = useState("grid"); const [filter, setFilter] = useState({}); const [loading, setLoading] = useState(true); const [selectedOption, setSelectedOption] = useState(data?.products?.sort_fields.default); const [sort, setSort] = useState({}); const [order, setOrder] = useState("DESC"); const [currentPage, setCurrentPage] = useState(1); const [pageSize, setPageSize] = useState(12); const [openSideBar, setSideBar] = useState(false); const [selectedFilters, setSelectedFilters] = useState(data?.products?.applied_filters || []); const fetchProduct = async () => { fetchGraphQL(GET_PRODUCT_BY_CATEGORY, { filter, sort, currentPage, page_size: parseInt(pageSize) }) .then((res) => { setData(res); const fetchedProduct = res.products.items || null; setProduct(fetchedProduct); setLoading(false); }) .catch((error) => { setLoading(false); console.error("Error fetchin category data:", error.message); }); }; const asscendDescend = () => { if (order === "DESC") { setOrder("ASC"); } else { setOrder("DESC"); } }; const handleChange = (event) => { const value = event.target.value; setSelectedOption(value); }; useEffect(() => { if (openSideBar) { document.body.classList.add("overflow-hidden"); } else { document.body.classList.remove("overflow-hidden"); } // Cleanup to remove class on unmount return () => { document.body.classList.remove("overflow-hidden"); }; }, [openSideBar]); useEffect(() => { fetchProduct(); }, [filter, sort, order, currentPage, pageSize]); useEffect(() => { if (selectedOption) { setSort({ [selectedOption]: order }); } }, [order, selectedOption]); if (loading) { return <div>Loading...</div>; } if (!product) { return <div>No product found.</div>; } const removeFilter = (attributeCode) => { setSelectedFilters((prev) => { const { [attributeCode]: _, ...rest } = prev; // Destructure to remove the specified attribute return rest; // Return the remaining filters }); // Update the filter state to remove the attribute code setFilter((prev) => { const updatedFilter = { ...prev }; delete updatedFilter[attributeCode]; // Remove the attribute if it's empty return updatedFilter; }); }; const resetFilter = () => { setFilter({}); setSelectedFilters([]); }; return ( <> <div className={`fixed inset-y-0 lg:hidden right-0 z-10 bg-white w-[99vw] transform ${ openSideBar ? "translate-x-0" : "translate-x-full" } transition-transform overflow-y-auto duration-300 pt-5 ease-in-out`} > <SidebarFilter selectedFilters={selectedFilters} resetFilter={resetFilter} removeFilter={removeFilter} aggregations={data?.products?.aggregations} setFilter={setFilter} setSelectedFilters={setSelectedFilters} /> <button onClick={() => setSideBar(false)} className="absolute right-0 p-2 rounded top-7"> <XMarkIcon className="w-5 h-5 text-slate-500" /> </button> </div> <div className="pb-20"> <div className="container grid w-full px-4 mx-auto"> <div className="grid lg:gap-5 lg:grid-cols-4"> <div className="hidden lg:flex lg:flex-col lg:col-span-1"> <SidebarFilter selectedFilters={selectedFilters} resetFilter={resetFilter} removeFilter={removeFilter} aggregations={data?.products?.aggregations} setFilter={setFilter} setSelectedFilters={setSelectedFilters} /> </div> <div className="flex flex-col lg:col-span-3"> <div className="flex justify-between px-2 mt-6 md:px-4"> <div className="items-center hidden gap-2 lg:flex"> <div className="flex w-[70px] border border-black"> <div onClick={() => setProductView("grid")} className=" rounded-l-sm border-r border-0 hover:bg-gray-300 cursor-pointer border-x-gray-400 w-10 p-2 px-2 shadow shadow-gray-300/80 bg-[#efefef] text-gray-600" > <GridView /> </div> <div className="rounded-r-sm w-10 border-0 p-2 px-2 shadow cursor-pointer hover:bg-gray-300 shadow-gray-300/80 bg-[#efefef] text-gray-600 " onClick={() => setProductView("list")} > <ListView /> </div> </div> <p className="text-xs">{data?.products?.total_count} items</p> </div> <button className="flex px-4 py-1 text-sm border border-black lg:hidden bg-neutral-100 max-w-fit" onClick={() => setSideBar(true)} > Shop By </button> <div className="flex items-center gap-2"> <select value={selectedOption} onChange={handleChange} className="outline-none py-[6px] text-sm border-black bg-gray-50 placeholder:text-gray-600 placeholder:px-2 border text-gray-900 focus:outline-none focus:ring-primary-600 focus:border-primary-600 block w-full p-1 px-2 rounded-sm dark:focus:ring-blue-500 dark:focus:border-blue-500 focus:shadow-[0_0_3px_1px_#00699d]" > <option disabled>{data?.products?.sort_fields.default}</option> {data?.products?.sort_fields.options.map((option) => ( <option key={option.value} value={option.value}> {option.label} </option> ))} </select> <button onClick={asscendDescend}> {order === "DESC" ? ( <ArrowDownIcon className="w-5 h-5 stroke-2 stroke-slate-500" /> ) : ( <ArrowUpIcon className="w-5 h-5 stroke-2 stroke-slate-500" /> )} </button> </div> </div> <div className="flex flex-col w-full mt-5 lg:hidden"> {Object.entries(filter)?.length > 0 && ( <div className="border-t"> <Accordion title={`Now Shopping by : (${Object.entries(filter)?.length})`}> <div className="pt-3 -ml-3"> <SelectedList removeFilter={removeFilter} resetFilter={resetFilter} selectedFilters={selectedFilters} /> </div> </Accordion> </div> )} <p className="mx-2 my-4 text-xs">{data?.products?.total_count} items</p> </div> {/* */} <div className={`gap-4 ${ productView !== "grid" ? "flex flex-col" : "grid-cols-2 md:grid-cols-3 grid lg:grid-cols-4" }`} > {product?.map((i, index) => ( <Product productView={productView} key={index} product={i} /> ))} </div> <Pagination data={data} currentPage={currentPage} setCurrentPage={setCurrentPage} setPageSize={setPageSize} pageSize={pageSize} /> </div> </div> </div> </div> </> ); }
4. Add Sidebar Filter Component In Your Category Page
Create the Sidebar Filter Component
touch src/components/category/SidebarFilter.jsx
Implement the Filter Component To promote reusability, create a separate component for the available filter list based on the attributes received from the category list API.
Add the following code to SidebarFilter.jsx
:
import Accordion from "./Accordion"; import SelectedList from "./SelectedFilter"; export default function SidebarFilter({ aggregations, setFilter, setSelectedFilters, selectedFilters, resetFilter, removeFilter, }) { const handleFilterChange = (attributeCode, value, label) => { if (attributeCode) { setSelectedFilters((prev) => { const currentSelections = prev[attributeCode] || []; const isSelected = currentSelections.includes(value); // Update the selected filters for checkboxes const newSelections = attributeCode === "price" ? [value] // Only one selection for price : isSelected ? currentSelections.filter((v) => v !== value) // Remove if already selected : [...currentSelections, value, label]; // Add if not selected return { ...prev, [attributeCode]: newSelections, }; }); // Update the filter state setFilter((prev) => ({ ...prev, [attributeCode]: attributeCode === "price" ? { from: value.split("_")?.[0], to: value?.split("_")?.[1] } : { eq: value }, // Use 'in' for selected price range })); } }; return ( <div> {Object.entries(selectedFilters)?.length > 0 && ( <div className="hidden lg:flex"> <SelectedList removeFilter={removeFilter} resetFilter={resetFilter} selectedFilters={selectedFilters} /> </div> )} <p className="flex p-2 text-2xl font-light border-b lg:text-base lg:font-medium">Shoping Options</p> {aggregations?.map((aggregation, index) => ( <Accordion key={index} title={aggregation.label}> <div className="flex flex-col gap-2 px-1 py-2"> {aggregation.options.map((option) => ( <div key={option.value}> <label className="text-sm cursor-pointer"> <input className="hidden" type="radio" value={option.value} checked={selectedFilters.price === option.value} // Ensure only one selection for price onChange={() => handleFilterChange(aggregation.attribute_code, option.value, option?.label)} /> <span dangerouslySetInnerHTML={{ __html: option?.label }}></span> ({option.count}) </label> </div> ))} </div> </Accordion> ))} </div> ); }
Mobile View
Filter List In Sidebar
5. Add Selected Filter List Component In Your Category Page
Create the Selected Filter Component
touch src/components/category/SelectedFilter.jsx
Implement the Selected Filter Component To promote reusability, create a separate component for displaying the selected filters.
This component will include functions for removing filters and rendering the filter list. Add the following code to SelectedFilter.jsx
:
import { XMarkIcon } from "./Icons"; const SelectedList = ({ removeFilter, resetFilter, selectedFilters }) => { return ( <div className="flex flex-col px-2"> <p className="hidden pb-5 text-base font-medium lg:flex">Now Shopping by</p> <ul> {Object.entries(selectedFilters).map(([attributeCode, options]) => ( <li key={attributeCode} className="flex items-center text-sm gap-0.5"> <button onClick={() => options.forEach((option) => removeFilter(attributeCode, option))}> <XMarkIcon className="w-5 h-5 text-slate-500" /> </button> <span className="font-semibold capitalize cursor-pointer">{attributeCode.replace("_", " ")}</span>:{" "} {options.length > 1 ? options[1] : options[0].replace("_", " - ")}{" "} </li> ))} <li className="mt-5 text-sm cursor-pointer mb-7 text-sky-600" onClick={resetFilter}> Clear All </li> </ul> </div> ); }; export default SelectedList;
6. Add Pagination In Your Category Page
Create the Pagination Component
touch src/components/category/Pagination.jsx
Implement the Pagination Component To incorporate pagination and page size functionality, add the following code to Pagination.jsx
:
import React from "react"; import { ChevronDownIcon } from "./Icons"; export default function Pagination({ data, currentPage, setCurrentPage, setPageSize, pageSize }) { const productCounts = Array.from( { length: Math.floor(data?.products?.total_count / 12) + 1 }, (_, index) => index * 12 ); const handleChangePageSize = (event) => { const value = event.target.value; setPageSize(value); }; const handlePageChange = (page) => { setCurrentPage(page); }; const getPageNumbers = () => { const pages = []; const maxVisiblePages = 5; // Determine the range of pages to display let startPage = Math.max(1, currentPage - Math.floor(maxVisiblePages / 2)); let endPage = Math.min(data?.products?.page_info?.total_pages, startPage + maxVisiblePages - 1); // Adjust startPage if there are not enough pages before if (endPage - startPage < maxVisiblePages - 1) { startPage = Math.max(1, endPage - maxVisiblePages + 1); } // Build the array of page numbers for (let i = startPage; i <= endPage; i++) { pages.push(i); } return pages; }; return ( data?.products?.page_info?.total_pages > 1 && ( <div className="flex items-center justify-between w-full"> <div className="flex justify-center space-x-2"> {/* Previous button */} {currentPage > 1 && ( <button onClick={() => handlePageChange(currentPage - 1)} className="px-2 py-1 text-black bg-gray-300 rounded" > <ChevronDownIcon className="w-5 h-5 rotate-90 text-slate-500" /> </button> )} {getPageNumbers().map((page) => ( <button key={page} onClick={() => handlePageChange(page)} className={`p-1 rounded text-center text-xs ${ currentPage === page ? "bg-neutral-200 text-black" : "text-sky-800" }`} > {page} </button> ))} {/* Next button */} {currentPage < data?.products?.page_info?.total_pages && ( <button onClick={() => handlePageChange(currentPage + 1)} className="px-2 py-1 text-black bg-gray-300 rounded" > <ChevronDownIcon className="w-5 h-5 -rotate-90 text-slate-500" /> </button> )} </div> <select value={pageSize} onChange={handleChangePageSize} className="outline-none max-w-fit py-[6px] px-4 text-sm bg-gray-50 placeholder:text-gray-600 placeholder:px-2 border border-gray-300 text-gray-900 focus:outline-none focus:ring-primary-600 focus:border-primary-600 block w-full p-1 px-2 rounded-sm dark:focus:ring-blue-500 dark:focus:border-blue-500 focus:shadow-[0_0_3px_1px_#00699d]" > <option disabled>{data?.products?.sort_fields.default}</option> {productCounts.map((option) => ( <option key={option} value={option}> {option} </option> ))} </select> </div> ) ); }
7. Create Product Card Component
Create A Product Card Component Component For Category Items
touch src/components/category/Product.jsx
Implement the Product Card Component To promote reusability, create a separate component for the product card structure that will be rendered in the category listing.
Add the following code to Product.jsx
:
import { ChartBarIcon, HeartIcon, StarIcon } from "./Icons"; const Product = ({ product, productView }) => { const priceData = product?.price_range?.maximum_price?.final_price; const currency = "USD"; const value = priceData?.value; const price = value?.toLocaleString("en-US", { style: "currency", currency, }); const listType = productView !== "grid"; return ( <div className="group"> <div className={`relative flex ${listType ? "flex-row" : "flex-col"} w-full overflow-hidden bg-white rounded-lg`}> <div className="relative grid object-cover mx-3 mt-3 overflow-hidden aspect-square h-60 rounded-xl"> <img className="object-cover aspect-square" src={product.media_gallery?.at(0).url} alt={product.name} /> </div> <div className="px-5 pb-5 mt-4"> <h5 className="text-xl tracking-tight text-slate-900"> {" "} <span dangerouslySetInnerHTML={{ __html: product?.name }}></span> </h5> <div className="flex items-center gap-2"> {product?.review_count > 0 && ( <div className="flex items-center"> {Array.from({ length: 5 }, (_, index) => { const ratingValue = index + 1; const isFilled = ratingValue <= product.rating_summary / 20; // Assuming rating_summary is out of 100 return <StarIcon index={index} isFilled={isFilled} />; })} <span className="rounded text-sky-600 px-2.5 py-0.5 text-xs font-normal"> {product?.review_count} Reviews </span> </div> )} </div> <div className="flex items-center justify-between mt-2 mb-5"> <p> <span className="font-bold text-md text-slate-900">{price}</span> </p> </div> <div className={`items-center gap-2 ${listType ? "flex" : "hidden group-hover:flex"}`}> <button className="w-full px-4 py-2 my-6 font-medium text-white max-w-fit bg-sky-600 hover:bg-sky-700"> Add to cart </button> <HeartIcon className="w-5 h-5 text-slate-500" /> <ChartBarIcon className="w-5 h-5 text-slate-500" /> </div> {listType && <p className="text-sm text-sky-600">Learn More</p>} </div> </div> </div> ); }; export default Product;
8. Add Accordion Component For Your Category Page
Create Accordion Component
touch src/components/category/Accordion.jsx
Implement the Accordion Component To promote reusability, create a separate component for the accordion, which will be used to render various types of filter lists.
Add the following code to Accordion.jsx
:
import { useState } from "react"; import { ChevronDownIcon, ChevronUpIcon } from "./Icons"; const Accordion = ({ children, title }) => { const [open, setOpen] =useState(false); return ( <> <div className="w-full p-2 border border-t-0 border-l-0 border-r-0"> <div onClick={() => setOpen(!open)} className="flex items-center justify-between w-full cursor-pointer"> <div className="text-base font-medium capitalize">{title}</div> <div className="text-base"> {open ? ( <span> <ChevronUpIcon className="w-5 h-5 text-slate-500" /> </span> ) : ( <span> <ChevronDownIcon className="w-5 h-5 text-slate-500" /> </span> )} </div> </div> {open && children} </div> </> ); }; export default Accordion;
9. Update App.jsx Component Of Your React Project
To ensure your React application effectively utilizes the new components you’ve created for the category page,
it’s essential to update the App.jsx
component. This update will involve integrating the header and the category page into a cohesive layout.
import "./App.css"; import Category from "./components/category/Index"; import Header "./components/Header"; function App() { return ( <> <Header /> <Category /> </> ); } export default App;
10. Run Your ReactJs Application:
Finally, run your React.Js app to see your category page in action:
npm start
Conclusion
In this blog, we explored how to create a robust shopping category page using React in conjunction with the Magento API.
We covered essential functionalities, including sorting options, pagination, and dynamic filters that respond to user interactions.
Key Takeaways:
- State Management: Leveraging React’s state hooks to manage category data ensures a seamless user experience.
- GraphQL Integration: Utilizing the Magento 2 GraphQL API for fetching and displaying category information allows for efficient data handling and flexibility in managing category operations.
By following these principles and utilizing the components we’ve discussed, developers can create an engaging and efficient product listing experience.
That integrates smoothly with Magento 2’s powerful e-commerce capabilities.
With this foundation, you can further enhance features, improve performance, and refine the user interface to meet your specific business needs.
Be the first to comment.