قلاب ریداکس در ری اکت – راهنمای مقدماتی
قلاب ریداکس در ری اکت از تاریخ آوریل 2019 به صورت عمومی عرضه شدهاند. در این تاریخ تیم ریداکس مجموعهای از قلابها برای کامپوننتهای تابعی معرفی کرد که جایگزین متد ()connect در نسخه 7.1 آلفا میشوند. هر چیزی که قبلاً با متد ()connect استفاده میشد، از قبیل لینک کردن کامپوننتها به استور ریداکس و استخراج حالت ریداکس و ارسال متد به صورت props اکنون با استفاده از قلابها پیادهسازی میشود. اکنون میتوانیم چندین متد useDispatch را با هم دستهبندی کنیم و در نتیجه کامپوننت تنها یک بار رندر مجدد میشود و استخراج حالت با useSelector به صورت تقسیمبندی شده انجام مییابد.
در این مقاله به بررسی جدیدترین قلابهای داخلی ریداکس میپردازیم. همچنین برخی از ابزارهای پیرامونی که همراه با آنها توسعه یافتهاند و رویههای مناسب برای پیادهسازیشان را مورد بررسی قرار میدهیم.
تاریخچه مختصر قلابهای ریداکس
مانند هر بهروزرسانی عمده در پکیجهای دیگر، معرفی API قلابهای ریداکس نیز موجب بروز برخی مشکلات در مواردی شد که برخی از قلابهای اولیه پیشنهاد و پیادهسازی شده حذف شدند. از عمده این موارد شامل useRedux و useActions هستند که با انتشار نسخه 7.1 حذف شدند و به جای مکانیسم استخراج استور ریداکس در یک کامپوننت تابعی اینک از قلاب useSelector استفاده میشود.
از آن زمان API قلاب تثبیت شده است و عمده تمرکز پکیج روی بهینهسازیهای پسزمینه بوده است. تطبیقپذیری ریداکس با ریاکت نیتیو بهبود یافته است و اکنون با هر دو دسته پروژههای مبتنی بر ریاکت و ریاکت نیتیو بدون هیچ مشکلی ادغام میشود. یک باگ عمده شامل قلاب useEffect حل شده است که عنوان آن باگ زمانبندی (timing) بود و منجر به بهروزرسانیهایی میشد که کاربر انتظار آنها را نداشت. این مشکل به تنهایی به جهت عملکرد غیرمترقبه، مانع استفاده بسیاری از اپلیکیشنهای پروداکشن از قلاب ریداکس میشد. قلابهای ریاکت اکنون در پسزمینه و درون API-های درونی ریداکس شامل متد ()connect مورد استفاده قرار میگیرند. روشن است که پروژه اکنون و پس از تقریباً سپری شدن یک سال از معرفیاش در کنفرانس ریاکت 18، کاملاً با API-های قلاب ریاکت همگام شده است.
اگر بخواهیم بر مبنای آمار دانلودهای هفتگی ریداکس (+) بگوییم که تأخیر پیادهسازی قلابها بر نصب مبنا تأثیر گذاشته است یا نه کار دشواری خواهد بود. در زمانی که قلابها معرفی شدند، با یک کاهش مواجه شدهایم که احتمالاً تا حدودی ناشی از هیاهوی بیش از حدی است که رسانههای اجتماعی پدید آوردهاند و شاید نوعی شک در مورد این مسئله ایجاد کرده است که آیا ریداکس میتواند با برخی API های سبک و ساده برای مدیریت حالت که هم اینک وجود دارند سازگار باشد یا نه. اکنون هیاهوی قلابها تا حدودی فروکش کرده است و پکیج دوباره در حال پیمودن مسیر صعودی خود است. با این که توصیه میشود که همواره API-های جدیدتر ریاکت را به کار بگیریم، اما بدیهی است که در نهایت بهینهسازیهای ریداکس به مسیر حالت concurrent منتهی خواهد شد.
در زمان نگارش این مقاله در شهریور 1398 نسخه موجود ریداکس 7.13 است که با توجه به تست و تکرار قلاب احتمالاً بهترین زمان برای شروع استفاده از قلابهای ریداکس با سطحی از پایداری است که برای اپلیکیشنهای پروداکشن مورد نیاز است.
قلابهای ریداکس کارکردهای نوینی معرفی کردهاند
قلابهای ریداکس جایگزینی برای ()connect محسوب نمیشوند، بلکه پیادهسازی کاملاً مجزایی هستند که دارای خصوصیتهای متفاوتی هستند و استفاده از آنها برخی مزایای خاص خود را دارد. برای نمونه پس از بررسی useSelector روشن میشود که یک قلاب بسیار جذاب است و بسطپذیری بیشتری نسبت به آن چه در ابتدا تصور میشود فراهم میسازد. ما در ادامه این مقاله useSelector را با جزییات بیشتری بررسی میکنیم و استفاده از آن را به همراه پکیجهایی مانند reselect تست کرده و ظرفیتهای useSelector را بیش از پیش بسط میدهیم.
درک قلاب useSelector
useSelector را میتوان معادلی برای شیء mapStateToProps دانست که به ما اجازه میدهد دادهها را از استور ریداکس استخراج کنیم و هرزمان که کانتینرش رندر شود فراخوانی خواهد شد. در واقع عملاً جایگزین متد ()connect با آرگومان mapStateToProps شده است.
useSelector جایگزین چه میشود؟
در دوران پیش از معرفی قلابهای ریداکس ما عادت داشتیم که از mapStateToProps استفاده کنیم و آن را پیش از قرار دادن درون «کامپوننت ارائهای» (Presentational Component) با HOC به ()connect ارسال کنیم:
این متد هم با کامپوننتهای کلاسی و هم کامپوننتهای تابعی کار میکرد. شما ملزم نیستید که صرفاً از قلابهای ریداکس با کامپوننتهای تابعی استفاده کنید. اما روی دیگر این سکه آن است که قلابها نمیتوانند در کامپوننتهای کلاسی استفاده شوند.
در مثال فوق ما صرفاً یک مقدار counter از یک استور ریداکس بازگشت دادهایم، اما با نادیده گرفتن آرگومان دوم mapDispatchToProps صرفاً مقدار null ارائه میشود. این کد قالبی به صورت ایجاد کامپوننتهای بیشتر همراه با تعریف کردن آرگومانهای مربوطه هزینه کوچکی نیز دارد که با رشد اندازه اپلیکیشن، افزایش مییابد:
- با افزودن کد قالبی (boilerplate) پیچیدگی افزایش مییابد.
- منطق کامپوننت به خارج از کامپوننت گسترش مییابد و معمولاً در یک فایل دیگر یا در کامپوننت بزرگتری قرار میگیرد.
useSelector مشکل دوم فوق را به صورت مؤثری حل میکند. قلاب درون کامپوننتهای تابعی تعریف شده است و همه منطق کامپوننت را در یک محل نگهداری میکند. اما وقتی نکته اول را بررسی میکنیم با موارد جالبی مواجهی شویم. کد قالبی ما در واقع زمانی که از نگاشت سادهای به استور استفاده میکنیم، سادهسازی شده است، اما استفاده از کتابخانههای سلکتور مانند reselect نیز به همراه useSelector نتیجه خوبی دارد. در ادامه استفاده از reselect را نیز بررسی میکنیم.
کاربرد ساده useSelector
useSelector دو آرگومان میگیرد که یکی خود تابع سلکتور و دیگری «تابع برابری» (equality function) است. بدین ترتیب گزینهای برای ابطال رفتار پیشفرض یک مقایسه سطحی بین حالت قبلی و حالت کنونی استور فراهم میآید:
تابع برابری میتواند منطقی سفارشی برای مقایسه حالت با حالت رندر شده قبلی فراهم سازد. بر اساس تجربه این وضعیت میتواند برای محدودسازی رندرهای مجدد کامپوننت صرفاً به مواردی که معیار مربوطه صراحتاً تأمین شده باشد، مورد استفاده قرار گیرد. این معیار صریحتر از مقایسه سطحی است. این وضعیت به طور معمول به شکل مقایسه زیرمجموعهای از props به جای مقایسه همه آنها بروز مییابد. اگر یک کامپوننت به API-ها گوش دهد تا رندر مجدد را تشخیص دهد (مثلاً وبسوکتها یا صرفاً یک درخواست fetch)، میتوانیم گزارههای شرطی دیگری نیز تدارک ببینیم تا رندرهای مجدد را بر مبنای سرورهای بکاند نیز کنترل کنیم.
اگر کار را با یک پیادهسازی پایهای برای بازگشت دادن یک مقدار منفرد از یک استور ریداکس آغاز کنیم، باید یک قلاب ریداکس مانند زیر تعریف کنیم:
مثال فوق صرفاً یک مقدار number بازگشت میدهد و به ما امکان میدهد که نتیجه counter را مستقیماً در JSX جاسازی کنیم. ضمناً توجه داشته باشید که آرگومان دوم یعنی تابع برابری را نیز نادیده گرفتهایم. این وضعیت میتواند به سادگی یک useSelector باشد.
فرض کنید میخواهیم یک شیء با دادههای بیشتری برای مقداردهی کامپوننت هدر بازگشت دهیم و ساختار فوق را تا حدودی بسط میدهیم:
توجه کنید که اینک یک شیء جدید با ارجاع دادن به چند مقدار از استور ریداکس میسازیم. بدین ترتیب به نکته جالبی در مورد استفاده از چند useSelector میرسیم و آن این است که میتوانستیم به جای بستهبندی همه قلابها در یک header، دو قلاب تعریف کنیم. مثلاً میتوانستیم یک قلاب به نام user و قلاب دیگری به نام notifications تعریف کنیم. در این حالت کامپوننت بر مبنای هر تغییری که در این مقادیر قلاب رخ میدهد، میتواند رندر مجدد شود. اما در نهایت این تصمیم شما است که useSelector-ها را چگونه ساماندهی کنید.
همانند ()connect در این مورد نیز میتوانیم props را مستقیماً به درون useSelector ارسال کنیم. تنها تفاوت در این است که این بار به جای تکیه بر پارامتر ownProps در ()connect باید به props ارجاع بدهیم:
برای مشاهده مثالهای بیشتر در مورد useSelector پیشنهاد میکنیم به مستندات رسمی (+) سر بزنید. یکی از دلایلی که از بسط ظرفیتهای سلکتور سرباز زدیم، کتابخانه سلکتور reselect بود. این مورد را نیز در ادامه بیشتر توضیح میدهیم.
استفاده از Reselect با useSelector
برخی اوقات ارسال مقادیر مستقیماً از استور ریداکس به useSelector به تنهایی کافی نیست. برای نمونه اگر دادهها در حالت خام هستند و باید پردازش بیشتری روی آنها انجام یابد تا برای مصرف کامپوننت آماده شوند این وضعیت مصداق مییابد. این همان جایی است که Reselect وارد بازی میشود. Reselect به ما یک API ساده میدهد که به صورت افزایشی، پردازشهای بیشتر برای قالببندی حالت ریداکس را اجرا میکند.
البته این پکیج حدود یک سال است که بهروز نشده است و در همان نسخه 4 باقی مانده است. با این حال شاهد استفاده روزافزون از این کتابخانه هستیم و دانلودهای هفتگی از 1.5 میلیون عدد تجاوز کرده است. بخشی از موفقیت Reselect در سهولت کاربرد آن با ریاکت و ریداکس و به طور خاص قلابهای ریداکس است.
با بررسی یکی از مثالهای مستندات رسمی و پیوند دادن آن به یک سناریوی سبد خرید فروشگاه میتوانیم آیتمهای سید خرید را از یک استور ریداکس بگیریم و سپس مراحل دیگری برای قالببندی دادههای آن اجرا کنیم. این کار را با استفاده از تابع createSelector به صورت زیر انجام میدهیم:
گام نخست این تنظیمات چیزی جدیدی ندارد و صرفاً آیتمهای سبد خرید را با استفاده از useSelector مانند مراحل پیشین واکشی میکنیم.
اما در گام 2 متد createSelector را برای پردازش بیشتر cartItems استفاده کردهایم. از cartItems به صورت یک مقدار مبنا استفاده میکنیم و به گزاره بعدی به صورت یک آرگومان تابع ارسال میکنیم. این تابع به پردازش بیشتر cartItems میپردازد تا مجموع جزئی سبد را محاسبه کند.
createSelector تعدادی از تابعها را میپذیرد که هر کدام مقادیر را از گامهای پیشین به عنوان تنها آرگومانهایشان میگیرند. در مورد فوق items به صورت تنها آرگومان برای استفاده در گزاره reduce جهت تجمیع مقادیر مجموع استفاده میشود.
در نهایت createSelector نتیجه آرگومانهای قبلی را به صورت آرگومانهایی به تابع گام بعدی ارسال میکند.
در اینجا از متد Resuce جاوا اسکریپت استفاده کردهایم که نباید با ریداسرهای ریداکس اشتباه گرفته شود.
در مثال فوق فرض کنید فروشگاه آنلاین در کشوری فعالیت میکند که مبلغ ارزش افزوده 20% باید روی مقدار هر آیتم سبد خرید اعمال شود. میتوانیم یک گام اضافی به صورت cartSubtotal برای محاسبه این مقدار اضافی طراحی کنیم:
اگر بخواهیم مثال فوق را تجزیه کنیم:
یک گام اضافی به صورت createSelector طراحی کردهایم که مقدار مالیات هر آیتم را در سبد خرید محاسبه میکند. این کار با نگاشت آرایه cartItems با map() انجام مییابد و یک آرایه جدید از مقادیر مالیات بازگشت مییابد که متناظر با هر آیتم سبد خرید است.
گام آخر اکنون دو آرگومان میگیرد که یکی آیتمهای سبد خرید و دیگری آرایه مالیات متناظر است. اکنون یک گزاره reduce() اضافی برای مقادیر مالیات معرفی میکنیم. این بار کار خود را با مقدار اولیه itemsSubtotal آغاز میکنیم. در نهایت مجموع جزئی را به مقدار مالیات کلی اضافه میکنیم و نتیجه را بازگشت میدهیم.
همچنین میتوانیم createSelector-ها را به createSelector-ها ارسال کنیم. در نهایت یک هزینه ارسال را به مجموع کل اضافه میکنیم:
cartTotal یک شیء با یک فیلد total بازگشت میدهد که به منظور روشنتر شدن موضوع طراحی شده است؛ اما باید مجموع را صرفاً به صورت یک number بازگشت دهد.
این مثال ساده نشان میدهد که چطور میتوانیم گزارهها را با createSelector ترکیب کنیم تا مقادیر حالت را بیشتر در دادههای قابل استفاده که خاص کامپوننت هستند، پردازش کنیم. دو مزیت استفاده از قلابهای ریداکس در مورد API مربوط به Reselect نیز صدق میکند: ساختار کمینهای دارد و همه منطق آن درون کامپوننت جای گرفته است.
برای این که دانش خود را از کتابخانه Reselect بیش از این مقدمه مختصر گسترش دهید و با جزییات بیشتری از آنچه ارائه میدهد آشنا شوید، بهتر است از مستندات رسمی آن (+) استفاده کنید که طیف وسیعی از کاربردهای آن را شامل memorization و ارسال props به درون سلکتورها پوشش میدهد.
اکنون به بررسی جزییات useSelector پرداختهایم و نوبت توجه به useDispatch رسیده که قلاب ریداکس برای توزیع کردن اکشنها است.
توزیع اکشنها با قلاب useDispatch
useDispatch صرفاً یک جایگزین mapDispatchToProps محسوب نمیشود، چون هیچ چیزی را نگاشت نمیکند، بلکه صرفاً یک ارجاع به تابع ()dispatch بازگشت میدهد که درون ریداکس قرار دارد. برای استفاده از آن کافی است، قلاب را درون یک کامپوننت تعریف کنیم:
اکنون توزیع کردن اکشنها به سادگی قرار دادن آنها درون یک dispatch است. مثلاً برای توزیع کردن یک اکشن SIGN_IN به صورت زیر عمل میکنیم:
ساختار + new Date() / 1000) اقدام به بازگشت دادن timestamp جاری یونیکس به صورت ثانیه میکند. استفاده از جاوا اسکریپت به صورت داخلی موجب میشود که اندازه بسته کوچکتر بماند. صرفاً در مواردی که به دستکاریهای پیچیدهتر تاریخ/زمان و یا قالببندی UI نیاز دارید از momentJS استفاده کنید.
در این جا توجه داشته باشید که نوع dispatch را مستقیماً درون JSX ارسال کردهایم و در مورد مقادیر payload از props کامپوننت والد بهره گرفتهایم. تعریف کردن این اکشنها در یک فایل اختصاصی و ایمپورت کردن آنها نیز گزینه مناسبی محسوب میشود:
در واقع این یکی از اصول اساسی ریداکس محسوب میشود، اما به خاطر سپردن آن همواره ارزش دارد. در مثال فوق با اضافه کردن یک نوع خاص به هر payload اکشن به صورت SignInPayload مطمئن میشویم که از دادههای صحیحی در یک محیط تایپ اسکریپت استفاده میکنیم. این نوع ممکن است چیزی مانند زیر باشد:
از این جا به بعد میتوانیم setSignIn را در هر کامپوننت برای توزیع اکشن ایمپورت کنیم:
برخی اوقات زمانی که با متدهای dispatch کار میکنیم، میخواهیم تابعهای خاصی را memorize کنیم تا نیازی به ارجاع مجدد در زمان رندر دوباره نباشد. ریاکت قلابهای داخلی برای حل این مشکل ارائه کرده است.
Memorize کردن useDispatch با useCallback
ما به عنوان توسعهدهنده همواره تلاش میکنیم که اپلیکیشنها را از طریق «تکرار» سادهسازی کنیم. در اغلب موارد لازم است که اکشنهای dispatch را به صورت props به کامپوننتهای فرزند ارسال کنیم. در چنین مواردی نمیخواهیم هر بار که کامپوننت والد رندر مجدد میشود، ارجاع جدیدی به رویداد dispatch ارسال کنیم چون به احتمال زیاد تغییری در رویداد dispatch رخ نداده است.
در چنین مواردی میتوانیم useDispatch را memoize کنیم و همزمان از قلاب useCallback ریاکت بهره بگیریم که متد dispatch درون آن قرار میگیرد و به صورت تنها وابستگی با آن رفتار میشود:
اکنون یک ارجاع جدید signIn تنها در صورتی ایجاد میشود که dispatch تغییر یابد. این اتفاق هرگز نخواهد افتاد، زیرا همواره تابع ()dispatch را به صورت درونی به ریداکس بازگشت میدهد.
useCallback یک قلاب کاربردی است که هر تابعی را memorize میکند و تنها در صورتی که وابستگی تغییر یابد، یک ارجاع به تابع درونی ایجاد میکند:
توجه کنید که a و b به احتمال زیاد props هستند یا مقادیری هستند که از props مشتق شدهاند و ممکن است در زمان برخی رندرهای مجدد تغییر یابند. اما تا زمانی که این اتفاق نیفتاده است نیازی به تعریف مجدد memoizedCallback نخواهیم داشت. بنابراین کامپوننت به نسخه کَششدهای از تابع اشاره میکند که useCallback در رندر اولیه ذخیره ساخته است.
بدن ترتیب بهتر است در برخی مواقع خاص اپلیکیشن خود را مورد بررسی قرار دهید تا ببینید آیا useCallback یا قلاب useMemo میتواند از رندرهای مجدد غیرضروری درون درخت کامپوننت چه در ریداکس و چه تابعهای دیگر جلوگیری کند. useMemo معادل useCallback است، اما مقادیر بازگشتی را به جای خود تابع memorize میکند.
استفاده از ()batch برای دستهبندی رویدادهای dispatch
ریداکس برای بهینهسازی هر چه بیشتر عملکرد تابعی به نام batch() نیز ارائه کرده است که میتواند برای بستهبندی چند رویداد dispatch در کنار هم مورد استفاده قرار گیرد. هر زمان که فراخوانی دو تابع dispatch ممکن است موجب دو رندر مجدد شود، میتوان با استفاده از ()batch از این کار جلوگیری کرد.
یک سناریوی احراز هویت را تصور کنید که در آن دادههای کاربر، دادههای احراز هویت و پیکربندی UX باید در استور ریداکس ذخیره شوند. بدین ترتیب میتوان از ()batch برای بهروزرسانی طیفی از ریداسرها در یک رندر استفاده کرد:
پیادهسازی ()batch عموماً در زمانی استفاده میشود که کارکرد اپلیکیشن تکرار شود و از همان ابتدا برای استفاده از آن برنامهریزی نمیشود.
قلابهای دیگر ریداکس
یکی دیگر از قلابهای دیگر ریداکس که باید به آن اشاره کنیم useStore است. useStore مقدار کلی استور ریداکس را که به <Provider /> سطح بالا ارسال شده است و به طور معمول کل اپلیکیشن را در بر میگیرد، بازگشت میدهد.
با این که کاربرد useStore دیگر توصیه نمیشود و با useSelector جایگزین شده است، اما در ریداکس میتوان از آن برای جایگزینی ریداسرها استفاده کرد. برخی مواقع هم میخواهیم کل استور را برای مقاصد دیباگ دریافت کنیم. مثلاً ممکن است کامپوننت در زمان یک بهروزرسانی استور در موقع استفاده از useSelector رندر مجدد نشود. در این موارد یافتن یک استور ریداکس در یک حالت خاص یک اپلیکیشن برای آنالیز استور در آن لحظه زمانی خاص کار دشواری محسوب میشود.
استفاده از useStore تقریباً به سادگی useDispatch است و هیچ پارامتری ندارد:
سخن پایانی
در این مقاله تلاش کردیم به صورت ابتدایی در مورد استفاده از قلابهای ریداکس در ریاکت صحبت کنیم. اکنون شما ایدهای کلی از امکاناتی که قلابهای ریداکس در کامپوننتهای تابعی فراهم میسازند و مزیتهای آنها به دست آوردهاید:
useSelector یک API ساده شده برای دریافت حالت استور ریداکس محسوب میشود و به ما امکان میدهد که از props مستقیماً برای فیلتر کردن حالت استور بر مبنای آن props بهره بگیریم.
استفاده از کتابخانه reselect الزامی نیست، ولی میتواند برای پردازش بیشتر دادههای استور ریداکس استفاده شود و دستکاری های بیشتر دادهای را پیش از تعریف کردن متد رندر اجرا کرد. Reselect به یک پکیج محبوب برای انجام این کار با یک API کمینه تبدیل شده است و رویکرد مناسبی برای پردازش دادهها ارائه میکند.
قلاب useDispatch یک ارجاع به متد ()dispatch برای توزیع اکشنها محسوب میشود. تابعهای Dispacth نیز میتواند با استفاده از قلاب useCallback ریاکت memorize شوند. برای بهینهسازی بیشتر متد ()batch ریداکس میتواند چندین فراخوانی dispatch را با هم جمع کند و در نتیجه تنها یک رندر مجد کامپوننت رخ دهد.
همان طور که در بخش تاریخچه مختصر در ابتدای مقاله توضیح دادیم، اینک بهترین زمان استفاده از قلابهای ریداکس برای محیط پروداکشن است زیرا API تثبیت شده است و باگهای عمده آن نیز رفع شدهاند. تبدیل کردن اپلیکیشنها از رویکرد ()connect قبلی به این رویکرد جدید نباید کاری ضروری تلقی شود. باید مزیتهای این کار از قبیل ساختار کمینه یا سازگاری همه کامپوننتها با یک استایل خاص برای توسعه روانتر سنجیده شود. در هر حال میتوانید از پیادهسازی قلابهای ریداکس و بهرهگیری از مزیتهای این جدیدترین بهروزرسانی ریداکس لذت ببرید.
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای JavaScript (جاوا اسکریپت)
- مجموعه آموزشهای برنامهنویسی
- آموزش JavaScript ES6 (جاوا اسکریپت)
- ریداکس (Redux) — مبانی مقدماتی
- راهنمای مقدماتی ریداکس (Redux) — به زبان ساده
==