آموزش React: راهنمای کامل useCallback
عباس سپهوند
عباس سپهوند
  • 1402/05/28
  • 40 دقیقه
  • 0 نظر

آموزش React: راهنمای کامل useCallback

با استفاده از useCallback امکان کش کردن یک function قبل از ارسال آن به یک کامپوننت بهینه شده وجود دارد. در این مقاله جزئیات این هوک پر کاربرد را بررسی می کنیم

در این بخش از آموزش React، در رابطه با useCallback که یکی از هوک های پر کاربرد در ری اکت است صحبت می کنیم. این هوک امکان کش کردن تعریف یک تابع را در رندرهای مجدد کامپوننت ها فراهم می کند. به عبارت دقیق تر به جای اینکه در هر بار رندر مجدد، function مورد نظر (که در یک کامپوننت تعریف شده است)، دوباره تعریف شود، در عوض کش می شود. اینکار باعث افزایش کارایی اپلیکیشن می شود. 

برای مطالعه بیشتر می توانید به پیج آموزش React مراجعه کنید.

قبل از اینکه به کاربردهای این هوک بپردازیم، ابتدا لازم است ببینیم ساختار کلی این هوک به چه شکلی است.

 

useCallback به شکل زیر تعریف می شود:

const cachedFn = useCallback(fn, dependencies)

در تعریف فوق:

  • fn، تابعی است که می خواهیم کش کنیم. این تابع می تواند هر تابعی با هر تعداد پارامتر و آرگومانی باشد. در رندر اول ری اکت تابع را عینا بر می گرداند. البته دقت کنیدری اکت تابع را کال نمی کند فقط رفرنس به تابع را بر می گرداند. در رندرهای بعدی ری اکت همین تابع را مجدد به شما بر می گرداند به شرطی که وابستگی های آن تغییر نکرده باشد. وابستگی های تابع همانطور که در کد فوق می بینید به عنوان پارامتر دوم مشخص می شوند. در صورتی که وابستگی ها تغییر کرده باشند در رندر بعدی تابع مجدد توسط ری اکت تعریف می شود. دلیل اینکه چرا نیاز به تعریف مجدد یک تابع است به بحث closure ها در جاوااسکریپت بر می گردد که در ادامه این مطلب بررسی می کنیم.
  • dependencies: در واقع لیستی از مقادیری است که به طور مستقیم در تابع تعریف شده اند. این مقادیر می تواند props، state یا هر مقداری باشد که در کامپوننت تعریف شده است. وابستگی ها به شکل یک آرایه با مقادیر ثابت تعریف می شود. به عنوان مثال آرایه [dep1, dep2, dep3] می تواند یک لیست از وابستگی ها در نظر گرفته شود. نکته مهم این است که هر کدام از از مقادیر باید به شکل مستقیم در تابع استفاده شده باشد.

مقدار برگشتی useCallback در صورتی که رندر اول باشد که همان تابعی است که به عنوان پارامتر اول تعریف کرده ایم. در رندرهای بعدی چنانچه مقدار هیچ یک از وابستگی های تغییر نکرده باشد همان تابع مجدد استفاده می شود. در غیر این صورت یک رفرنس جدید از تابع تعریف می شود و به عنوان خروجی useCallback برگردانده می شود. 

 

کاربردهای useCallback

 

جلوگیری از رندر مجدد کامپوننت های فرزند

برای بررسی دقیق این سناریو با یک مثال کار رو آغاز می کنیم:

function ProductPage({ productId, referrer, theme }) {
  function handleSubmit(orderDetails) {
    post('/product/' + productId + '/buy', {
      referrer,
      orderDetails,
    });
  }
  
  return (
    <div className={theme}>
      <ShippingForm onSubmit={handleSubmit} />
    </div>
  );
}

 

در مثال فوق، کامپوننتی به نام ProductPage تعریف کرده ایم. در این کامپوننت تابعی به نام handleSubmit تعریف شده است که محصول انتخابی کاربر را در دیتابیس ذخیره می کند. اما نکته مهم این است که این تابع در کامپوننت ProductPage اجرا نمی شود در عوض از طریق Props به کامپوننت ShippingForm ارسال شده است تا در آن کامپوننت اجرا شود. اما در این قسمت مسئله ای وجود دارد. در واقع تغییر theme باعث می شود کامپوننت ProductPage مجددا رندر شود که این باعث می شود کامپوننت ShippingForm هم re-render شود. طبق قاعده زمانی که کامپوننت پدر re-render می شود کامپوننت های فرزند هم re-render می شوند. اگر کامپوننت ShippingForm ساختار ساده ای داشته باشد مشکلی نداریم ولی اگر در این کامپوننت محاسبات سنگینی انجام می شود پس هر بار re-render این کامپوننت می تواند باعث کندی اپلیکیشن شود. بنابراین در این حالت بهتر است کامپوننت ShippingForm فقط زمانی re-render شود که props آن تغییر کند. برای این کار می توانیم از memo به شکل زیر استفاده کنیم:

 

import { memo } from 'react';

const ShippingForm = memo(function ShippingForm({ onSubmit }) {
  // ...
});

با این تغیییر اکنون کامپوننت ShippingForm فقط زمانی re-render می شود که props مربوط به آن (در اینجا تابع onSubmit) تغییر کند. این یک گام به سوی افزایش کارایی اپلیکیشن است. اما در اینجا مسئله ای وجود دارد که عملا استفاده از memo را بی تاثیر می کند. در واقع وجود این مسئله باعث می شود کامپوننت ShippingForm با هر بار re-render شدن کامپوننت پدر، re-render شود. اما مسئله چیست؟

در جاوااسکریپت تعریف یک تابع به صورت همیشه یک نمونه جدید از آن تابع را ایجاد می کند. به عبارت دقیق تر زمانی که کامپوننت پدر re-render می شود، در بدنه این کامپوننت تابع handleSubmit تعریف شده است. با هر بار re-render یک نمونه جدید از آن تابع ایجاد می شود و این یعنی اینکه کامپوننت ShippingForm همیشه یک رفرنس جدید از تابع handleSubmit را دریافت می کند. در واقع ما با استفاده از memo کامپوننت ShippingForm را کش کردیم که هر بار re-render نشود البته به شرطی که props مربوط به آن یعنی تابع onSubmit تغییر نکند. اما از آنجا که در هر بار re-render شدن پدر یک نمونه جدید از تابع handleSubmit ساخته و به عنوان props به ShippingForm ارسال می شود در واقع همیشه پارامتر OnSubmit تغییر می کند و این باعث رندر مجدد ShippingForm می شود. 

برای حل این مشکل می توانیم handleSubmit را در useCallback قرار دهیم. با اینکار handleSubmit کش می شود و همیشه یه نمونه یکسان به ShippingForm ارسال می شود (البته در شرایطی که لیست وابستگی ها تعییر نکند). در این حالت دیگر ShippingForm رندر مجدد نمی شود. با توجه به این توضیحات کافیست handleSubmit را در useCallback قرار دهیم.

 

function ProductPage({ productId, referrer, theme }) {
  // Tell React to cache your function between re-renders...
  const handleSubmit = useCallback((orderDetails) => {
    post('/product/' + productId + '/buy', {
      referrer,
      orderDetails,
    });
  }, [productId, referrer]); // ...so as long as these dependencies don't change...

  return (
    <div className={theme}>
      {/* ...ShippingForm will receive the same props and can skip re-rendering */}
      <ShippingForm onSubmit={handleSubmit} />
    </div>
  );
}

 

آپدیت State در useCallback

در شرایطی لازم است یک یا چند state را در useCallback بروزرسانی کنیم. به عنوان مثال تابع handleAddTodo در یک useCallback قرار گرفته است. و از آنجا که در این تابع state آپدیت می شود، todos به عنوان وابستگی به لیست وابستگی های useCallback اضافه شده است:

function TodoList() {
  const [todos, setTodos] = useState([]);

  const handleAddTodo = useCallback((text) => {
    const newTodo = { id: nextId++, text };
    setTodos([...todos, newTodo]);
  }, [todos]);
 


به عنوان یک قاعده کلی در نظر داشته باشید که ما تا حد امکان باید وابستگی های کمتری داشته باشیم تا بتوانیم از نسخه کش شده تابع استفاده کنیم. بنابراین چنانچه در یک useCallback بخواهیم از state استفاده کنیم به شرطی که استفاده از state صرفا برای محاسبه state بعدی باشد می توانیم وابستگی را به شکل زیر حذف کنیم ولی تغییر state به شکل function باید انجام شود:


function TodoList() {
  const [todos, setTodos] = useState([]);

  const handleAddTodo = useCallback((text) => {
    const newTodo = { id: nextId++, text };
    setTodos(todos => [...todos, newTodo]);
  }, []); // ✅ No need for the todos dependency
 

اجتناب از اجرای های مکرر useEffect با استفاده از useCallback

گاهی لازم است تابعی را در useEffect کال کنیم:

 

const User = (id: string) => {
    const [user, setUser] = useState();
    const fetchData = async () => {
        const response = await fetch(
            "https://jsonplaceholder.typicode.com/users/" + id
        );
        const fetchedUser = await response.json();
        setUser(fetchedUser);
    };

    useEffect(() => {
        fetchData();
    }, [fetchData]);
    return <>{<p> user.name</p>}</>;
};

اما در این کد یک مسئله اساسی وجود دارد. طبق قاعده در useEffect، هر مقدار reactive ی باید به عنوان وابستگی به لیست وابستگی های useEffect اضافه شود. اما در کد فوق با اضافه کردن تابع fetchData به لیست وابستگی های useEffect به شکل متوالی و بی نهایت تابع fetchData کال می شود و هر بار یک reqeust به سرور ارسال می شود. پس مشکل اینجاست که fetchData را در لیست وابستگی های useEffect اضافه کرده ایم. برای حل این مشکل می توانیم به سادگی fetchData function رو در useCallback قرار دهیم:

 

"use client";
import { useCallback, useEffect, useState } from "react";

const Temp = () => {
    const [state, setState] = useState(1);
    setInterval(() => {
        setState(state);
    }, 3000);
    return <InnerTemp id={state} />;
};

type data = {
    id: number;
};

const User = (id: string) => {
    const [user, setUser] = useState();
    const fetchData = useCallback(async () => {
        const response = await fetch(
            "https://jsonplaceholder.typicode.com/users/" + id
        );
        const fetchedUser = await response.json();
        setUser(fetchedUser);
    }, [id]);

    useEffect(() => {
        fetchData();
    }, [fetchData]);
    return <>{<p> user.name</p>}</>;
};
export default Temp;

با این کار تضمین می کنیم که در re-render های مختلف تا زمانی که id تغییر نکند همیشه یک نمونه یکسان از fetchData function ارسال می شود و این باعث می شود useEffect اجرا نشود مگر اینکه id تغییر کرده باشد. اما راه ساده تری هم وجود دارد. در واقع ما می توانیم بدون استفاده از useCallback و فقط با انتقال تابع fetchData به داخل useEffect بدون هیچ نگرانی کد را مدیریت کنیم:

 

const User = (id: string) => {
    const [user, setUser] = useState();
    useEffect(() => {
        const fetchData = async () => {
            const response = await fetch(
                "https://jsonplaceholder.typicode.com/users/" + id
            );
            const fetchedUser = await response.json();
            setUser(fetchedUser);
        };
        fetchData();
    }, [id]);
    return <>{<p> user.name</p>}</>;
};

در این حالت کد ما ساده تر شد و نیازی هم به استفاده از useCallback وجود ندارد.

 

 

بهینه سازی یک Custom Hook

در این کاربرد چنانچه در حال پیاده سازی یک هوک سفارشی هستید، پیشنهاد می شود چنانچه در این هوک قصد دارید یک یا چند تابع برگردانید، این توابع در useCallback قرار گیرند:

 

function useRouter() {
  const { dispatch } = useContext(RouterStateContext);

  const navigate = useCallback((url) => {
    dispatch({ type: 'navigate', url });
  }, [dispatch]);

  const goBack = useCallback(() => {
    dispatch({ type: 'back' });
  }, [dispatch]);

  return {
    navigate,
    goBack,
  };
}

در این حالت استفاده کننده گان یا consumer های این هوک می توانند به سادگی و در صورت نیاز کد خود را بهینه کنند.

 

متداول ترین اشتباهات در استفاده از useCallback

در ادامه برخی از متداول ترین اشتباهات در استفاده از از useCallback را مورد بررسی قرار می دهیم. 

 

هر بار کامپوننت رندر-مجدد می شود، useCallback یک تابع متفاوت بر می گرداند

مطمئن شوید که آرگومان دوم یعنی لیست وابستگی ها تعریف شده باشد. دقت کنید چنانچه آرگومان دوم تعریف نشده باشد، useCallback هر بار یک تابع جدید را پس از رندر مجدد بر می گرداند:


function ProductPage({ productId, referrer }) {
  const handleSubmit = useCallback((orderDetails) => {
    post('/product/' + productId + '/buy', {
      referrer,
      orderDetails,
    });
  }); // 🔴 Returns a new function every time: no dependency array
  // ...


به جای کد فوق دقت کنید که لیست وابستگی ها را به فرض مثال به شکل زیر حتما مشخص شود:

 

function ProductPage({ productId, referrer }) {
  const handleSubmit = useCallback((orderDetails) => {
    post('/product/' + productId + '/buy', {
      referrer,
      orderDetails,
    });
  }, [productId, referrer]); // ✅ Does not return a new function unnecessarily
  // ...

 

چنانچه تغییر فوق مشکل را حل نکرد، این احتمال وجود دارد که یکی از وابستگی ها نسبت به رندر دفعه پیش تغییر کرده که باعث ایجاد یک نمونه جدید از تابع می شود.  برای بررسی این مشکل لازم است از console.log مسئله را بررسی کنید:


 const handleSubmit = useCallback((orderDetails) => {
    // ..
  }, [productId, referrer]);

  console.log([productId, referrer]);


در ادامه در کنسول روی هر کدام از مقادیری که در هر رندر-مجدد لاگ شده است راست کلیک کنید و گزینه "Store as a global variable" را کلیک کنید. با این فرض که آیتم اول با نام temp1 و آیتم دوم با نام temp2 ذخیره شوند، می توانید از در مرورگر و در تب کنسول با دستور زیر مشخص کنید که وابستگی ها در هر بار رندر مجدد، یکسان هستند یا متفاوت:


Object.is(temp1[0], temp2[0]); // Is the first dependency the same between the arrays?
Object.is(temp1[1], temp2[1]); // Is the second dependency the same between the arrays?
Object.is(temp1[2], temp2[2]); // ... and so on for every dependency ...


نیاز دارید برای هر آیتم یک لیست useCallback را کال کنید

فرض کنید chart component در یک memo قرار داده شده است. در این حالت می خواهیم از هر بار رندر chart component در صورتی که ReportList component رندر-مجدد شد، جلوگیری کنیم.اما مشکل اینجاست که نمی شود در یک حلقه از useCallback به شکل زیر استفاده کرد:


function ReportList({ items }) {
  return (
    <article>
      {items.map(item => {
        // 🔴 You can't call useCallback in a loop like this:
        const handleClick = useCallback(() => {
          sendReport(item)
        }, [item]);

        return (
          <figure key={item.id}>
            <Chart onClick={handleClick} />
          </figure>
        );
      })}
    </article>
  );
}



برای حل این مشکل لازم است هر آیتم را در یک کامپوننت دیگر رندر کنیم و useCallback را در آنجا قرار دهیم:

function ReportList({ items }) {
  return (
    <article>
      {items.map(item =>
        <Report key={item.id} item={item} />
      )}
    </article>
  );
}

function Report({ item }) {
  // ✅ Call useCallback at the top level:
  const handleClick = useCallback(() => {
    sendReport(item)
  }, [item]);

  return (
    <figure>
      <Chart onClick={handleClick} />
    </figure>
  );
}

حتی می توان کار را بهتر کرد و به جای useCallback از memo به شکل زیر استفاده کرد:


function ReportList({ items }) {
  // ...
}

const Report = memo(function Report({ item }) {
  function handleClick() {
    sendReport(item);
  }

  return (
    <figure>
      <Chart onClick={handleClick} />
    </figure>
  );
});

با تغییر فوق، چنانچه item props تغییر نکند، کامپوننت Report رندر مجدد نمی شود که در نتیجه آن کامپوننت Chart هم، رندر مجدد نمی شود.

در انتها، برای دسترسی به جدیدترین مباحث در ری اکت دوره کاملا رایگان آموزش React را می توانید بررسی کنید. 

دیدگاه

برای ارسال دیدگاه های خود ابتدا وارد شوید یا ثبت نام کنید

ورود یا ثبت نام
عباس سپهوند
عباس سپهوند

برنامه نویس و توسعه دهنده نرم افزار

مشاهده پروفایل
5 مقاله اخیر

آموزش React: راهنمای کامل useCallback

عباس سپهوند
زمان مطالعه: 40

آموزش React: راهنمای کامل useEffect

عباس سپهوند
زمان مطالعه: 25

آموزش React: راهنمای کامل Ref ها در React

عباس سپهوند
زمان مطالعه: 15
مشاهده همه مقالات