كيفية بناء مكوّن ترقيم صفحات مخصّص في React باحترافية
مقدمة: لماذا نحتاج إلى ترقيم الصفحات في تطبيقات React؟
عند تطوير تطبيقات ويب حديثة، نحتاج كثيراً إلى جلب كميات كبيرة من البيانات من الخادم عبر واجهات API ثم عرضها للمستخدم. يظهر هذا السيناريو في منصات التواصل الاجتماعي عند عرض المنشورات والتعليقات، وفي لوحات الموارد البشرية عند استعراض طلبات التوظيف، وكذلك في تطبيقات البريد الإلكتروني عند إظهار الرسائل.
لكن عرض جميع البيانات دفعة واحدة يؤدي غالباً إلى إبطاء الصفحة بسبب الزيادة الكبيرة في عناصر DOM التي يجب على المتصفح معالجتها. لذلك يصبح من الضروري استخدام تقنيات تقلّل الحمل على الواجهة وتحافظ على سلاسة الأداء.
من أشهر هذه التقنيات:
- التمرير اللانهائي
Infinite Scroll. - الافتراضية
Virtualization. - ترقيم الصفحات
Pagination.
يُعد ترقيم الصفحات مناسباً عندما يكون حجم البيانات معروفاً مسبقاً، ولا تحدث عليه تغييرات مستمرة مثل الإضافة أو الحذف بشكل متكرر. لذلك قد لا يكون مثالياً في منصات تنشر محتوى جديداً كل لحظة، لكنه ممتاز في لوحات التحكم، والجداول، وصفحات التقارير، والواجهات التي تتطلب فرزاً وتصفية منظمة للبيانات.
في هذا المقال سنبني مكوّناً مخصّصاً ومداراً بالكامل لترقيم الصفحات في React، كما سننشئ دالة ربط مخصّصة Hook باسم usePagination تتولى حساب نطاق الصفحات المعروض بطريقة مرنة وقابلة لإعادة الاستخدام.


إعداد مشروع React من البداية
إذا كنت معتاداً على إنشاء مشاريع React، يمكنك تجاوز هذا القسم. أما إذا أردت بداية سريعة، فيمكنك استخدام أداة create-react-app لإنشاء المشروع.
لتثبيت الأداة عالمياً باستخدام npm أو yarn:
npm install -g create-react-app
yarn global add create-react-app
بعد ذلك أنشئ مشروعاً جديداً بالأمر التالي:
npx create-react-app react-pagination
سنحتاج أيضاً إلى مكتبة classnames لأنها تسهّل التعامل مع الأصناف الشرطية classNames داخل المكوّنات.
npm install classnames
yarn add classnames
ثم شغّل المشروع:
yarn start
تصميم واجهة المكوّن وتحديد Props
قبل كتابة المنطق البرمجي، يجب تحديد المدخلات التي يحتاجها مكوّن Pagination. هذا التنظيم يسهّل إعادة استخدامه في أكثر من صفحة أو مشروع.
القيم الأساسية التي سنمررها إلى المكوّن هي:
totalCount: العدد الكلي للعناصر القادمة من مصدر البيانات.currentPage: رقم الصفحة الحالية، وسنعتمد فهرسة تبدأ من1بدلاً من0.pageSize: عدد العناصر التي ستظهر في كل صفحة.onPageChange: دالةcallbackتُستدعى عند تغيير الصفحة.siblingCount: عدد أزرار الصفحات التي تظهر على يمين ويسار الصفحة الحالية، والقيمة الافتراضية هي1.className: صنف اختياري لتخصيص التنسيق الخارجي للمكوّن.

مكوّن Pagination سيعتمد على Hook مخصّص باسم usePagination، والذي سيستقبل القيم التالية لحساب نطاق الصفحات:
totalCountcurrentPagepageSizesiblingCount
بناء Hook مخصص باسم usePagination
ما الذي يجب أن يفعله usePagination؟
هذا الـ Hook هو قلب المكوّن. مهمته الأساسية أن يعيد مصفوفة تحتوي على الصفحات التي يجب عرضها، بما في ذلك الفواصل المنطقية مثل النقاط ... عندما يكون عدد الصفحات كبيراً.
هناك عدة شروط مهمة يجب مراعاتها:
- يجب أن يُرجع نطاق أرقام جاهزاً للعرض.
- يجب إعادة الحساب عند تغيّر أي من
currentPageأوpageSizeأوsiblingCountأوtotalCount. - يفضّل أن يظل عدد العناصر المعادة ثابتاً قدر الإمكان حتى لا يتغيّر عرض المكوّن بصرياً أثناء تنقّل المستخدم.
أنشئ ملفاً باسم usePagination.js داخل مجلد src، ثم ابدأ بهذا الهيكل الأولي:
export const usePagination = ({ totalCount, pageSize, siblingCount = 1, currentPage }) => {
const paginationRange = useMemo(() => {
// منطق الحساب هنا
}, [totalCount, pageSize, siblingCount, currentPage]);
return paginationRange;
};
نستخدم هنا useMemo حتى لا يُعاد تنفيذ الحساب إلا عند تغيّر القيم المؤثرة فعلاً، وهذا أفضل للأداء من إعادة الحساب في كل عملية إعادة رسم render.
الحالات الأساسية التي يجب دعمها
مكوّن الترقيم لا يعرض الصفحات بالطريقة نفسها دائماً. بل تتغير حالته بحسب إجمالي الصفحات وموقع الصفحة الحالية. لدينا أربع حالات منطقية رئيسية:
- عدد الصفحات الكلي أقل من عدد الأزرار المطلوب عرضها.
- وجود نقاط في الجهة اليمنى فقط.
- وجود نقاط في الجهة اليسرى فقط.
- وجود نقاط في الجهتين اليمنى واليسرى.

حساب عدد الصفحات الكلي
أول خطوة هي حساب عدد الصفحات بناءً على العدد الكلي للعناصر totalCount وحجم الصفحة pageSize:
const totalPageCount = Math.ceil(totalCount / pageSize);
استخدام Math.ceil مهم جداً لأنه يضمن تخصيص صفحة إضافية عند وجود عناصر متبقية لا تكوّن صفحة مكتملة.
إنشاء دالة range() المساعدة
سنحتاج إلى دالة بسيطة تُرجع مصفوفة أرقام من قيمة بداية إلى قيمة نهاية:
const range = (start, end) => {
let length = end - start + 1;
return Array.from({ length }, (_, idx) => idx + start);
};
هذه الدالة ستفيدنا في توليد أزرار الصفحات بشكل مباشر دون كتابة حلقات يدوية في كل مرة.
التنفيذ الكامل لـ usePagination
export const DOTS = '...';
const range = (start, end) => {
let length = end - start + 1;
return Array.from({ length }, (_, idx) => idx + start);
};
export const usePagination = ({ totalCount, pageSize, siblingCount = 1, currentPage }) => {
const paginationRange = useMemo(() => {
const totalPageCount = Math.ceil(totalCount / pageSize);
// عدد العناصر الظاهرة = siblingCount + الصفحة الأولى + الأخيرة + الحالية + نقطتان
const totalPageNumbers = siblingCount + 5;
if (totalPageNumbers >= totalPageCount) {
return range(1, totalPageCount);
}
const leftSiblingIndex = Math.max(currentPage - siblingCount, 1);
const rightSiblingIndex = Math.min(currentPage + siblingCount, totalPageCount);
const shouldShowLeftDots = leftSiblingIndex > 2;
const shouldShowRightDots = rightSiblingIndex < totalPageCount - 2;
const firstPageIndex = 1;
const lastPageIndex = totalPageCount;
if (!shouldShowLeftDots && shouldShowRightDots) {
let leftItemCount = 3 + 2 * siblingCount;
let leftRange = range(1, leftItemCount);
return [...leftRange, DOTS, totalPageCount];
}
if (shouldShowLeftDots && !shouldShowRightDots) {
let rightItemCount = 3 + 2 * siblingCount;
let rightRange = range(totalPageCount - rightItemCount + 1, totalPageCount);
return [firstPageIndex, DOTS, ...rightRange];
}
if (shouldShowLeftDots && shouldShowRightDots) {
let middleRange = range(leftSiblingIndex, rightSiblingIndex);
return [firstPageIndex, DOTS, ...middleRange, DOTS, lastPageIndex];
}
}, [totalCount, pageSize, siblingCount, currentPage]);
return paginationRange;
};
الفكرة هنا بسيطة وفعالة في الوقت نفسه: نحدد أولاً الأرقام الواجب عرضها، ثم ندرج الفواصل DOTS عندما توجد أجزاء مخفية من النطاق. بهذه الطريقة نحصل على مكوّن واضح، متناسق، وسهل التوسع لاحقاً.
بناء مكوّن Pagination نفسه
بعد الانتهاء من منطق الحساب، نأتي إلى طبقة العرض. هذا المكوّن سيستهلك usePagination ويحوّل الناتج إلى أزرار فعلية يمكن للمستخدم التفاعل معها.
أنشئ ملفاً باسم Pagination.js داخل مجلد src، ثم استخدم الكود التالي:
import React from 'react';
import classnames from 'classnames';
import { usePagination, DOTS } from './usePagination';
import './pagination.scss';
const Pagination = props => {
const {
onPageChange,
totalCount,
siblingCount = 1,
currentPage,
pageSize,
className
} = props;
const paginationRange = usePagination({
currentPage,
totalCount,
siblingCount,
pageSize
});
if (currentPage === 0 || paginationRange.length < 2) {
return null;
}
const onNext = () => {
onPageChange(currentPage + 1);
};
const onPrevious = () => {
onPageChange(currentPage - 1);
};
let lastPage = paginationRange[paginationRange.length - 1];
return (
<ul className={classnames('pagination-container', { [className]: className })}>
<li
className={classnames('pagination-item', {
disabled: currentPage === 1
})}
onClick={onPrevious}
>
<div className="arrow left" />
</li>
{paginationRange.map(pageNumber => {
if (pageNumber === DOTS) {
return <li className="pagination-item dots">…</li>;
}
return (
<li
className={classnames('pagination-item', {
selected: pageNumber === currentPage
})}
onClick={() => onPageChange(pageNumber)}
>
{pageNumber}
</li>
);
})}
<li
className={classnames('pagination-item', {
disabled: currentPage === lastPage
})}
onClick={onNext}
>
<div className="arrow right" />
</li>
</ul>
);
};
export default Pagination;
كيف يعمل هذا المكوّن؟
- إذا كان عدد الصفحات أقل من صفحتين، فلن يظهر المكوّن أساساً، وسيُرجع
null. - يعرض سهماً للانتقال إلى الصفحة السابقة، وسهماً آخر للصفحة التالية.
- يعرض أرقام الصفحات اعتماداً على النطاق العائد من
usePagination. - إذا صادف قيمة
DOTSفسيعرض علامة الحذف…بدلاً من رقم. - يضيف صنف
disabledإلى الأسهم عند أول صفحة أو آخر صفحة لمنع التنقل غير الصالح.
تنسيقات CSS لمكوّن ترقيم الصفحات
لإظهار المكوّن بشكل أنيق وقابل للاستخدام، يمكن اعتماد التنسيقات التالية:
.pagination-container {
display: flex;
list-style-type: none;
.pagination-item {
padding: 0 12px;
height: 32px;
text-align: center;
margin: auto 4px;
color: rgba(0, 0, 0, 0.87);
display: flex;
box-sizing: border-box;
align-items: center;
letter-spacing: 0.01071em;
border-radius: 16px;
line-height: 1.43;
font-size: 13px;
min-width: 32px;
&.dots:hover {
background-color: transparent;
cursor: default;
}
&:hover {
background-color: rgba(0, 0, 0, 0.04);
cursor: pointer;
}
&.selected {
background-color: rgba(0, 0, 0, 0.08);
}
.arrow {
&::before {
position: relative;
content: '';
display: inline-block;
width: 0.4em;
height: 0.4em;
border-right: 0.12em solid rgba(0, 0, 0, 0.87);
border-top: 0.12em solid rgba(0, 0, 0, 0.87);
}
&.left {
transform: rotate(-135deg) translate(-50%);
}
&.right {
transform: rotate(45deg);
}
}
&.disabled {
pointer-events: none;
.arrow::before {
border-right: 0.12em solid rgba(0, 0, 0, 0.43);
border-top: 0.12em solid rgba(0, 0, 0, 0.43);
}
&:hover {
background-color: transparent;
cursor: default;
}
}
}
}
هذه التنسيقات تمنحك أساساً جيداً لمكوّن بسيط وأنيق. ويمكنك لاحقاً تعديل الألوان، الأحجام، وحالات التفاعل بما يتماشى مع نظام التصميم في مشروعك.
مثال عملي: استخدام المكوّن مع جدول بيانات
بعد تجهيز المكوّن، نحتاج إلى دمجه في تطبيق حقيقي. سنبدأ بعرض بيانات ثابتة داخل جدول، ثم نربط هذا الجدول بترقيم الصفحات.
import React from 'react';
import data from './data/mock-data.json';
export default function App() {
return (
<>
<table>
<thead>
<tr>
<th>ID</th>
<th>FIRST NAME</th>
<th>LAST NAME</th>
<th>EMAIL</th>
<th>PHONE</th>
</tr>
</thead>
<tbody>
{data.map(item => {
return (
<tr>
<td>{item.id}</td>
<td>{item.first_name}</td>
<td>{item.last_name}</td>
<td>{item.email}</td>
<td>{item.phone}</td>
</tr>
);
})}
</tbody>
</table>
</>
);
}

الخطوة التالية هي:
- إدارة حالة الصفحة الحالية باستخدام
useState. - حساب العناصر التي تخص الصفحة الحالية فقط باستخدام
useMemo. - تمرير القيم المناسبة إلى مكوّن
Pagination.
سنثبت قيمة PageSize على 10 في هذا المثال، مع إمكانية تطوير الواجهة لاحقاً لإتاحة تغييرها من قبل المستخدم.
import React, { useState, useMemo } from 'react';
import Pagination from '../Pagination';
import data from './data/mock-data.json';
import './style.scss';
let PageSize = 10;
export default function App() {
const [currentPage, setCurrentPage] = useState(1);
const currentTableData = useMemo(() => {
const firstPageIndex = (currentPage - 1) * PageSize;
const lastPageIndex = firstPageIndex + PageSize;
return data.slice(firstPageIndex, lastPageIndex);
}, [currentPage]);
return (
<>
<table>
<thead>
<tr>
<th>ID</th>
<th>FIRST NAME</th>
<th>LAST NAME</th>
<th>EMAIL</th>
<th>PHONE</th>
</tr>
</thead>
<tbody>
{currentTableData.map(item => {
return (
<tr>
<td>{item.id}</td>
<td>{item.first_name}</td>
<td>{item.last_name}</td>
<td>{item.email}</td>
<td>{item.phone}</td>
</tr>
);
})}
</tbody>
</table>
<Pagination
className="pagination-bar"
currentPage={currentPage}
totalCount={data.length}
pageSize={PageSize}
onPageChange={page => setCurrentPage(page)}
/>
</>
);
}
أفضل ممارسات عند استخدام ترقيم الصفحات في React
1. افصل منطق الحساب عن واجهة العرض
استخدام Hook مثل usePagination يجعل الكود أكثر نظافة وأسهل للاختبار وإعادة الاستخدام.
2. استخدم useMemo عند الحاجة
عندما تكون لديك حسابات مرتبطة بعرض البيانات أو توليد النطاقات، فإن useMemo يساعد في تقليل العمليات غير الضرورية.
3. لا تعرض ترقيم الصفحات إذا لم تكن هناك حاجة فعلية
إذا كانت البيانات كلها تظهر في صفحة واحدة، فمن الأفضل إخفاء المكوّن حتى تبقى الواجهة أبسط.
4. احرص على تجربة استخدام واضحة
تعطيل أزرار التنقل غير المتاحة، وإبراز الصفحة الحالية، وتوفير مسافات مريحة بين العناصر كلها تفاصيل صغيرة لكنها تؤثر بقوة في سهولة الاستخدام.
5. راعِ قابلية التوسع
يمكنك لاحقاً تطوير هذا المكوّن لدعم:
- تغيير
pageSizeمن الواجهة. - الربط مع معاملات الرابط
URL Query Params. - الجلب من الخادم لكل صفحة على حدة
Server-side Pagination. - تحسين الوصول
Accessibilityعبر السمات المناسبة مثلaria-label.
مقارنة مختصرة بين ترقيم الصفحات والتمرير اللانهائي
| الأسلوب | متى يفضّل استخدامه | أبرز ميزة | أبرز قيد |
|---|---|---|---|
ترقيم الصفحات Pagination |
الجداول، التقارير، لوحات التحكم | تنقل منظم وسهل | أقل مرونة مع البيانات المتغيرة باستمرار |
التمرير اللانهائي Infinite Scroll |
الشبكات الاجتماعية وتدفقات المحتوى | تجربة تصفح مستمرة | أصعب في الوصول السريع إلى موضع محدد |
الافتراضية Virtualization |
القوائم الضخمة جداً | تحسين كبير في الأداء | يتطلب تنفيذاً أكثر تقدماً |
الخلاصة التقنية
بناء مكوّن Pagination مخصص في React يمنحك مرونة أكبر من الاعتماد الكامل على مكتبات جاهزة، خاصة عندما تحتاج إلى سلوك دقيق أو تصميم متوافق مع هوية مشروعك. استخدام usePagination لفصل منطق الحساب عن واجهة العرض يُعد قراراً تقنياً ممتازاً لأنه يحسن قابلية الصيانة والاختبار وإعادة الاستخدام. وإذا كان تطبيقك يعرض بيانات معروفة الحجم نسبياً، فإن ترقيم الصفحات يبقى من أكثر الحلول توازناً بين الأداء، والوضوح، وتجربة المستخدم.