Pagination

Place page-number navigation in the DataGrid footer and connect it to a server-side pagination API.

#pagination#page#server-side-paging#pageSize#totalElements#bottomBarHeight
Last reviewed: 2026-08-23
GitHub
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;

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:

  1. Reduce database load for large datasets: Fetch only 10–50 records at a time from the backend with LIMIT / OFFSET, reducing network costs.
  2. Provide a clear position in the result set: Let users jump directly to a known location, such as “the fifth item on page 3.”
  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, send currentPage - 1 in the request and convert the response back to pageIndex + 1 for UI state. Provide both currentPage and totalPages to display the page-number controls.