كيفية بناء مكوّن ترقيم صفحات مخصّص في React باحترافية

دقائق القراءة: 9

مقدمة: لماذا نحتاج إلى ترقيم الصفحات في تطبيقات React؟

عند تطوير تطبيقات ويب حديثة، نحتاج كثيراً إلى جلب كميات كبيرة من البيانات من الخادم عبر واجهات API ثم عرضها للمستخدم. يظهر هذا السيناريو في منصات التواصل الاجتماعي عند عرض المنشورات والتعليقات، وفي لوحات الموارد البشرية عند استعراض طلبات التوظيف، وكذلك في تطبيقات البريد الإلكتروني عند إظهار الرسائل.

لكن عرض جميع البيانات دفعة واحدة يؤدي غالباً إلى إبطاء الصفحة بسبب الزيادة الكبيرة في عناصر DOM التي يجب على المتصفح معالجتها. لذلك يصبح من الضروري استخدام تقنيات تقلّل الحمل على الواجهة وتحافظ على سلاسة الأداء.

من أشهر هذه التقنيات:

  • التمرير اللانهائي Infinite Scroll.
  • الافتراضية Virtualization.
  • ترقيم الصفحات Pagination.

يُعد ترقيم الصفحات مناسباً عندما يكون حجم البيانات معروفاً مسبقاً، ولا تحدث عليه تغييرات مستمرة مثل الإضافة أو الحذف بشكل متكرر. لذلك قد لا يكون مثالياً في منصات تنشر محتوى جديداً كل لحظة، لكنه ممتاز في لوحات التحكم، والجداول، وصفحات التقارير، والواجهات التي تتطلب فرزاً وتصفية منظمة للبيانات.

في هذا المقال سنبني مكوّناً مخصّصاً ومداراً بالكامل لترقيم الصفحات في React، كما سننشئ دالة ربط مخصّصة Hook باسم usePagination تتولى حساب نطاق الصفحات المعروض بطريقة مرنة وقابلة لإعادة الاستخدام.

واجهة توضيحية لبناء مكوّن ترقيم صفحات مخصص في React لتحسين عرض البيانات الكبيرةعرض متحرك يوضح شكل مكوّن Pagination مخصص داخل تطبيق React

إعداد مشروع 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: صنف اختياري لتخصيص التنسيق الخارجي للمكوّن.

رسم توضيحي يشرح مفهوم siblingCount في مكوّن Pagination داخل React

مكوّن Pagination سيعتمد على Hook مخصّص باسم usePagination، والذي سيستقبل القيم التالية لحساب نطاق الصفحات:

  • totalCount
  • currentPage
  • pageSize
  • siblingCount

بناء 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.

الحالات الأساسية التي يجب دعمها

مكوّن الترقيم لا يعرض الصفحات بالطريقة نفسها دائماً. بل تتغير حالته بحسب إجمالي الصفحات وموقع الصفحة الحالية. لدينا أربع حالات منطقية رئيسية:

  1. عدد الصفحات الكلي أقل من عدد الأزرار المطلوب عرضها.
  2. وجود نقاط في الجهة اليمنى فقط.
  3. وجود نقاط في الجهة اليسرى فقط.
  4. وجود نقاط في الجهتين اليمنى واليسرى.

الحالات المختلفة لمكوّن Pagination مع ظهور النقاط يميناً ويساراً في React

حساب عدد الصفحات الكلي

أول خطوة هي حساب عدد الصفحات بناءً على العدد الكلي للعناصر 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>
    </>
  );
}

جدول بيانات داخل React قبل إضافة مكوّن Pagination المخصص

الخطوة التالية هي:

  1. إدارة حالة الصفحة الحالية باستخدام useState.
  2. حساب العناصر التي تخص الصفحة الحالية فقط باستخدام useMemo.
  3. تمرير القيم المناسبة إلى مكوّن 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 لفصل منطق الحساب عن واجهة العرض يُعد قراراً تقنياً ممتازاً لأنه يحسن قابلية الصيانة والاختبار وإعادة الاستخدام. وإذا كان تطبيقك يعرض بيانات معروفة الحجم نسبياً، فإن ترقيم الصفحات يبقى من أكثر الحلول توازناً بين الأداء، والوضوح، وتجربة المستخدم.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *