تخفیف ویژه برای همه دوره‌ها از شنبه

اسکرول بینهایت یک تکنیک قدرتمند است که تجربه کاربری را با بارگذاری داینامیک محتوا بهبود می‌بخشد. در این مقاله، سه روش مختلف برای پیاده‌سازی اسکرول بینهایت در React را بررسی خواهیم کرد که شامل راهکارهای سفارشی و مبتنی بر کتابخانه‌های آماده می‌شود.

رایج‌ترین روش برای پیاده‌سازی اسکرول بینهایت، استفاده از کتابخانه‌های آماده React مانند react-infinite-scroll-component و react-window-infinite-loader است:

import InfiniteScroll from "react-infinite-scroll-component";
import { useState, useEffect } from 'react'; 

function App() {
  const [page, setPage] = useState(1);
  const [products, setProducts] = useState([]);
  const [totalProducts, setTotalProducts] = useState(0);

  const fetchData = async (page: number) => {
    try {
      const res = await fetch(
        `https://dummyjson.com/products/?limit=10&skip=${(page - 1) * 10}`
      );
      const data = await res.json();
      if (res.ok) {
        setProducts((prevItems) => [...prevItems, ...data.products]);
        page === 1 && setTotalProducts(() => data.total);
      }
    } catch (error) {
      console.log(error)
    }
  };

  useEffect(() => {
    let subscribed = true;
    (async () => {
      if (subscribed) {
        await fetchData(1);
      }
    })();
    return () => {
      subscribed = false;
    };
  }, []);

  const handleLoadMoreData = () => {
    setPage((prevPage) => {
      const nextPage = prevPage + 1;
      fetchData(nextPage);
      return nextPage;
    });
  };
  
  return (
    <InfiniteScroll
      dataLength={products.length}
      next={handleLoadMoreData}
      hasMore={totalProducts > products.length}
      loader={<p>Loading...</p>}
      endMessage={<p>No more data to load.</p>}
    >
      <div>
        {products.map((product) => (
          <div key={product.id}>
            <h2>
              {product.title} - {product.id}
            </h2>
          </div>
        ))}
      </div>
    </InfiniteScroll>
  );
}

با این حال، روش‌های مختلفی برای ایجاد این افکت وجود دارد. در این مقاله، سه رویکرد منحصربه‌فرد برای پیاده‌سازی اسکرول بینهایت در اپلیکیشن‌های React را بررسی خواهیم کرد:

اسکرول بینهایت چیست؟

اسکرول بینهایت نیاز به صفحه‌بندی سنتی را از بین می‌برد. به جای اینکه کاربران بین صفحات مختلف جابه‌جا شوند، می‌توانند بدون توقف اسکرول کنند و محتوای بیشتری ببینند، که این کار تجربه‌ای جذاب‌تر و شهودی‌تر را فراهم می‌کند.

این تکنیک به طور گسترده در پلتفرم‌های شبکه اجتماعی مانند اینستاگرام، X و تیک‌تاک استفاده می‌شود تا کاربران بتوانند بدون وقفه فیدهای پر از تصاویر و ویدیوها را مرور کنند.

اکنون که با مفهوم اسکرول بینهایت آشنا شدیم، وارد بخش آموزش می‌شویم.

تنظیمات اولیه اپلیکیشن React

در ابتدا، یک پایه مشترک را ایجاد می‌کنیم که در تمام روش‌های مختلف پیاده‌سازی اسکرول بینهایت استفاده خواهد شد.

راه‌اندازی اپلیکیشن React

برای شروع، ابتدا یک اپلیکیشن React را با استفاده از Vite راه‌اندازی می‌کنیم. برای این کار، دستورات زیر را در ترمینال اجرا می‌نماییم:

npm create vite@latest ecommerce-app -- --template react-ts
cd ecommerce-app
npm i

دستورات بالا یک اپلیکیشن React با تایپ اسکریپت ایجاد کرده و تمام وابستگی‌های لازم را نصب می‌کنند. در مرحله بعد، در فایل App.tsx state اولیه کامپوننت را تنظیم می‌کنیم. این شامل لیست آیتم‌هایی که باید نمایش داده شوند، نشانگرهای بارگذاری و خطا، و یک متغیر برای پیگیری تعداد کل محصولات موجود خواهد بود.

// App.tsx
import React, { useState, useEffect } from 'react';

type ProductItem = {
  id: number;
  title: string;
  description: string;
  category: string;
  price: number;
  rating: number;
  thumbnail: string;
  brand: string;
  discountPercentage: number;
};

function App() {
  const [loading, setLoading] = useState(true);
  const [products, setProducts] = useState<ProductItem[]>([]);
  const [totalProducts, setTotalProducts] = useState(0);
  const [error, setError] = useState<null|Error>(null);
  // rest of component
}

بارگذاری داده‌ها

در مرحله بعد، یک تابع برای دریافت داده‌ها از یک API ایجاد می‌کنیم، شماره صفحه را افزایش می‌دهیم و state را با آیتم‌های دریافت‌شده به‌روزرسانی می‌کنیم. علاوه بر این، هنگام دریافت داده‌ها، هرگونه خطا را نیز مدیریت خواهیم کرد:

const fetchData = async (page: number) => {
  try {
    setLoading(true);
    const res = await fetch(
      `https://dummyjson.com/products/?limit=10&skip=${(page - 1) * 10}`
    );
    const data = await res.json();
    if (res.ok) {
      setProducts((prevItems) => [...prevItems, ...data.products]);
      page === 1 && setTotalProducts(() => data.total); // only set this once
    }
    setLoading(false);
  } catch (error) {
    setLoading(false);
    if (error instanceof Error) {
      setError(error);
    }
  }
};

استفاده از API محصولات DummyJSON

در این مقاله، از API محصولات DummyJSON استفاده خواهیم کرد. این API پارامتر page صریحی برای صفحه‌بندی ندارد. در عوض، از دو پارامتر limit و skip برای صفحه‌بندی استفاده می‌کند.

فراخوانی fetchData هنگام بارگذاری کامپوننت

سپس از هوک useEffect برای فراخوانی تابع fetchData در زمان بارگذاری اولیه کامپوننت استفاده خواهیم کرد:

useEffect(() => {
  let subscribed = true;
  (async () => {
    if (subscribed) {
      await fetchData(1);
    }
  })();

  return () => {
    subscribed = false;
  };
}, []);

درون useEffect، باید مطمئن شویم که درخواست‌های asynchronous را پاک‌سازی می‌کنیم تا از به‌روزرسانی state پس از آن که کامپوننت از بین رفت، جلوگیری شود. یکی از روش‌های لغو درخواست fetch، استفاده از AbortController است.

کامپوننت ProductCard

در نهایت، کامپوننت ProductCard را ایجاد می‌کنیم که برای نمایش هر محصول استفاده خواهد شد و استایل‌های CSS مربوط به آن را نیز تعریف می‌کنیم. برای این کار، یک پوشه component درون مسیر src ایجاد کرده و فایل ProductCard.tsx را درون آن می‌سازیم. سپس کد زیر را در آن قرار می‌دهیم:

export const ProductCard = ({ product }: { product: ProductItem }) => {
  const discountedPrice = (
    product.price -
    (product.price * product.discountPercentage) / 100
  ).toFixed(2);

  return (
    <div className="product-card">
      <img
        src={product.thumbnail}
        alt={product.title}
        className="product-image"
      />
      <div className="product-info">
        <h2 className="product-title">
          {product.title} - {product.id}
        </h2>
        <span className="product-category">{product.category}</span>
        {product.brand && (
          <span className="product-brand">{product.brand}</span>
        )}
        <p className="product-description">{product.description}</p>
        <div className="product-props">
          <div className="product-price">
            ${discountedPrice}
            <span className="product-original-price">
              ${product.price.toFixed(2)}
            </span>
          </div>
          <div className="product-rating">
            <span className="star-rating">{"★"}</span>
            <span>{Math.floor(product.rating)}</span>
          </div>
        </div>
        <button className="add-to-cart">Add to Cart</button>
      </div>
    </div>
  );
};

قالب Vite که برای ساخت اپلیکیشن React استفاده کردیم، به‌طور پیش‌فرض دو فایل CSS دارد، اما ما فقط به یکی از آن‌ها نیاز داریم.

@import url("https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap");

.App {
  font-family: "Poppins", sans-serif;
}
.products-list {
  display: grid;
  grid-template-columns: 1fr;
  grid-gap: 10px;
  max-width: 768px;
  margin: 0 auto;
}
@media screen and (min-width: 768px) {
  .products-list {
    grid-template-columns: 1fr 1fr;
  }
}
.product-card {
  background-color: white;
  border-radius: 8px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
  max-width: 400px;
  width: 100%;
  overflow: hidden;
  font-family: "Poppins", sans-serif;
}
.product-image {
  width: 100%;
  height: 250px;
  object-fit: cover;
}
.product-info {
  padding: 20px;
}
.product-title {
  font-size: 24px;
  margin: 0 0 10px;
}
.product-category,
.product-brand {
  display: inline-block;
  background-color: #e0e0e0;
  padding: 5px 10px;
  border-radius: 15px;
  font-size: 12px;
  margin-right: 5px;
}
.product-description {
  font-size: 14px;
  color: #666;
  margin: 10px 0;
}
.product-props {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.product-price {
  font-size: 24px;
  font-weight: bold;
  margin: 10px 0;
}
.product-original-price {
  text-decoration: line-through;
  color: #999;
  font-size: 16px;
  margin-left: 10px;
}
.product-rating {
  display: flex;
  align-items: center;
  margin: 10px 0;
}
.star-rating {
  color: #ffd700;
  font-size: 18px;
  margin-right: 5px;
}
.add-to-cart {
  display: block;
  width: 100%;
  padding: 10px;
  background-color: #4caf50;
  color: white;
  border: none;
  border-radius: 4px;
  font-size: 16px;
  cursor: pointer;
  margin-top: 20px;
}
.add-to-cart:hover {
  background-color: #45a049;
}

این مراحل پایه در تمام روش‌هایی که در این مقاله بررسی خواهیم کرد، وجود دارند. ما آن‌ها را اصلاح کرده و به عنوان پایه‌ای برای پیاده‌سازی‌های مختلف توسعه خواهیم داد.

سه روش برای پیاده‌سازی اسکرول بینهایت در React

۱-  پیاده‌سازی کامل از صفر

در این روش، ما کل مکانیزم اسکرول بینهایت را از ابتدا می‌سازیم. این روش شامل کنترل event scroll، بارگذاری داده‌های بیشتر و به‌روزرسانی state در برنامه React است. این روش امکان کنترل کامل بر سفارشی‌سازی و عملکرد را به ما می‌دهد.

برای شروع، یک کامپوننت به نام FromScratch.tsx در دایرکتوری components ایجاد کرده و مقدار اولیه آن را تنظیم می‌کنیم.

import { useEffect, useState } from "react";
import { ProductCard } from "./ProductCard";
import { ProductItem } from "../types";

export const FromScratch = ({
  products,
  fetchData,
  loading,
  error
}: {
  products: ProductItem[];
  fetchData: (page: number) => Promise;
  loading: boolean;
  error: null|Error
}) => {
  const [page, setPage] = useState(1);
  
  // scroll logic  

  return (
    <div>
      <div className="products-list">
        {products.map((product, index) => (
          <ProductCard product={product} key={index} />
        ))}
      </div>
      {loading && <p>Loading...</p>}
      {error && <p>Error: {error.message}</p>}
    </div>
  );
};

کنترل event scroll

سپس، یک تابع برای مدیریت event scroll می‌سازیم. این تابع بررسی می‌کند که آیا کاربر به انتهای صفحه رسیده است یا نه؛ و در صورت نیاز، تابع fetchData را برای دریافت داده‌های بیشتر فراخوانی می‌کند. همچنین، یک event listener برای event scroll روی window اضافه می‌کنیم و هنگام unmount شدن کامپوننت، آن را حذف می‌نماییم. در کدی که داشتیم، به جای کامنت // scroll logic کد زیر را اضافه می‌کنیم:

const handleScroll = () => {
  const bottom =
    Math.ceil(window.innerHeight + window.scrollY) >=
    document.documentElement.scrollHeight - 200;
  if (bottom) {
    setPage((prevPage) => {
      const nextPage = prevPage + 1;
      fetchData(nextPage);
      return nextPage;
    });
  }
};

useEffect(() => {
  window.addEventListener("scroll", handleScroll);
  return () => {
    window.removeEventListener("scroll", handleScroll);
  };
}, []);

دلیل استفاده از fetchData در setPage

سوالی که ممکن است در این مرحله پیش بیاید این است که چرا fetchData در داخل callback setPage قرار گرفته است. دلیل این اتفاق این است که fetchData به مقدار به‌روز state صفحه نیاز دارد.

به‌روزرسانی state در React به‌صورت asynchronous انجام می‌شود، بنابراین اگر بلافاصله بعد از setState به مقدار جدید state وابسته باشیم، ممکن است دچار race condition شویم (race condition زمانی رخ می‌دهد که دو فرآیند سعی می‌کنند به‌طور هم‌زمان به یک منبع دسترسی پیدا کنند). راه دیگر این است که از هوک useEffect برای دریافت داده‌ها هنگام تغییر page استفاده کنیم، اما در این مورد، این امکان را نداریم.

اکنون، کامپوننتی که ساختیم را در App.tsx ایمپورت کرده و اپلیکیشن را اجرا می‌کنیم:

// App.tsx
<div>
  <FromScratch
    products={products}
    fetchData={fetchData}
    loading={loading}
    error={error}
  />
</div>

برای اجرای برنامه می‌توانیم از دستور زیر استفاده کنیم:

npm run dev

پس از اجرای برنامه، می‌توانیم خروجی آن را در مرورگر مشاهده کنیم. اگر پروژه را هم‌زمان با این آموزش ایجاد نمی‌کنید، می‌توانید از نمونه کد Codesandbox استفاده نمایید.

Debouncing

تابع handleScroll در هر بار اسکرول کاربر، بررسی می‌کند که آیا به انتهای صفحه رسیده یا نه، اما این کار بهینه نیست. در عوض، بهتر است فقط زمانی که کاربر اسکرول را متوقف کرد، بررسی انجام شود. برای این کار، از Debouncing استفاده می‌کنیم که یک وقفه ایجاد می‌کند و فقط بعد از متوقف شدن اسکرول، تابع بررسی اجرا می‌شود:

// FromScratch.tsx
const debounce = (func: (args: any) => void, delay: number) => {
  let timeoutId: ReturnType<typeof setTimeout>;

  return function (...args: any) {
    if (timeoutId) {
      clearTimeout(timeoutId);
    }

    timeoutId = setTimeout(() => {
      func(args);
    }, delay);
  };
};

سپس، تابع handleScroll را به‌روزرسانی می‌کنیم تا از یک تابع debounce استفاده کند.

const handleScroll = debounce(() => {
  const bottom =
    Math.ceil(window.innerHeight + window.scrollY) >=
    document.documentElement.scrollHeight - 200;

  if (bottom) {
    setPage((prevPage) => {
      const nextPage = prevPage + 1;
      fetchData(nextPage);
      return nextPage;
    });
  }
}, ۳۰۰);

هدف از پاک کردن timeout درون تابع debounce این است که هر بار که کاربر دوباره اسکرول را شروع کند، تایمر قبلی لغو شود و تابع بررسی اسکرول بی‌دلیل اجرا نشود.

ساخت یک هوک قابل استفاده مجدد

در اپلیکیشن‌های بزرگ، معمولاً اسکرول بینهایت را در صفحات مختلف نیاز خواهیم داشت. بنابراین، در این بخش، تابع اسکرول را به‌صورت یک هوک سفارشی با قابلیت استفاده مجدد پیاده‌سازی می‌کنیم.

برای این کار:

import { useEffect, useState } from "react";
const debounce = (func: (args: any) => void, delay: number) => {
  let timeoutId: ReturnType<typeof setTimeout>;
  return function (...args: any) {
    if (timeoutId) {
      clearTimeout(timeoutId);
    }
    timeoutId = setTimeout(() => {
      func(args);
    }, delay);
  };
};

export const useInfiniteScroll = (fetchData: (page: number) => Promise<void>) => {
  const [page, setPage] = useState(1);
  
  const handleScroll = debounce(() => {
    const bottom =
      Math.ceil(window.innerHeight + window.scrollY) >=
      document.documentElement.scrollHeight - 200;
    if (bottom) {
      setPage((prevPage) => {
        const nextPage = prevPage + 1;
        fetchData(nextPage);
        return nextPage;
      });
    }
  }, ۳۰۰);

  useEffect(() => {
    window.addEventListener("scroll", handleScroll);
    return () => {
      window.removeEventListener("scroll", handleScroll);
    };
  }, []);
};

حال در FromScratch.tsx، منطق اسکرول را با این هوک جدید جایگزین می‌کنیم.

export const FromScratch = ({
  products,
  fetchData,
  loading,
  error,
}: {
  products: ProductItem[];
  fetchData: (page: number) => Promise<void>;
  loading: boolean;
  error: null | Error;
}) => {
  useInfiniteScroll(fetchData);

  // rest of component
};

با این روش، یک پیاده‌سازی کاملاً سفارشی و قابل توسعه برای اسکرول بینهایت در React داریم. این روش امکان کنترل کامل و سفارشی‌سازی بالا را فراهم می‌کند، اما در عین حال، زمان‌برتر است و نسبت به استفاده از کتابخانه‌های آماده، به نگه‌داری بیشتری نیاز دارد.

۲- استفاده از کتابخانه‌های موجود برای اسکرول بینهایت

استفاده از یک کتابخانه آماده برای اسکرول بینهایت باعث صرفه‌جویی در زمان و انرژی می‌شود، زیرا از راهکارهای آماده و تست شده بهره می‌بریم و در عین حال امکان سفارشی‌سازی را نیز خواهیم داشت. در این بخش، دو مورد از این کتابخانه‌ها را بررسی می‌کنیم.

react-infinite-scroll-component

یکی از محبوب‌ترین کتابخانه‌ها برای پیاده‌سازی اسکرول بینهایت در React، کتابخانه react-infinite-scroll-component است. در اینجا یاد می‌گیریم که چگونه از این کتابخانه برای ایجاد اسکرول بینهایت در یک فروشگاه اینترنتی استفاده کنیم.

ابتدا react-infinite-scroll-component را نصب می‌نماییم:

npm install react-infinite-scroll-component

اکنون، یک کامپوننت جدید در دایرکتوری components ایجاد می‌کنیم و آن را WithReactScroll.tsx می‌نامیم. در این کامپوننت:

import InfiniteScroll from "react-infinite-scroll-component";
import { ProductCard } from "./ProductCard";
import { useState } from "react";

export const WithReactScroll = ({
  products,
  fetchData,
  totalProducts,
}: {
  products: ProductItem[];
  fetchData: (page: number) => Promise<void>;
  totalProducts: number;
}) => {
  const [page, setPage] = useState(1);

  const handleLoadMoreData = () => {
    setPage((prevPage) => {
      const nextPage = prevPage + 1;
      fetchData(nextPage);
      return nextPage;
    });
  };

  return (
    <InfiniteScroll
      dataLength={products.length}
      next={handleLoadMoreData}
      hasMore={totalProducts > products.length}
      loader={<p>Loading...</p>}
      endMessage={<p>No more data to load.</p>}
    >
      <div className="products-list">
        {products.map((item) => (
          <ProductCard product={item} key={item.id} />
        ))}
      </div>
    </InfiniteScroll>
  );
};

اکنون می‌توانیم کامپوننت WithReactScroll را در App.tsx ایمپورت کرده و نتیجه را مشاهده کنیم. همچنین می‌توانید نتیجه را در Codesandbox نیز ببینیم.

<div>
  <WithReactScroll
    products={products}
    fetchData={fetchData}
    totalProducts={totalProducts}
  />
</div>
مزایا و معایب این روش

با این روش، توانستیم اسکرول بینهایت را در اپلیکیشن خود پیاده‌سازی کنیم، بدون اینکه نیاز باشد event scroll ویندوز را به‌صورت دستی مدیریت نماییم. کتابخانه react-infinite-scroll-component این کار را به طور خودکار برای ما انجام می‌دهد.

مزایا:

معایب:

استفاده از react-window-infinite-loader و react-window

دومین روش استفاده از کتابخانه react-window است که برای رندر کردن لیست‌های بزرگ بهینه شده است. همراه با این کتابخانه، از react-window-infinite-loader برای بارگذاری داده‌های بیشتر هنگام اسکرول استفاده می‌کنیم.

ابتدا react-window و react-window-infinite-loader را نصب می‌نماییم:

npm install react-window-infinite-loader react-window

سپس، یک کامپوننت جدید در دایرکتوری components با نام WithReactWindow ایجاد می‌کنیم.

import { useState } from "react";
import { FixedSizeList as List } from "react-window";
import InfiniteLoader from "react-window-infinite-loader";
import { ProductCard } from "./ProductCard";

export const WithReactWindow = ({
  fetchData,
  products,
  totalProducts,
  loading,
}: {
  products: ProductItem[];
  fetchData: (page: number) => Promise<void>;
  totalProducts: number;
  loading: boolean;
}) => {
  const [page, setPage] = useState(1);
  const hasNextPage = totalProducts > products.length;

  const handleLoadMoreData = () => {
    if (loading) return;
    setPage((prevPage) => {
      const nextPage = prevPage + 1;
      fetchData(nextPage);
      return nextPage;
    });
  };

  const isItemLoaded = (index: number) => !hasNextPage || index < products.length;

  const Row = ({ index, style }: { index: number, style: { [key:string]:any } }) => {
    return (
      <div style={style}>
        {isItemLoaded(index) ? (
          <ProductCard product={products[index]} />
        ) : (
          "Loading..."
        )}
      </div>
    );
  };

  return (
    <InfiniteLoader
      isItemLoaded={isItemLoaded}
      itemCount={hasNextPage ? products.length + 1 : products.length}
      loadMoreItems={handleLoadMoreData}
    >
      {({ onItemsRendered, ref }) => (
        <List
          height={window.innerHeight}
          itemCount={products.length}
          itemSize={600}
          onItemsRendered={onItemsRendered}
          ref={ref}
          width={450}
        >
          {Row}
        </List>
      )}
    </InfiniteLoader>
  );
};

در کد بالا، از ترکیب InfiniteLoader() و FixedSizeList() استفاده می‌کنیم. این ترکیب باعث می‌شود فقط آیتم‌های قابل مشاهده در صفحه رندر شوند. هنگام اسکرول، داده‌های جدید بارگذاری می‌شوند و ویژگی Infinite Scroll را فراهم می‌کند.

اکنون می‌توانیم این کامپوننت را در App ایمپورت کنیم تا نتیجه را مشاهده نماییم. همچنین می‌توانیم نتیجه را در Codesandbox ببینیم.

<div>
  <WithReactWindow
    products={products}
    fetchData={fetchData}
    totalProducts={totalProducts}
    loading={loading}
  />
</div>

۳- استفاده از Intersection Observer API

Intersection Observer API یک تکنیک مدرن توسعه است که می‌تواند تشخیص دهد چه زمانی المنت‌ها به نمایش در می‌آیند و در نتیجه باعث بارگذاری محتوا برای اسکرول بینهایت شود. این API تغییرات در تقاطع المنت هدف با یک المنت parent یا viewport را مشاهده می‌کند و آن را برای پیاده‌سازی اسکرول بینهایت ایده‌آل می‌سازد.

ما یک کامپوننت جدید در دایرکتوری components ایجاد می‌کنیم و آن را WithIntersectionObserver می‌نامیم. سپس، یک ref برای المنت هدف observer ایجاد کرده و Intersection Observer را در یک هوک از نوع useEffect تنظیم می‌کنیم. زمانی که المنت هدف به نمایش درآید، تابع fetchData را فراخوانی خواهیم کرد.

import { useEffect, useRef, useState } from "react";
import { ProductCard } from "./ProductCard";

export const WithIntersectionObserver = ({
  products,
  fetchData,
  error,
  loading
}: {
  products: ProductItem[];
  fetchData: (page: number) => Promise<void>;
  error: null|Error,
  loading: boolean
}) => {
  const [page, setPage] = useState(1);
  const observerTarget = useRef(null);

  useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        if (entries[0].isIntersecting) {
          setPage((prevPage) => {
            const nextPage = prevPage + 1;
            fetchData(nextPage);
            return nextPage;
          });
        }
      },
      { threshold: 1 }
    );
    if (observerTarget.current) {
      observer.observe(observerTarget.current);
    }
    return () => {
      if (observerTarget.current) {
        observer.unobserve(observerTarget.current);
      }
    };
  }, [observerTarget]);

  // rest of component
};

سپس، آیتم‌ها، نشانگر بارگذاری، پیام‌های خطا و المنت هدف observer را درون کامپوننت رندر می‌کنیم:

return (
  <>
    <div className="products-list">
      {products.map((product) => (
        <ProductCard product={product} key={product.id} />
      ))}
    </div>
    <div ref={observerTarget}></div>
    {loading && <p>Loading...</p>}
    {error && <p>Error: {error.message}</p>}
  </>
);

با استفاده از Intersection Observer API، ما یک راه‌حل اسکرول بینهایت کارآمد و با عملکرد بهینه در برنامه React خود ایجاد کرده‌ایم. این روش یک روش مدرن و browser-native برای تشخیص نمایش المنت‌ها است، اما ممکن است در مرورگرهای قدیمی بدون استفاده از polyfill پشتیبانی نشود.

سپس، این کامپوننت جدید را در کامپوننت App ایمپورت می‌کنیم تا نتیجه را ببینیم. همچنین می‌توانیم نتیجه را در Codesandbox نیز مشاهده نماییم.

//App.tsx

<div>
  <WithIntersectionObserver
    products={products}
    fetchData={fetchData}
    loading={loading}
    error={error}
  />
</div>

انتخاب بهترین روش

ما سه روش مختلف برای پیاده‌سازی اسکرول بینهایت در React را بررسی کردیم. از آنجایی که احتمالاً فقط به یکی از این روش‌ها در پروژه خود نیاز داریم، انتخاب روش مناسب بستگی به نیازهای خاص برنامه ما دارد. در اینجا برخی نکات کلیدی برای کمک به تصمیم‌گیری را باهم بررسی می‌کنیم:

بررسی مزایا و معایب هر روش

پیاده‌سازی از صفر

این روش کنترل کامل بر پیاده‌سازی را فراهم می‌کند و امکان سفارشی‌سازی نامحدود را می‌دهد. با این حال، برای بهینه‌سازی عملکرد (مثلاً کاهش دفعات اجرای event scroll، به‌روزرسانی کارآمد DOM) نیاز به تلاش بیشتری دارد و در صورت پیاده‌سازی نادرست ممکن است دارای اشکالاتی باشد.

چه زمانی از این روش استفاده کنیم؟

استفاده از کتابخانه‌ها

کتابخانه‌ها اغلب با بهینه‌سازی‌های داخلی و پشتیبانی community، راه‌حلی سریع و مطمئن برای پیاده‌سازی اسکرول بینهایت ارائه می‌دهند. با این حال، ممکن است باعث افزایش حجم کد برنامه ما شوند و برای به‌روزرسانی‌ها و رفع اشکالات به نگه‌داری‌کنندگان کتابخانه وابسته خواهیم بود.

چه زمانی از این روش استفاده کنیم؟

استفاده از Intersection Observer

این روش مدرن نیازی به استفاده از eventهای scroll و کاهش فراخوانی آن‌ها ندارد، بنابراین کارایی بیشتری دارد و نگه‌داری آن آسان‌تر است. با این حال، در مرورگرهای قدیمی مانند Internet Explorer پشتیبانی نمی‌شود.

چه زمانی از این روش استفاده کنیم؟

پیاده‌سازی اسکرول به بالا در React با استفاده از ref و scrollTop API

اسکرول به بالا یک قابلیت اضافی است که معمولاً در اسکرول بینهایت پیاده‌سازی می‌شود و تجربه کاربری را بهبود می‌بخشد.

مثلاً در  X، هنگامی که در صفحه For You اسکرول می‌کنیم، این صفحه هیچ‌وقت به پایان نمی‌رسد؛ این نمونه‌ای از اسکرول بینهایت است. سپس، وقتی روی آیکون خانه در منوی ناوبری X کلیک می‌کنیم، صفحه دوباره به بالای لیست برمی‌گردد. این آیکون دو وظیفه دارد: به‌روزرسانی و بارگذاری داده‌های جدید در For You و ارائه قابلیت اسکرول به بالا.

برای بهبود تجربه کاربری، بهتر است تمام پیاده‌سازی‌های اسکرول بینهایت دارای گزینه‌ای برای برگشت به بالای صفحه باشند. برای پیاده‌سازی این قابلیت در React، از ویژگی scrollTop() و هوک useRef() استفاده خواهیم کرد تا کنترل بهتری بر موقعیت اسکرول داشته باشیم.

ترکیب این دو، امکان پیاده‌سازی قابلیت‌هایی مانند دکمه‌های اسکرول به بالا یا بارگذاری محتوای پویا هنگام اسکرول را فراهم می‌کند، همان‌طور که در مثالی که از ابتدا پیاده‌سازی کردیم دیدیم. این قابلیت را در فایل App.tsx پیاده‌سازی می‌کنیم:

import React, { useRef } from 'react';

function App() {
  const scrollableDiv = useRef<HTMLDivElement | null>(null);

  // scroll logic</span>
  const scrollToTop = () => {
    if (scrollableDiv.current) {
      scrollableDiv.current.scrollTop = 0;
    }
  };

  return (
    <div ref={scrollableDiv}>
      <!-- Infinite Scroll content -->
      <button onClick={scrollToTop}>Scroll to Top</button>
    </div>
  );
}

export default ScrollableComponent;

در کد بالا، اسکرول به بالا با استفاده از ref و scrollTop() انجام می‌شود که مستقیماً موقعیت اسکرول div را دستکاری می‌کند. با این کار، می‌توانیم تجربه کاربری در اسکرول بینهایت را به‌طور قابل‌توجهی بهبود دهیم.

جمع‌بندی

اسکرول بینهایت یک تکنیک قدرتمند در طراحی وب است که تجربه کاربری را با بارگذاری تدریجی محتوا هنگام اسکرول به پایین صفحه بهبود می‌بخشد و نیاز به صفحه‌بندی را از بین می‌برد. در این مقاله، سه روش مختلف برای پیاده‌سازی اسکرول بینهایت در برنامه‌های React را بررسی کردیم و یک صفحه محصولات برای یک فروشگاه اینترنتی ایجاد نمودیم.

هر تکنیک مزایا و معایب خاص خود را دارد، بنابراین انتخاب روش مناسب بستگی به نیازهای خاص پروژه ما و کاربران دارد. با پیاده‌سازی اسکرول بینهایت در برنامه‌های React، می‌توانیم یک تجربه کاربری جذاب و روان ایجاد کنیم که کاربران را به تعامل بیشتر با محتوای ما ترغیب کند.