محمود سعداوي2 نشر 1 أغسطس أرسل تقرير نشر 1 أغسطس السلام عليكم. الكود التالي لصفحة ويب تعرض المنتجات مقسمة على عدة صفحات وتحتوي عنصر بحث مع قائمة جانبية للفلترة. 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 لكن لم يتغير شيء. هل من حلول تقوم بتحسين الأداء شكرا 1 اقتباس
0 Mustafa Suleiman نشر 1 أغسطس أرسل تقرير نشر 1 أغسطس المشكلة أنك تستخدم كائن حالة واحد 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); 1 اقتباس
0 محمود سعداوي2 نشر 1 أغسطس الكاتب أرسل تقرير نشر 1 أغسطس بتاريخ 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 اقتباس
السؤال
محمود سعداوي2
السلام عليكم.
الكود التالي لصفحة ويب تعرض المنتجات مقسمة على عدة صفحات وتحتوي عنصر بحث مع قائمة جانبية للفلترة.
المشكل هو عند كل تغيير في البحث مثلا يحصل rerender لقائمة الفلترة والعكس صحيح وهذا يكلف ثمنا باهضا على الأداء.
قمت باستخدام useCallback useMemo React.memo لكن لم يتغير شيء.
هل من حلول تقوم بتحسين الأداء
شكرا
2 أجوبة على هذا السؤال
Recommended Posts
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.