ادی ام. عاشق جاوااسکریپت و فعال ریاکت. علاقه به R&D دارم و اینجا از چیزایی که برام جالبن میگم. اگه هروقت هرکمکی از دستم برمیومد بهم بگید 3>
کی باید به setState تابع پاس بدیم؟
تابع setState رو توی ریاکت به دو شکل میتونیم استفاده کنیم.
۱- با پاس دادن مقدار، و ۲- با پاس دادن تابع.
تو حالت اول مقداری که پاس میدیم جایگزین مقدار قبلی استیت میشه (توی class component مقدار باید object باشه و با object قبلی merge میشه) و تو حالت دوم، ریاکت آخرین مقدار state رو بعنوان ورودی به تابعی که بهش دادیم میده و از مقدار خروجی تابع بعنوان مقدار برای setState کردن، مشابه حالت اول، استفاده میکنه
حالا سوالی که پیش میاد اینه:
چه زمانی ما نیاز داریم که به setState تابع پاس بدیم؟ و چرا؟
بعنوان یه حکم همیشگی، میشه گفت که هروقت مقدار جدید state به مقدار قبل از خودش وابسته بود، باید از function برای setState استفاده کنیم. ینی چی؟ به این مثال دقت کنید:
توی این مثال مقدار جدید value واضحا به مقدار قبلی خودش بستگی داره. ما در حال حاضر یه مقداری رو بعنوان value داریم که مثلا برابر ۱ هست و با استفاده از اون میخوایم value جدید که ۲ میشه رو بسازیم. اینجور مواقع بهتره که از function بجای پاس دادن مستقیم مقدار استفاده کنیم:
اینجا ما بجای اینکه مستقیم از value استفاده کنیم، یه تابع به setValue پاس دادیم. ریاکت آخرین مقدار value رو به تابع ما پاس میده و مقداری که return میشه رو داخل استیت ست میکنه.
برای این حکم همیشگی رو رعایت میکنیم، که اگر ۱۰۰٪ مواقع کدمون رو اینجوری بنویسیم، میتونیم مطمئن باشیم که هیچوقت هیچ مشکلی پیش نمیاد. هرچند که فقط ۲۰٪ مواقع مشکلساز باشن.
این ۲۰٪ مواقع کیها هستند؟
این ۲۰٪ مواقع دردسرساز، که ما رو مجبور به استفاده از function برای setState میکنند، به دو دسته تقسیم میشن.
دسته اول:
همه ما با نکته زیر توی جاوااسکریپت آشناییم. ولی انگار وقتی وارد ریاکت میشیم فراموشش میکنیم.
ما این رو میدونیم و مطمعنیم که firstLogger رو هروقت صدا بزنیم مقدار 0 رو داخل کنسول چاپ میکنه. و مهم نیست که بعد از دفعه اول، تابع getLogger رو با مقادیر دیگهای هم صدا زده باشیم یا نه. چرا؟ چون تابع firstLogger مقدار 0 رو داخل closure خودش ذخیره کرده. حالا مثال زیر رو ببینید:
توی این مثال، ما دوتا دکمه روی صفحه داریم که کلیک بر روی یکیشون مقدار a رو لاگ میگیره و کلیک بر روی اونیکی مقدار a رو به 1 تغییر میده. حالا اگه اول روی دکمه Log کلیک کنیم، میبینیم که عدد 0 لاگ گرفته میشه. ولی اگه روی دکمه Inc و سپس Log کلیک کنیم، انتظار داریم عدد 1 لاگ گرفته بشه اما چیزی که میبینیم همون عدد 0 هست. چرا؟
دلیلش سادهاس. ما از useCallback استفاده کردیم و بعنوان dependencies array بهش یه آرایه خالی دادیم (اگر نمیدونید dependencies array چیه اینجا رو بخونید). درواقع با پاس دادن dependencies array خالی به useCallback، داریم به ریاکت میگیم که فرقی نمیکنه که چه اتفاقی میافته، تابع logger باید همیشه همون تابعی که دفعه اول ساخته شده بمونه. ینی همون تابع firstLogger توی مثال قبلی. توی مثال قبلی اینکه دوباره تابع getLogger رو با مقدار جدید صدا میزدیم، باعث میشد که مقداری که firstLogger لاگ میگیره عوض بشه؟ نه. پس توی این مثال هم اینکه کامپوننت رو با استیت جدید رندر بکنیم، باعث نمیشه تابعی که دفعه اول ساخته شد مقدار جدیدی رو لاگ بگیره. و useCallback داره باعث میشه که همیشه همون تابع اولیه برگرده. درست کردنش راحته:
اینجوری میتونیم بگیم که هروقت مقدار a عوض شد، useCallback تابع جدیدی که ساخته میشه رو بهمون پس بده. یعنی انگار که توی مثال جاوااسکریپتی بالا، همیشه آخرین مقدار برگشتی از getLogger رو داشته باشیم.
خب برگردیم سر بحث خودمون. بیاید فرض کنیم بجای تابع logger همچین چیزی داریم:
این کد درست کار نمیکنه. توی مثال قبل یادتونه که تابع logger همیشه 0 رو لاگ میگرفت؟ توی این مثال هم، این تابع مقدار count = 0 رو حفظ کرده و هر دفعه که increment رو صدا میزنیم، انگار که داریم میگیم:
ینی همیشه داریم مقدار 1 رو بعنوان count ست میکنیم. برا همین دکمه Inc بعد از بار اول که مقدار count رو 1 میکنه، بنظر میرسه که دیگه کار نمیکنه.
راه حل چیه؟ سادهاس. دوتا کار میشه کرد، یکی اینکه مثل حالت قبل، به ریاکت بگیم که هردفعه یه تابع جدید برای increment بسازه:
اینجوری خیالمون راحته که هردفعه که کامپوننت با مقدار جدیدی برای count دوباره رندر میشه، یه تابع جدید ساخته میشه و این تابع جدید بعنوان روی button ست میشه.
و راه حل دوم اینه که بجای خوندن count از closure، به ریاکت بگیم خودش آخرین مقدار count رو بهمون بده:
اینجوری فقط یک بار تابع increment رو میسازیم ولی هربار که صداش میزنیم، آخرین مقدار count رو میگیره و یک واحد بهش اضافه میکنه.
البته ترجیح من برای ساخت همچین تابعی اینه:
دسته دوم:
اگر مورد بالا رو توی function component ها رعایت کنید، خودکار درمقابل دسته دوم هم مقاوم شدید. برای همین این دسته رو برای class component ها بررسی میکنیم.
فرض کنید همچین کامپوننتی داریم:
این کامپوننت قراره در وهله اول، یه آرایه از userها رندر کنه. یه prop به اسم shouldTrackCount هم میگیره که اگه true باشه تعداد userها رو هم داخل state نگه میداره (بچههای تو خونه قول بدید این کار رو نکنید) (فقط برای آموزش) (Don't do this at home) (بجاش همون this.state.users.length رو داخل رندر استفاده کنید)
قسمت هیجان انگیز ماجرا توی تابع addUser اتفاق میافته. تو این تابع ۲ تا setState داریم. اولی یه یوزر جدید به آرایه یوزرها اضافه میکنه، و دومین setState تعداد جدید یوزرهارو حساب میکنه و setState میکنه.
اما این تابع درست کار نمیکنه. چرا؟ بخاطر batching.
درواقع ریاکت setStateها رو بصورت sync انجام نمیده. یعنی چی؟ اگه تابع addUser رو به این شکل بازنویسی کنیم:
میبینید که هیچوقت user ای که اخیرا به state اضافه شده توی log دیده نمیشه. چرا؟ چون ریاکت setState رو همون لحظه انجام نمیده. درواقع با صدا زدن setState، ما ریاکت رو از تغییر باخبر میکنیم. اما تصمیم اینکه کی این تغییر انجام بشه با ریاکته و ریاکت معمولا ترجیح میده که بعد از تموم شدن تابع تغییرات رو اعمال کنه. چرا؟ چون اگه همون لحظه اینکار رو بکنه، مجبوره یه دور درخت رو با استیت جدید رندر کنه و بعد اگه تو خط پایین یه setState دیگه داشته باشیم، مجبور میشه دوباره کل رندر کردن درخت رو تکرار کنه. برا همین صبر میکنه تا اجرای تابع تموم بشه، بعد همه تغییرات رو همزمان انجام میده.
تو مثال addUser اگر فرض کنیم که داریم:
کاری که ریاکت درنهایت انجام میده شبیه به اینه:
که معادله با:
و به وضوح میتونیم ببینیم که this.state.users هنوز به مقدار قبلی آرایه users اشاره میکنه، چرا که درحال اجرای setState هستیم و هنوز تغییر استیت انجام نشده و درنتیجه length آرایه users هم همیشه مقدار قدیمی رو نشون میده.
و راه حل این مشکل بازهم مثل همیشه استفاده از function برای setState عه. اینجوری به ریاکت میگیم که برای حساب کردن count به آخرین مقدار state نیاز داریم و ریاکت هم این مقدار رو بعنوان ورودی تابعمون بهمون میده.
والسلام.
(نکته اضافی)
شاید متوجه شده باشید که نکته دوم، ینی batch کردن setState ها، همیشه اتفاق نمیافته. دلیلش اینه که ریاکت درحال حاضر یسری محدودیتها برای انجام دادن اینکار داره و فقط setStateهای داخل تابعهایی رو batch میکنه که توسط ریاکت فراخوانی میشن. بعنوان مثال تابع تابعیه که ما به ریاکت میدیم و ریاکت هروقت که برروی المنت کلیک بشه صداش میزنه. و یا تابع componentDidMount تابعیه که توسط ریاکت صدا زده میشه و درنتیجه همه setStateهای داخلش باهمدیگه batch میشن.
اما اگه کدی مثل شکل زیر داشته باشیم:
متوجه میشیم که این دو setState بجای اینکه باهمدیگه batch بشن و منجر به ۱ رندر بشن، جدا جدا اعمال میشن و درخت ریاکت ۲ بار رندر میشه. دلیلش اینه که باوجود اینکه تابع componentDidMount توسط ریاکت صدا زده میشه، ولی تابع داخل then بصورت async اجرا میشه و در زمانی در آینده، توسط خود انجین جاوااسکریپت (و توسط Promise) صدا زده میشه. بنابراین کنترلش دست ریاکت نیست.
درواقع کاری که ریاکت میکنه شبیه به اینه:
و این توی مثال بالا کار نمیکنه چون then بعد از صدا زده شدن endBatching صدا زده میشه و درنتیجه setState ها بصورت batch نشده اتفاق میافتن.
راه حل چیه؟ درحال حاضر ریاکت یه تابع به اسم unstable_batchedUpdates اکسپورت میکنه که به ما اجازه میده خودمون دستی به ریاکت بگیم که setState هارو batch کنه.
میتونیم پیادهسازی این تابع رو معادل زیر فرض کنیم:
اما توی استفاده ازش بشدت محتاط باشید. این تابع همونطور که از اسمش معلومه unstable هست و ممکنه تو هر ورژنی حذف بشه یا تغییر کنه و باعث بشه ارتقا دادن ریاکتتون سختتر از چیزی بشه که باید باشه.
ولی چرا ریاکت باید بخواد این تابع رو حذف کنه؟ دلیلش اینه که بعد از ریلیز شدن ورژن جدید ریاکت، با قابلیت concurrent mode (که درحال حاضر میتونید تحت فلگ experimental تستش کنید)، ریاکت اونقدری باهوش میشه که بتونه setState های دستی ما رو هم تشخیص بده و همرو با هم batch کنه و دیگه نیازی به این شکل استفاده کردن از unstable_batchedUpdates نخواهد بود.
اما اینکه ریاکت چجوری میتونه setState های ما رو متوجه بشه؟ موضوع یه مقاله دیگهاس :)
شاد و موفق باشید و امیدوارم از این به بعد توی setStateهاتون بیشتر از function استفاده کنید.
دیگر مقالات من:
مطلبی دیگر از این انتشارات
حذف Rerender های اضافی در کامپوننت های React
مطلبی دیگر از این انتشارات
ایجاد پروژه با react و TypeScript
مطلبی دیگر از این انتشارات
نکات طلایی ری اکت نیتیو - ۳