React هوکهای مختلفی را ارائه میدهد که مدیریت state برنامه و سایر ویژگیهای React را در کامپوننتهای فانکشنال آسانتر میکند. هوکها ویژگیهای کلاس کامپوننتها را برای کامپوننتهای فانکشنال فراهم میکنند و در مقایسه با کلاس کامپوننتها به کدنویسی کمتری نیاز دارند. در این بین، ما useMemo و useCallback را داریم که درک تفاوت بین این دو هوک و به کار گرفتن آنها به بهبود عملکرد برنامههای ما کمک میکند.
در این مقاله قصد داریم تا در مورد هر دو هوک useMemo و useCallback بحث کنیم و با تفاوت بین آنها و زمان استفاده از هر هوک بیشتر آشنا شویم.
هوک useMemo مقدار بازگشتی یک محاسبه پرهزینه بین رندرها را به خاطر میسپارد. Memoizing به معنای ذخیره value
به عنوان یک مقدار ذخیرهشده در حافظه cache است تا نیازی به محاسبه مجدد نباشد، مگر اینکه لازم باشد این کار را مجددا انجام دهیم.
useMemo هوکی است که برای بهینهسازی عملکرد رندرهای ما مورد استفاده قرار میگیرد. به طور معمول، زمانی که یک متغیر را در داخل یک کامپوننت تعریف میکنیم در هر بار رندر شدن، آن متغیر دوباره ایجاد میشود. اگر این متغیر مقدار بازگشتی یک تابع را ذخیره کند، هر بار که کامپوننت ما رندر میشود، تابع فراخوانی میگردد.
معمولا این موضوع باعث ایجاد مشکل نمیشود. اما، اگر تابعی که داریم یک تابع پرهزینه باشد چه اتفاقی میافتد؟ به عنوان مثال:
function calculate() { let result = 0; for (let i = 0; i < 1000000000; i++) { result += i; } return result; } function App1() { const [count, setCount] = useState(0); const value = calculate(); return ( <div className="App"> <button onClick={() => setCount(count + 1)}>Increment Count</button> <p>Count: {count}</p> </div> ); }
هنگامی که روی Increment Count
کلیک میکنیم، چند ثانیه طول میکشد تا state شمارش بهروزرسانی شود. این اتفاق به این دلیل است هر بار که یک کامپوننت پس از ایجاد تغییری در state مجددا رندر میشود، تابع calculate
نیز دوباره اجرا میگردد.
حال تصور کنید که چندین متغیر state در کامپوننت خود داشته باشیم که هر کدام هدف خاص خود را دنبال میکنند و هر بهروزرسانی state، باعث رندر مجدد و اجرای این تابع پرهزینه میشود.
این متغیرهای state ممکن است کاملاً با محاسبه پرهزینه انجام شده نامرتبط باشند که باعث تاخیرهای غیرضروری میشود. همین موضوع بر روی عملکرد وبسایت ما تأثیر منفی میگذارد و میتواند منجر به ایجاد تجربه کاربری نامناسب شود.
هوک useMemo میتواند به ما در حل این مشکل کمک کند. ابتدا سینتکس آن را بررسی میکنیم:
const value = useMemo(expensiveFunction, [...dependencyArray])
ما باید این هوک را در قسمت بالای کامپوننت خود تعریف کنیم. آرگومانهایی که هوک useMemo دریافت میکند به شرح زیر میباشد:
expensiveFunction
شامل محاسبه پرهزینهای است که میخواهیم آن را انجام دهیم. اگر تابع را در خارج تعریف کرده باشیم، فقط رفرنس تابع را بدون ()
ارسال میکنیم. همچنین میتوانیم به طور مستقیم از arrow functionها برای تعریف تابع داخل هوک استفاده کنیم.dependencyArray
حاوی لیستی از dependencyها برای هوک است. تابع پرهزینه تنها زمانی فراخوانی میشود که یکی از این dependencyها بهروزرسانی شود. ما میتوانیم متغیرهای state یا propهای وابسته به این محاسبه را ارسال کنیم. سایر بهروزرسانیهای state تابع را فعال نمیکند.در اولین رندر، هوک useMemo نتیجهی expensiveFunction
را return کرده و آن را ذخیره میکند. در طول رندرهای بعدی، اگر هیچ یک از dependencyها تغییر نکرده باشد، useMemo مقدار ذخیره شده را return میکند. اما اگر هر کدام از این dependencyها تغییر کنند، تابع را مجددا فراخوانی کرده و اجرا مینماید.
اکنون از هوک useMemo در مثالی که داریم استفاده میکنیم:
const [dependentCount, setDependentCount] = useState(10); const value = useMemo(calculate, [dependentCount]); return ( <div className="App"> // ... <button onClick={() => setDependentCount(dependentCount + 1)}> Increment Dependent Count </button> <p>Dependent Count: {dependentCount}</p> </div> );
ما یک state dependentCount
دیگر ایجاد کردهایم که فرض میکنیم وابسته به محاسبه پرهزینهای که داریم، میباشد. زمانی که این state کامپوننت را بهروزرسانی و رندر میکند، تابع calculate
اجرا میشود. اما اگر هر state دیگری تغییر کند، useMemo به جای اجرای مجدد تابع، مقدار ذخیره شده را return میکند.
اکنون، هنگامی که روی Increment Count
کلیک میکنیم، رندر سریعتر انجام میشود. زیرا تابع calculate
در هر رندر فراخوانی نمیگردد. این اتفاق در طول هر بهروزرسانی state دیگری که در آرایه dependency هوک useMemo فهرست نشده است، یکسان میباشد.
اما هنگامی که روی Increment dependent Count
کلیک میکنیم، مدت زمانی طول میکشد تا مقدار بهروزرسانی شده رندر شود. این به این دلیل است که dependentCount
یک dependency مربوط به useMemo است و تغییر آن، تابع پرهزینه را فراخوانی میکند؛ بنابراین زمان میبرد تا کامپوننت مجددا رندر شود.
به این ترتیب، با استفاده از هوک useMemo، میتوانیم اجرای یک تابع پرهزینه را با فراخوانی آن فقط برای بهروزرسانیهای stateای که در واقع به مقدار بازگشتی آن نیاز دارند، کنترل کنیم. همین موضوع میتواند عملکرد برنامه ما را به شدت بهبود بخشد.
useEffect
وابسته به آرایه یا آبجکت باشد.Array.map
رندر میکنیم که نیازی به تغییر ندارند مگر اینکه مقدار state خاصی تغییر کند.هوک useCallback یک تابع callback را به خاطر میسپارد و آن را return میکند.
باید به این نکته توجه داشته باشیم که useCallback خود تابع را حفظ میکند، نه مقدار بازگشتی آن را. همچنین این هوک تعریف تابع یا رفرنس تابع را نیز در حافظه cache ذخیره میکند. اما، هوک useMemo مقدار بازگشتی توابع را در حافظه cache ذخیره میکند تا دیگر نیازی به اجرای تابع نباشد.
تابعی که در داخل یک کامپوننت تعریف شده است، در هر رندر کامپوننت، شبیه به یک متغیر، دوباره ایجاد میشود. اما تفاوت این است که هر بار با یک رفرنس متفاوت رندر میشود. بنابراین، یک useEffect
وابسته به این تابع در هر رندر دوباره اجرا میگردد. مشابه این اتفاق در مورد کامپوننتهای child نیز اتفاق میافتد.
به عنوان مثال:
const App = () => { const [count, setCount] = useState(0); const [value, setValue] = useState(""); const handleClick = () => { setValue("Kunal"); }; return ( <div className="App"> <button onClick={() => setCount(count + 1)}>Increment Count</button> <p>Count: {count}</p> <p>Value: {value}</p> <SlowComponent handleClick={handleClick} /> </div> ); }; const SlowComponent = React.memo(({ handleClick, value }) => { // Intentially making the component slow for (let i = 0; i < 1000000000; i++) {} return ( <div> <h1>Slow Component</h1> <button onClick={handleClick}>Click Me</button> </div> ); });
در این مثال، ما یک SlowComponent
به عنوان کامپوننت child برای کامپوننت App
داریم. هنگامی که یک کامپوننت parent رندر میشود، همه کامپوننتهای child آن رندر میشوند، صرف نظر از اینکه آیا چیزی در آنها تغییر کرده است یا خیر.
برای جلوگیری از رندرهای غیرضروری کامپوننتهای child، ما معمولاً از تابع React.memo
استفاده میکنیم. این تابع اساساً کامپوننت را cache میکند و تنها در صورتی آن را دوباره رندر میکند که ویژگیهای آن تغییر کرده باشد.
اکنون، هنگامی که روی Increment Count
کلیک میکنیم، باز هم زمان زیادی طول میکشد تا رندر شود، زیرا SlowComponent
در تغییر state، دوباره رندر میشود. اما چرا اینطور است؟ ما هیچ یک از props آن را تغییر نمیدهیم.
در ظاهر، ممکن است اینطور به نظر برسد که ما مقدار prop handleClick
را تغییر نمیدهیم. اما از آنجایی که توابع با یک رفرنس متفاوت دوباره ایجاد میشوند، در هر رندر کامپوننت App
، کامپوننت child آن (که در این مثال SlowComponent
است) رندر میشود.
برای حفظ برابری ارجاعی، ما تعریف این تابع را در داخل یک هوک useCallback قرار میدهیم.
سینتکس هوک useCallback به شکل زیر میباشد:
const cachedFn = useCallback(fn, [...dependencyArray])
هوک useCallback آرگومانهای زیر را میگیرد:
fn
تابعی است که میخواهیم آن را cache کنیم. این تعریف تابعی است که میخواهیم آن را ایجاد کنیم، و میتواند هر آرگومانی را بگیرد و هر مقداری را return کند.dependencyArray
لیستی از dependencyها است که هر تغییری در آنها باعث ایجاد مجدد تابع میشود. ما میتوانیم مقادیر state یا propهای وابسته به این تابع را ارسال نماییم.در اولین رندر، React تابع را ایجاد کرده و آن را cache میکند. باید به این نکته توجه داشته باشیم که React آن تابع را فراخوانی نمیکند.
در رندرهای بعدی، این تابع که در حافظه cache ذخیره شده بود به ما return میشود. زیرا، هوک useCallback به جای مقدار بازگشتی تابع، خود تابع را return کرده و ذخیره میکند.
اکنون از هوک useCallback در مثالی که داریم استفاده میکنیم:
import { useCallback } from "react"; const App = () => { // ... const handleClick = useCallback(() => { setValue("Kunal"); }, [value, setValue]); // ... };
در این مثال، ما تابع را در داخل یک useCallback قرار دادهایم و دو dependency که با این تابع درگیر هستند را ارسال کردهایم. اکنون، هنگامی که روی Increment Count
کلیک میکنیم، رندر بسیار سریعتر است. دلیل این اتفاق این است که رفرنس handleClick
بین رندرها ذخیره میشود و بنابراین SlowComponent
دوباره رندر نمیگردد.
اما وقتی روی دکمه داخل SlowComponent
کلیک کنیم دوباره رندر میشود. این به این دلیل است که وقتی state value
تغییر میکند، متد handleClick
دوباره ایجاد میشود و بنابراین props کامپوننت کُند تغییر کرده است.
تا زمانی که state value
را بهروزرسانی نکنیم، میتوانیم stateهای بیشتری را به کامپوننت App
اضافه نماییم و بدون اینکه بر روی performance برنامه تاثیر منفی بگذاریم، آنها را بهروزرسانی کنیم.
useEffect
فراخوانی میکنیم، معمولاً تابع را به عنوان یک dependency ارسال مینماییم. برای جلوگیری از استفاده غیرضروری از useEffect
در هر رندر، بهتر است تعریف تابع را در داخل useCallback قرار دهیم.در این بخش قصد داریم تا به بررسی تفاوت بین دو هوک useMemo و useCallback بپردازیم.
همچنین چند نکته دیگر که باید به آنها توجه داشته باشیم این است که نباید از هوکهای useMemo و useCallback در همه جا استفاده کنیم؛ فقط در صورتی میتوانیم از این هوکها در برنامههای خود بهرهمند شویم که میخواهیم محاسبات پرهزینه را به خاطر بسپاریم یا از رندرهای غیرضروری جلوگیری کنیم.
برای عملکردهای معمولی، این هوکها تفاوت چندانی ندارند. استفاده بیش از حد از آنها کد ما را ناخوانا میکند. در عوض، میتوانیم از راههای دیگری برای بهبود عملکرد اپلیکیشن خود بهرهمند شویم.
useMemo و useCallback هوکهای مفیدی در React هستند که درک تفاوت بین این دو هوک و استفاده از آنها میتواند به ما در بهینهسازی عملکرد برنامههایی که داریم کمک کند.
در این مقاله نحوه عملکرد هر دو هوک را بررسی کردهایم. هوک useMemo نتیجه یک محاسبه پرهزینه را در حافظه cache ذخیره میکند، در حالی که هوک useCallback رفرنس تابع را در حافظه cache ذخیره مینماید. ما همچنین سناریوهایی را بررسی کردیم که چه زمانی باید از هر هوک استفاده کنیم. هر دوی این هوکها میتوانند سرعت برنامه ما را افزایش داده و تجربه کاربری را بهبود بخشند.