Overview
Tienda ETCA implements pagination for the product gallery using React-Bootstrap’s Pagination component. The system displays 5 products per page and includes smooth scrolling to the top of the gallery when changing pages.
Implementation
Pagination is implemented in the ProductList component at ~/workspace/source/src/components/ProductList.jsx:5-70.
Import
import Pagination from 'react-bootstrap/Pagination'
State Management
const [ currentPage , setCurrentPage ] = useState ( 1 )
const itemsPerPage = 5
The pagination state tracks the current page number, starting from 1.
The component calculates which products to display based on the current page:
const ProductList = () => {
const { productosFiltrados , busqueda , setBusqueda } = useContext ( CartContext )
const [ currentPage , setCurrentPage ] = useState ( 1 )
const itemsPerPage = 5
const indexOfLast = currentPage * itemsPerPage
const indexOfFirst = indexOfLast - itemsPerPage
const currentProducts = productosFiltrados . slice ( indexOfFirst , indexOfLast )
const totalPages = Math . ceil ( productosFiltrados . length / itemsPerPage )
// ...
}
Calculation Breakdown
Calculate Last Index
const indexOfLast = currentPage * itemsPerPage
For page 1: 1 * 5 = 5
Calculate First Index
const indexOfFirst = indexOfLast - itemsPerPage
For page 1: 5 - 5 = 0
Slice Products Array
const currentProducts = productosFiltrados . slice ( indexOfFirst , indexOfLast )
For page 1: productosFiltrados.slice(0, 5) returns items 0-4
Calculate Total Pages
const totalPages = Math . ceil ( productosFiltrados . length / itemsPerPage )
For 23 products: Math.ceil(23 / 5) = 5 pages
The pagination component renders at ~/workspace/source/src/components/ProductList.jsx:50-70:
< div className = "paginacion-container" >
< Pagination className = "pagination" >
< Pagination.Prev
onClick = { () => setCurrentPage ( p => Math . max ( p - 1 , 1 )) }
disabled = { currentPage === 1 }
/>
{ Array . from ({ length: totalPages }, ( _ , i ) => (
< Pagination.Item
key = { i + 1 }
active = { i + 1 === currentPage }
onClick = { () => setCurrentPage ( i + 1 ) }
>
{ i + 1 }
</ Pagination.Item >
)) }
< Pagination.Next
onClick = { () => setCurrentPage ( p => Math . min ( p + 1 , totalPages )) }
disabled = { currentPage === totalPages }
/>
</ Pagination >
</ div >
Component Features
Previous Button Decrements page by 1 Disabled on page 1
Page Numbers Dynamically generated based on total pages Array . from ({ length: totalPages })
Active page highlighted
Next Button Increments page by 1 Math . min ( p + 1 , totalPages )
Disabled on last page
When users navigate between pages, the gallery automatically scrolls to the top for better UX:
const galeriaRef = useRef ( null );
const isFirstRender = useRef ( true );
useEffect (() => {
if ( isFirstRender . current ) {
isFirstRender . current = false ;
return ;
}
if ( galeriaRef . current ) {
galeriaRef . current . scrollIntoView ({ behavior: 'smooth' });
}
}, [ currentPage ]);
The isFirstRender check prevents scrolling on initial page load, only scrolling when users actively change pages.
The gallery container is marked with a ref:
< div className = 'galeria' ref = { galeriaRef } >
{ currentProducts . map ( product => (
< Product key = { product . id } product = { product } />
)) }
</ div >
Integration with Search
Pagination automatically adapts to search results. When users search for products, the pagination recalculates based on filtered results:
const { productosFiltrados } = useContext ( CartContext )
const currentProducts = productosFiltrados . slice ( indexOfFirst , indexOfLast )
const totalPages = Math . ceil ( productosFiltrados . length / itemsPerPage )
All Products (23)
Search Results (8)
Search Results (3)
totalPages = Math . ceil ( 23 / 5 ) = 5 pages
Shows: Page 1, 2, 3, 4, 5 totalPages = Math . ceil ( 8 / 5 ) = 2 pages
Shows: Page 1, 2 totalPages = Math . ceil ( 3 / 5 ) = 1 page
Shows: Page 1 (pagination may be hidden)
Full Component Code
Complete implementation from ~/workspace/source/src/components/ProductList.jsx:7-72:
const ProductList = () => {
const { productosFiltrados , busqueda , setBusqueda } = useContext ( CartContext )
const [ currentPage , setCurrentPage ] = useState ( 1 )
const itemsPerPage = 5
const indexOfLast = currentPage * itemsPerPage
const indexOfFirst = indexOfLast - itemsPerPage
const currentProducts = productosFiltrados . slice ( indexOfFirst , indexOfLast )
const totalPages = Math . ceil ( productosFiltrados . length / itemsPerPage )
const galeriaRef = useRef ( null );
const isFirstRender = useRef ( true );
useEffect (() => {
if ( isFirstRender . current ) {
isFirstRender . current = false ;
return ;
}
if ( galeriaRef . current ) {
galeriaRef . current . scrollIntoView ({ behavior: 'smooth' });
}
}, [ currentPage ]);
return (
<>
< div className = "busqueda-container" >
< input
type = 'text'
placeholder = 'Buscar productos...'
value = { busqueda }
onChange = { ( e ) => setBusqueda ( e . target . value ) }
/>
</ div >
< div className = 'galeria' ref = { galeriaRef } >
{ currentProducts . map ( product => (
< Product key = { product . id } product = { product } />
)) }
</ div >
< div className = "paginacion-container" >
< Pagination className = "pagination" >
< Pagination.Prev
onClick = { () => setCurrentPage ( p => Math . max ( p - 1 , 1 )) }
disabled = { currentPage === 1 }
/>
{ Array . from ({ length: totalPages }, ( _ , i ) => (
< Pagination.Item
key = { i + 1 }
active = { i + 1 === currentPage }
onClick = { () => setCurrentPage ( i + 1 ) }
>
{ i + 1 }
</ Pagination.Item >
)) }
< Pagination.Next
onClick = { () => setCurrentPage ( p => Math . min ( p + 1 , totalPages )) }
disabled = { currentPage === totalPages }
/>
</ Pagination >
</ div >
</>
)
}
Customization Options
Change Items Per Page
const itemsPerPage = 10 ; // Show 10 products per page
Reset to First Page on Search
useEffect (() => {
setCurrentPage ( 1 );
}, [ busqueda ]);
Custom Page Button Styling
< Pagination.Item
key = { i + 1 }
active = { i + 1 === currentPage }
onClick = { () => setCurrentPage ( i + 1 ) }
className = "custom-page-item"
>
{ i + 1 }
</ Pagination.Item >
Best Practices
Prevent Out-of-Bounds Navigation
Use Math.max() and Math.min() to ensure page numbers stay within valid range: Math . max ( p - 1 , 1 ) // Never go below page 1
Math . min ( p + 1 , totalPages ) // Never exceed last page
Disable Navigation at Boundaries
Disable Previous button on first page and Next button on last page for better UX: disabled = { currentPage === 1 }
disabled = { currentPage === totalPages }
Smooth Scroll Enhancement
Always calculate total pages based on filtered results for search compatibility: Math . ceil ( productosFiltrados . length / itemsPerPage )
For large product catalogs with many pages, consider implementing page number ellipsis (e.g., “1 … 5 6 7 … 20”) to keep the pagination UI clean.