useCallback — هوک React است که نسخه memoize شده تابع را برمیگرداند؛ تابعی که تا زمانی که وابستگیهایش تغییر نکنند، بین رندرها تغییر نمیکند. برخلاف تعریف معمولی تابع در داخل کامپوننت (که در هر رندر یک تابع جدید میسازد)، useCallback ارجاع به تابع را پایدار میکند و این کار از رندرهای غیرضروری کامپوننتهای فرزندی که با React.memo بهینه شدهاند جلوگیری میکند. طبق React Documentation (2025)، useCallback فقط در ترکیب با React.memo یا هوکهایی که به ارجاع پایدار وابستهاند مفید است.
نکات کلیدی
useCallback — هوکی است که در React 16.8 اضافه شده و تابع را memoize میکند: تا زمانی که وابستگیها تغییر نکنند، همان ارجاع را برمیگرداند. بدون useCallback هر تعریف تابع در داخل کامپوننت، در هر رندر یک شیء تابع جدید میسازد. برای مقادیر اولیه این موضوع نامحسوس است، اما هنگام ارسال چنین callbackهایی به کامپوننتهای فرزندی که با React.memo بهینه شدهاند، هر ارجاع جدید باعث رندر مجدد کامپوننت فرزند میشود.
از نظر نحوی، useCallback معادل useMemo برای تابع است: useCallback(fn, deps) — مخففی برای useMemo(() => fn, deps) است. React تابع memoize شده را در حافظه داخلی گره fiber ذخیره میکند و در هر رندر وابستگیها را مقایسه میکند. اگر وابستگیها تغییر نکرده باشند (Object.is برای هر عنصر)، تابع قبلی برگردانده میشود.
طبق React Documentation — useCallback (2025)، لازم نیست هر تابع را در useCallback بپیچیم. این هوک هزینه خودش را دارد: فراخوانی هوک، مقایسه وابستگیها و تخصیص حافظه برای آرایه وابستگیها. اگر کامپوننت ساده است و درختهای عمیق با React.memo ندارد، useCallback فقط برنامه را کندتر میکند. بهینهسازی باید قابل اندازهگیری باشد، نه شهودی.
import { useCallback } from 'react';
function Parent() {
const [count, setCount] = useState(0);
// ارجاع پایدار — تا تغییر deps همان تابع
const handleClick = useCallback(() => {
setCount(prev => prev + 1);
}, []);
return <Child onClick={handleClick} />;
}
memoization در useCallback بر اساس کش کردن نتیجه فراخوانی تابع است. React closure ساختهشده در لحظه رندر اول را حفظ میکند و در رندرهای بعدی تا زمانی که وابستگیها بدون تغییر بمانند، همان را برمیگرداند. در داخل گره fiber، هر فراخوانی useCallback یک گره در لیست پیوندی هوکها ایجاد میکند که وابستگیهای قبلی و مقدار memoize شده در آن ذخیره میشود.
فرآیند مقایسه وابستگیها دقیقاً از طریق Object.is انجام میشود — این مقایسهای سطحی است، بدون بررسی عمیق اشیا و آرایهها. اگر در وابستگیها شیء یا آرایه وجود داشته باشد، ارجاع جدید در هر رندر بهعنوان تغییر در نظر گرفته میشود. بنابراین در آرایه وابستگیها باید مقادیر اولیه یا ارجاعهای پایدار (مثلاً از useRef یا useMemo) ارسال شود.
طبق React Core Team — Optimization Guide (2024)، هزینه memoization شامل سه مؤلفه است: تخصیص آرایه وابستگیها در هر رندر، پیمایش و مقایسه عناصر از طریق Object.is و هزینه احتمالی جمعآوری زباله هنگام بازآفرینی. برای کامپوننتی با صدها بستهبندی useCallback این میتواند محسوس باشد — بنابراین انتخابگری در استفاده از هوک حیاتی است.
| سناریو | بدون useCallback | با useCallback |
|---|---|---|
| ساخت تابع | جدید در هر رندر | یکسان با deps پایدار |
| ارسال به React.memo | فرزند رندر مجدد میشود | فرزند رندر مجدد نمیشود |
| در آرایه useEffect | افکت دوباره اجرا میشود | افکت پایدار است |
| هزینه | حداقل | مقایسه وابستگیها + حافظه |
این باور رایج است که useCallback بهطور خودکار عملکرد را بهبود میبخشد. در واقع، بهصورت مجزا (بدون React.memo) useCallback حتی کمی برنامه را کندتر میکند به دلیل هزینه مقایسه وابستگیها. هوک فقط در سه سناریو فایده واقعی دارد: جلوگیری از رندر مجدد کامپوننتهای React.memo، پایداری callbackها در useEffect و ارسال به هوکهای سفارشی که به reference equality وابستهاند.
قانون ساده است: تا وقتی مشکل عملکردی را از طریق React DevTools Profiler پیدا نکردهای، از useCallback استفاده نکن. تیم React بارها تأکید کرده که بهینهسازی زودهنگام ریشه شر است. اول کد تمیز بدون memoization بنویس، اندازه بگیر، گلوگاه را در پروفایلر پیدا کن و فقط بعداً useCallback را در جاهایی که واقعاً لازم است اضافه کن.
// بهینهسازی قابل اندازهگیری: Child در React.memo پیچیده شده
const Child = React.memo(({ onClick }) => {
console.log('Child رندر مجدد شد');
return <button onClick={onClick}>Click</button>;
});
function Parent() {
const handleClick = useCallback(() => {
console.log('کلیک شد');
}, []);
return <Child onClick={handleClick} />;
}
طبق Dan Abramov — Before You memo() (2024)، بیش از 90 درصد موارد استفاده از useCallback در پروژههای متنباز اضافی است. توسعهدهندگان هر تابع را «برای هر احتیاطی» میپیچند بدون اینکه اثر آن را اندازه بگیرند. جایگزین: اگر کامپوننت فرزند سنگین است و رندر مجدد آن گران است، React.memo + useCallback توجیه دارد. اگر کامپوننت فرزند سبک است، رندر مجدد ارزانتر از مقایسه وابستگیهاست.
سناریوی اول — React.memo. اگر کامپوننت فرزند در React.memo پیچیده شده و تابع callback را بهعنوان prop میپذیرد، بدون useCallback کامپوننت فرزند در هر رندر والد دوباره رندر میشود، حتی اگر دادههای خودش تغییر نکرده باشد. useCallback ارجاع را پایدار میکند و React.memo میتواند رندر مجدد را بهدرستی رد کند.
سناریوی دوم — useEffect با callback در وابستگیها. اگر تابع به آرایه وابستگیهای useEffect ارسال شود، هر ارجاع جدید افکت را دوباره اجرا میکند. useCallback تضمین میکند که ارجاع پایدار است و افکت فقط هنگام تغییر دادههای واقعی اجرا میشود، نه در هر رندر. این برای اشتراکها و درخواستها بسیار مهم است.
// useCallback برای وابستگی پایدار useEffect
const fetchData = useCallback(async (id) => {
const res = await fetch(`/api/${id}`);
setData(res.data);
}, []); // ارجاع پایدار، هرگز دوباره ساخته نمیشود
useEffect(() => {
fetchData(props.id);
}, [props.id, fetchData]); // افکت فقط هنگام تغییر props.id اجرا میشود
تفاوت اصلی بین useCallback و useMemo در این است که هر کدام چه چیزی را memoize میکنند. useCallback تابع را memoize میکند: useCallback(fn, deps) همان fn (نسخه یکسان یا قبلی) را برمیگرداند. useMemo نتیجه فراخوانی تابع را memoize میکند: useMemo(() => computeExpensive(a, b), [a, b]) مقدار محاسبهشده را برمیگرداند، نه تابع را.
از نظر فنی، useCallback شکر نحوی بر روی useMemo است: useCallback(fn, deps) معادل useMemo(() => fn, deps) است. این نحو فقط برای خوانایی وجود دارد — تا توسعهدهنده بهوضوح ببیند که دقیقاً تابع memoize میشود، نه مقدار. تفاوتی در عملکرد بین useCallback و useMemo با تابع وجود ندارد — آنها کد یکسانی تولید میکنند.
| هوک | memoize میکند | نحو | کاربرد |
|---|---|---|---|
| useCallback | تابع (ارجاع) | useCallback(fn, deps) | callback برای کامپوننتهای فرزند |
| useMemo | نتیجه محاسبه | useMemo(() => value, deps) | محاسبات گران، memoization اشیا |
// اینها معادل هستند:
const handleClick = useCallback(() => doSomething(a, b), [a, b]);
const handleClick = useMemo(() => () => doSomething(a, b), [a, b]);
رایجترین خطا — پیچیدن بیمعنای همه توابع در useCallback بدون React.memo روی کامپوننتهای فرزند است. اگر کامپوننت فرزند در React.memo پیچیده نشده باشد، در هر رندر والد دوباره رندر میشود، صرفنظر از اینکه ارجاع callback تغییر کند یا نه. useCallback بدون React.memo — هزینه بدون منفعت است.
// ❌ بیفایده: بدون React.memo روی child
const handle = useCallback(() => doStuff(), []);
<Child onClick={handle} />; // Child بدون React.memo همچنان رندر مجدد میشود
// ❌ Stale closure: وابستگی جاافتاده
const handle = useCallback(() => {
console.log(count); // count همیشه ۰ است — stale closure!
}, []);
// ✅ درست: وابستگیها را ذکر کنید
const handle = useCallback(() => {
console.log(count);
}, [count]);
مشکل stale closure در useCallback با ذکر همه متغیرهای استفادهشده در آرایه وابستگیها حل میشود. eslint-plugin-react-hooks با exhaustive-deps بهطور خودکار بررسی میکند که همه متغیرهای داخل بدنه callback در آرایه حضور دارند. اگر callback از setState استفاده کند که بین رندرها تغییر نمیکند، میتوان با خیال راحت آن را در deps ذکر کرد — React پایداری setState را تضمین میکند.
سؤالات متداول
خیر. useCallback فقط در سه مورد معنا دارد: کامپوننت فرزند در React.memo پیچیده شده، تابع در آرایه وابستگیهای useEffect استفاده میشود، یا تابع به هوک سفارشی وابسته به reference equality ارسال میشود. در سایر موارد useCallback هزینه اضافه بدون منفعت است. تیم React توصیه میکند ابتدا بدون بهینهسازی بنویسید و آنها را بر اساس نتایج پروفایلینگ اضافه کنید.
برای کامپوننتهای ساده — تابع جدید در هر رندر کمی سریعتر است، زیرا useCallback منابع را صرف مقایسه وابستگیها و تخصیص آرایه میکند. برای کامپوننتهایی با درختهای عمیق React.memo — useCallback برنده است و از رندر مجدد هزاران عنصر فرزند جلوگیری میکند. اندازه بگیر و مقایسه کن، حدس نزن — برای ارزیابی عینی از React DevTools Profiler استفاده کن.
بله، useCallback با توابع async دقیقاً مانند توابع همزمان کار میکند. هوک خود تابع را memoize میکند، و نتیجه (Promise) هر بار هنگام فراخوانی برگردانده میشود. تابع ناهمزمان داخل useCallback الگویی رایج برای callbackهای پایدار بارگذاری داده است که در useEffect استفاده میشوند: const fetchData = useCallback(async (id) => {...}, []).
از React DevTools Profiler استفاده کن — نشان میدهد کدام کامپوننتها رندر مجدد میشوند و چرا. برای بررسی برنامهای console.log یا useWhyDidYouUpdate — کتابخانهای که دلیل رندر مجدد را ثبت میکند — اضافه کن. دلایل اصلی: تغییر prop (از جمله ارجاع callback)، تغییر state یا زمینه. اگر useCallback کمکی نمیکند، بررسی کن که همه وابستگیها بهدرستی ذکر شدهاند.
حتی بدون React.memo، useCallback میتواند در ترکیب با useMemo برای value زمینه مفید باشد. اگر شیءای با توابع به Context.Provider ارسال میکنی، ساخت شیء را در useMemo بپیچ و هر تابع را در useCallback. این کار از رندر مجدد همه مصرفکنندگان زمینه هنگام تغییر یکی از توابع جلوگیری میکند. اما برای ارسال مستقیم callback در props بدون React.memo، useCallback فایدهای ندارد.
خلاصه
ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد
IT Sectr از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.
همچنین بخوانید