اذهب إلى المحتوى

السؤال

نشر

السلام عليكم.

الكود التالي لصفحة ويب تعرض المنتجات مقسمة على عدة صفحات وتحتوي عنصر بحث مع قائمة جانبية للفلترة.

import { useCallback, useState } from "react";
import { IoFilter, IoClose, IoAlertCircleOutline  } from "react-icons/io5";
import FormFilters from "../components/products/FormFilters";
import { useFetchProducts } from "../lib/queries/productsQueries";
import Alert from "../components/Alert";
import Skeleton from "../components/loaders/Skeleton";
import Pagination from "../components/products/Pagination";
import ProductCard from "../components/products/ProductCard";
import Search from "../components/products/Search";

const ProductsPage = () => {
  const [filters, setFilters] = useState({
    pageNumber: 1,
    keyword: "",
    category: [],
    minPrice: 0,
    maxPrice: 2000,
    sort: "",
  });
  const {
    data,
    isPending: isProductsPending,
    isError: isProductsError,
    error: productsError,
  } = useFetchProducts(filters);
  const [showMobileFilters, setShowMobileFilters] = useState(false);
  /**
   * -------------------------------------------
   * Search Products
   * -------------------------------------------
  */
  const handleSearchProducts = useCallback((text) => {
    setFilters((prev) => ({
      ...prev,
      keyword: text.trim(),
      pageNumber: 1,
    }));
  }, []);
  /**
   * -------------------------------------------
   * Pagination
   * -------------------------------------------
  */
  const handlePageChange = useCallback((newPage) => {
    setFilters((prev) => ({ ...prev, pageNumber: newPage }));
  }, []);
  /** ------------------------------------------
   * Loading
  ------------------------------------------- */
  if (isProductsPending) return <Skeleton />;
  return (
    <div className="w-full bg-white text-gray-800 my-5 px-3">
      {isProductsError && (
        <Alert message={productsError.message} type="error" />
      )}
      <div className="max-w-7xl mx-auto">
        <div className="md:flex">
          {showMobileFilters && (
            <div className="fixed inset-0 z-50 flex">
              <div
                className="fixed inset-0 bg-opacity-30 backdrop-blur-sm"
                onClick={() => setShowMobileFilters(false)}
              ></div>

              <div className="relative bg-white w-3/4 max-w-sm h-full shadow-xl animate-slide-in-left">
                <button
                  className="absolute top-3 right-4 text-2xl text-gray-600"
                  onClick={() => setShowMobileFilters(false)}
                >
                  <IoClose />
                </button>
                {/* -------------------------------------------
                  Filters for Mobile
                ------------------------------------------- */}
                <FormFilters
                  formClassName="p-4 space-y-6 mt-7"
                  defaultValues={filters}
                  onApplyFilters={(newFilters) => setFilters(newFilters)}
                />
              </div>
            </div>
          )}
          {/* -------------------------------------------
            Desktop Filters
          ------------------------------------------- */}
          <FormFilters
            formClassName="hidden md:block md:w-1/4 space-y-6"
            defaultValues={filters}
            onApplyFilters={(newFilters) => setFilters(newFilters)}
          />
          <div className="md:w-3/4 md:pl-8 mx-auto">
            {/* ------------------------------------------ 
              Products Searching (Mobile & Desktop)
            ------------------------------------------- */}
            <div className="flex items-center justify-center space-x-2 mb-3">
              <div className="md:hidden">
                <button
                  onClick={() => setShowMobileFilters(true)}
                  className="flex items-center text-white bg-[#F8AD47] font-semibold p-3 rounded-md cursor-pointer"
                >
                  <IoFilter size={20} />
                </button>
              </div>
              <Search onSearch={handleSearchProducts}/>
            </div>
            {/* ------------------------------------------ 
              Display Products
            ------------------------------------------- */}
            <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
            {data.products?.length > 0 ? (
              data.products.map((product) => (
                <ProductCard key={product._id} product={product} />
              ))
            ) : (
              <div className="col-span-full flex flex-col items-center justify-center text-center p-6 border border-dashed border-gray-300 rounded-xl text-gray-500 dark:text-gray-400">
                <IoAlertCircleOutline className="text-4xl mb-2" />
                <p className="text-lg font-medium">No products found</p>
                <p className="text-sm">Try adjusting your filters or search term.</p>
              </div>
            )}
            </div>
            {/* -------------------------------------------
              Pagination
            ------------------------------------------- */}
            {data?.page && parseInt(data?.pages) > 1 && (
              <Pagination
                currentPage={data?.page}
                totalPages={data?.pages}
                onPageChange={handlePageChange}
              />
            )}
          </div>
        </div>
      </div>
    </div>
  );
};

export default ProductsPage;

المشكل هو عند كل تغيير في البحث مثلا يحصل rerender لقائمة الفلترة والعكس صحيح وهذا يكلف ثمنا باهضا على الأداء.

قمت باستخدام useCallback useMemo React.memo لكن لم يتغير شيء.

هل من حلول تقوم بتحسين الأداء 

شكرا

Recommended Posts

  • 0
نشر

المشكلة أنك تستخدم كائن حالة واحد filters لجميع متغيرات الفلترة والبحث والترقيم، وباستدعاء setFilters لتغيير أي خاصية مثلاً keyword عند البحث، يتم إنشاء كائن جديد تمامًا في الذاكرة.

بالتالي FormFilters يتلقى كائنًا جديدًا في كل مرة ويتلقى دالة جديدة في كل مرة في onApplyFilters، ونفس المبدأ ينطبق على مكون Search عندما تتغير الفلاتر الجانبية.

والمشكلة أيضًا ليست فقط في إنشاء كائن جديد، بل في تمرير نفس الكائن وإن كان جديدًا إلى جميع المكونات في آنٍ واحد، فكل مكون يرى أنّ أحد الـ props تغير، فيُعاد تركيبه.

لو استخدمت مكتبة إدارة حالة مثل Zustand كان سيجنبك تلك المشكلة من البداية بكل سهولة.

بدونها الأفضل استخدام useReducer بدلاً من useState لكن للتسهيل استخدم useState، أي يجب فصل الحالة إلى أجزاء مستقلة، فبدلاً من كائن واحد، استخدم أكثر من useState، بتقسيم كائن filters إلى حالات مستقلة في ProductsPage.jsx:

const [keyword, setKeyword] = useState("");
const [pageNumber, setPageNumber] = useState(1);

const [formFilters, setFormFilters] = useState({
  category: [],
  minPrice: 0,
  maxPrice: 2000,
  sort: "",
});

وبما أن useFetchProducts يتوقع كائن واحد، فنستطيع تجميعه باستخدام useMemo، وبالتالي كائن الفلاتر لن يتم إنشاؤه من جديد إلا إذا تغيرت إحدى الحالات التي يعتمد عليها:

import { useCallback, useState, useMemo } from "react";


// بعد تعريف الحالات المنفصلة
const apiFilters = useMemo(() => ({
  keyword,
  pageNumber,
  ...formFilters,
}), [keyword, pageNumber, formFilters]);

const {
  data,
  isPending: isProductsPending,
  isError: isProductsError,
  error: productsError,
} = useFetchProducts(apiFilters);

ثم إنشاء دوال المعالجة عن طريق useCallback مع dependencies صحيحة.


const handleSearchProducts = useCallback((text) => {
  setKeyword(text.trim());
  setPageNumber(1); 
}, []); 

const handlePageChange = useCallback((newPage) => {
  setPageNumber(newPage);
}, []);

const handleApplyFilters = useCallback((newFilters) => {
  setFormFilters(newFilters);
  setPageNumber(1); 
}, []);

ثم تغليف المكونات الفرعية FormFilters, Search, Pagination بـ React.memo لتستفيد من الـ props المستقرة، في FormFilters.js:

import React from 'react';

const FormFilters = ({ }) => {  };
export default React.memo(FormFilters);

وفي Search.js:

import React from 'react';
const Search = ({ }) => {  };
export default React.memo(Search);

 

  • 0
نشر
بتاريخ 11 دقائق مضت قال Mustafa Suleiman:

المشكلة أنك تستخدم كائن حالة واحد filters لجميع متغيرات الفلترة والبحث والترقيم، وباستدعاء setFilters لتغيير أي خاصية مثلاً keyword عند البحث، يتم إنشاء كائن جديد تمامًا في الذاكرة.

بالتالي FormFilters يتلقى كائنًا جديدًا في كل مرة ويتلقى دالة جديدة في كل مرة في onApplyFilters، ونفس المبدأ ينطبق على مكون Search عندما تتغير الفلاتر الجانبية.

والمشكلة أيضًا ليست فقط في إنشاء كائن جديد، بل في تمرير نفس الكائن وإن كان جديدًا إلى جميع المكونات في آنٍ واحد، فكل مكون يرى أنّ أحد الـ props تغير، فيُعاد تركيبه.

لو استخدمت مكتبة إدارة حالة مثل Zustand كان سيجنبك تلك المشكلة من البداية بكل سهولة.

بدونها الأفضل استخدام useReducer بدلاً من useState لكن للتسهيل استخدم useState، أي يجب فصل الحالة إلى أجزاء مستقلة، فبدلاً من كائن واحد، استخدم أكثر من useState، بتقسيم كائن filters إلى حالات مستقلة في ProductsPage.jsx:

const [keyword, setKeyword] = useState("");
const [pageNumber, setPageNumber] = useState(1);

const [formFilters, setFormFilters] = useState({
  category: [],
  minPrice: 0,
  maxPrice: 2000,
  sort: "",
});

وبما أن useFetchProducts يتوقع كائن واحد، فنستطيع تجميعه باستخدام useMemo، وبالتالي كائن الفلاتر لن يتم إنشاؤه من جديد إلا إذا تغيرت إحدى الحالات التي يعتمد عليها:

import { useCallback, useState, useMemo } from "react";


// بعد تعريف الحالات المنفصلة
const apiFilters = useMemo(() => ({
  keyword,
  pageNumber,
  ...formFilters,
}), [keyword, pageNumber, formFilters]);

const {
  data,
  isPending: isProductsPending,
  isError: isProductsError,
  error: productsError,
} = useFetchProducts(apiFilters);

ثم إنشاء دوال المعالجة عن طريق useCallback مع dependencies صحيحة.


const handleSearchProducts = useCallback((text) => {
  setKeyword(text.trim());
  setPageNumber(1); 
}, []); 

const handlePageChange = useCallback((newPage) => {
  setPageNumber(newPage);
}, []);

const handleApplyFilters = useCallback((newFilters) => {
  setFormFilters(newFilters);
  setPageNumber(1); 
}, []);

ثم تغليف المكونات الفرعية FormFilters, Search, Pagination بـ React.memo لتستفيد من الـ props المستقرة، في FormFilters.js:

import React from 'react';

const FormFilters = ({ }) => {  };
export default React.memo(FormFilters);

وفي Search.js:

import React from 'react';
const Search = ({ }) => {  };
export default React.memo(Search);

 

المشكل بالأساس مثلما قلت هو مشاركة filters مع العناصر الفرعية وهذا شيء لابد منه لذلك كان لزاما إما تمريرها عبر props أو إستعمال global state وهو الحل الأمثل.

شكرا لكم على التوضيح @Mustafa Suleiman

انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أجب على هذا السؤال...

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.

  • إعلانات

  • تابعنا على



×
×
  • أضف...