Pagination
Place page-number navigation in the DataGrid footer and connect it to a server-side pagination API.
import * as React from 'react';
import { BGrid, BGridColumn, BGridDataItem } from 'beautiful-grid';
import DataGridContainer from '../components/DataGridContainer';
import { useContainerSize } from '../hooks/useContainerSize';
interface MemberRecord {
memberNo: string;
name: string;
email: string;
membership: string;
status: '정상' | '휴면' | '차단';
joinedAt: string;
}
const TOTAL_ELEMENTS = 498;
const PAGE_SIZE = 50;
const names = ['김민준', '이서연', '박지후', '최하윤', '정도현', '한유진'];
const memberships = ['일반', 'Silver', 'Gold', 'VIP'];
const createPageData = (currentPage: number): BGridDataItem<MemberRecord>[] => {
const startIndex = (currentPage - 1) * PAGE_SIZE;
const length = Math.max(0, Math.min(PAGE_SIZE, TOTAL_ELEMENTS - startIndex));
return Array.from({ length }, (_, pageIndex) => {
const index = startIndex + pageIndex + 1;
return {
values: {
memberNo: `MBR-${String(index).padStart(6, '0')}`,
name: names[index % names.length],
email: `member${String(index).padStart(4, '0')}@example.com`,
membership: memberships[index % memberships.length],
status: index % 17 === 0 ? '차단' : index % 7 === 0 ? '휴면' : '정상',
joinedAt: `2026-${String(((index - 1) % 8) + 1).padStart(2, '0')}-${String(((index - 1) % 27) + 1).padStart(2, '0')}`,
},
};
});
};
function PagingExample() {
const [currentPage, setCurrentPage] = React.useState(1);
const [columns, setColumns] = React.useState<BGridColumn<MemberRecord>[]>([
{ key: 'memberNo', label: '회원번호', width: 120, align: 'center', sortDisable: true },
{ key: 'name', label: '회원명', width: 110, align: 'center' },
{ key: 'email', label: '이메일', width: 240 },
{ key: 'membership', label: '등급', width: 100, align: 'center' },
{ key: 'status', label: '계정 상태', width: 100, align: 'center' },
{ key: 'joinedAt', label: '가입일', width: 120, align: 'center' },
]);
const data = React.useMemo(() => createPageData(currentPage), [currentPage]);
const containerRef = React.useRef<HTMLDivElement>(null);
const { width, height } = useContainerSize(containerRef);
return (
<DataGridContainer ref={containerRef}>
<BGrid<MemberRecord>
width={width}
height={height}
headerHeight={35}
data={data}
columns={columns}
rowKey='memberNo'
onChangeColumns={(_columnIndex, { columns }) => setColumns(columns)}
page={{
currentPage,
pageSize: PAGE_SIZE,
totalPages: Math.ceil(TOTAL_ELEMENTS / PAGE_SIZE),
totalElements: TOTAL_ELEMENTS,
loading: false,
onChange: pageNo => setCurrentPage(pageNo),
displayPaginationLength: 5,
}}
/>
</DataGridContainer>
);
}
export default PagingExample;import * as React from 'react';
import './DataGridContainer.css';
interface DataGridContainerProps extends React.HTMLAttributes<HTMLDivElement> {
children?: React.ReactNode;
}
/**
* Keeps a DataGrid in a measured, fixed layout box.
*
* BGrid's rendered root is absolutely positioned within this relative
* container. This makes a ResizeObserver measurement authoritative when a
* surrounding flex or grid layout shrinks as well as when it expands.
*/
const DataGridContainer = React.forwardRef<HTMLDivElement, DataGridContainerProps>(
({ className, ...rest }, ref) => (
<div ref={ref} className={`data-grid-container ${className ?? ''}`.trim()} {...rest} />
),
);
DataGridContainer.displayName = 'DataGridContainer';
export default DataGridContainer;.data-grid-container {
position: relative;
width: 100%;
height: 400px;
overflow: hidden;
font-size: 13px;
}
.data-grid-container > .bgrid-root {
position: absolute;
inset: 0;
}import * as React from 'react';
export function useContainerSize(ref: React.MutableRefObject<HTMLElement | null>, additionalDeps: unknown[] = []) {
const [width, setWidth] = React.useState(0);
const [height, setHeight] = React.useState(0);
const resizeObserver = React.useRef(
new ResizeObserver(entries => {
if (entries.length !== 1) {
throw new Error('Invalid Container length');
}
const [entry] = entries;
const { width, height } = entry.contentRect;
setWidth(width);
setHeight(height);
}),
);
React.useEffect(() => {
if (!ref.current) return;
const observer = resizeObserver.current;
const element = ref.current;
setWidth(element.clientWidth);
setHeight(element.clientHeight);
observer.observe(element);
return () => {
observer.unobserve(element);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [...additionalDeps, ref]);
return {
width,
height,
};
}1. When and why should you use it?
Virtual scrolling is useful when users browse a complete dataset continuously, much like infinite scrolling. Pagination is a better fit when you need to:
- Reduce database load for large datasets: Fetch only 10–50 records at a time from the backend with
LIMIT / OFFSET, reducing network costs. - Provide a clear position in the result set: Let users jump directly to a known location, such as “the fifth item on page 3.”
- Support printing and reports: Work with documents that must be printed or reviewed one page at a time.
2. Complete example: server-side pagination
The following example uses an asynchronous API simulation to refresh the data whenever the page changes:
import React, { useState, useEffect } from 'react';
import { BGrid, type BGridColumn, type BGridDataItem } from 'beautiful-grid';
interface MemberItem {
id: number;
email: string;
name: string;
joinDate: string;
status: 'ACTIVE' | 'DORMANT' | 'BLOCKED';
}
export default function MemberPaginationGrid() {
const [currentPage, setCurrentPage] = useState(1); // 1-based page number
const pageSize = 10;
const totalElements = 145; // 145 records in total
const [loading, setLoading] = useState(false);
const [data, setData] = useState<BGridDataItem<MemberItem>[]>([]);
// Simulate loading data when the page changes
useEffect(() => {
setLoading(true);
const timer = setTimeout(() => {
const startIdx = (currentPage - 1) * pageSize;
const mockItems: BGridDataItem<MemberItem>[] = Array.from({ length: pageSize }).map((_, i) => {
const itemIndex = startIdx + i + 1;
return {
values: {
id: itemIndex,
email: `user_${itemIndex}@example.com`,
name: `User_${itemIndex}`,
joinDate: '2026-08-01',
status: itemIndex % 5 === 0 ? 'DORMANT' : 'ACTIVE',
},
};
});
setData(mockItems);
setLoading(false);
}, 200);
return () => clearTimeout(timer);
}, [currentPage]);
const columns: BGridColumn<MemberItem>[] = [
{ key: 'id', label: 'Member ID', width: 90, align: 'center' },
{ key: 'name', label: 'Member Name', width: 140, align: 'center' },
{ key: 'email', label: 'Email Address', width: 250 },
{ key: 'joinDate', label: 'Joined At', width: 130, align: 'center' },
{
key: 'status',
label: 'Status',
width: 100,
align: 'center',
itemRender: ({ values }) => (
<span style={{ color: values.status === 'ACTIVE' ? '#16a34a' : '#d97706', fontWeight: 600 }}>
{values.status === 'ACTIVE' ? 'Active' : 'Dormant'}
</span>
),
},
];
return (
<div>
<BGrid<MemberItem>
width={750}
height={360}
columns={columns}
data={data}
rowKey="id"
loading={loading}
// Pagination configuration
page={{
currentPage,
pageSize,
totalElements,
totalPages: Math.ceil(totalElements / pageSize),
loading,
onChange: (newPage: number) => {
console.log(`Go to page ${newPage}`);
setCurrentPage(newPage);
},
}}
bottomBarHeight={36} // Height of the pagination bar
headerHeight={34}
itemHeight={28}
/>
</div>
);
}
3. page property reference
interface BGridPage {
// Current page number (starts at 1)
currentPage?: number;
// Number of rows per page
pageSize?: number;
// Total number of pages; required to render page-number controls
totalPages?: number;
// Total number of records on the server
totalElements?: number;
// Whether page data is loading
loading?: boolean;
// Called when the user clicks a page number or the Previous/Next button
onChange?: (newPage: number, pageSize?: number) => void;
}
4. Practical tips and gotchas
[!IMPORTANT] 1-based page numbering: The built-in pagination UI treats the first page as
1. If your backend API expects a zero-based page index, sendcurrentPage - 1in the request and convert the response back topageIndex + 1for UI state. Provide bothcurrentPageandtotalPagesto display the page-number controls.