کی باید به setState تابع پاس بدیم؟

تابع setState رو توی ری‌اکت به دو شکل میتونیم استفاده کنیم.
۱- با پاس دادن مقدار، و ۲- با پاس دادن تابع.
تو حالت اول مقداری که پاس میدیم جایگزین مقدار قبلی استیت میشه (توی class component مقدار باید object باشه و با object قبلی merge میشه) و تو حالت دوم، ری‌اکت آخرین مقدار state رو بعنوان ورودی به تابعی که بهش دادیم میده و از مقدار خروجی تابع بعنوان مقدار برای setState کردن، مشابه حالت اول، استفاده می‌کنه
حالا سوالی که پیش میاد اینه:

چه زمانی ما نیاز داریم که به setState تابع پاس بدیم؟ و چرا؟

بعنوان یه حکم همیشگی، میشه گفت که هروقت مقدار جدید state به مقدار قبل از خودش وابسته بود، باید از function برای setState استفاده کنیم. ینی چی؟ به این مثال دقت کنید:

 setValue(value + 1);
setValue(value + 1);

توی این مثال مقدار جدید value واضحا به مقدار قبلی خودش بستگی داره. ما در حال حاضر یه مقداری رو بعنوان value داریم که مثلا برابر ۱ هست و با استفاده از اون می‌خوایم value جدید که ۲ میشه رو بسازیم. اینجور مواقع بهتره که از function بجای پاس دادن مستقیم مقدار استفاده کنیم:

 value + 1);setValue(value => value + 1);" />
value + 1);" />setValue(value => value + 1);

اینجا ما بجای اینکه مستقیم از value استفاده کنیم، یه تابع به setValue پاس دادیم. ری‌اکت آخرین مقدار value رو به تابع ما پاس میده و مقداری که return میشه رو داخل استیت ست می‌کنه.

برای این حکم همیشگی رو رعایت میکنیم، که اگر ۱۰۰٪ مواقع کدمون رو اینجوری بنویسیم، می‌تونیم مطمئن باشیم که هیچوقت هیچ مشکلی پیش نمیاد. هرچند که فقط ۲۰٪ مواقع مشکل‌ساز باشن.

این ۲۰٪ مواقع کی‌ها هستند؟

این ۲۰٪ مواقع دردسرساز، که ما رو مجبور به استفاده از function برای setState می‌کنند، به دو دسته تقسیم میشن.

دسته اول:

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

مفهوم closure در جاوا‌اسکریپت
مفهوم closure در جاوا‌اسکریپت

ما این رو میدونیم و مطمعنیم که firstLogger رو هروقت صدا بزنیم مقدار 0 رو داخل کنسول چاپ می‌کنه. و مهم نیست که بعد از دفعه اول، تابع getLogger رو با مقادیر دیگه‌ای هم صدا زده باشیم یا نه. چرا؟ چون تابع firstLogger مقدار 0 رو داخل closure خودش ذخیره کرده. حالا مثال زیر رو ببینید:

مفهوم closure در function component
مفهوم closure در function component

توی این مثال، ما دوتا دکمه روی صفحه داریم که کلیک بر روی یکیشون مقدار a رو لاگ میگیره و کلیک بر روی اونیکی مقدار a رو به 1 تغییر میده. حالا اگه اول روی دکمه Log کلیک کنیم، میبینیم که عدد 0 لاگ گرفته میشه. ولی اگه روی دکمه Inc و سپس Log کلیک کنیم، انتظار داریم عدد 1 لاگ گرفته بشه اما چیزی که میبینیم همون عدد 0 هست. چرا؟

دلیلش ساده‌اس. ما از useCallback استفاده کردیم و بعنوان dependencies array بهش یه آرایه خالی دادیم (اگر نمی‌دونید dependencies array چیه اینجا رو بخونید). درواقع با پاس دادن dependencies array خالی به useCallback، داریم به ری‌اکت میگیم که فرقی نمی‌کنه که چه اتفاقی میافته، تابع logger باید همیشه همون تابعی که دفعه اول ساخته شده بمونه. ینی همون تابع firstLogger توی مثال قبلی. توی مثال قبلی اینکه دوباره تابع getLogger رو با مقدار جدید صدا میزدیم، باعث میشد که مقداری که firstLogger لاگ میگیره عوض بشه؟ نه. پس توی این مثال هم اینکه کامپوننت رو با استیت جدید رندر بکنیم، باعث نمیشه تابعی که دفعه اول ساخته شد مقدار جدیدی رو لاگ بگیره. و useCallback داره باعث میشه که همیشه همون تابع اولیه برگرده. درست کردنش راحته:

رفع مشکل با اضافه کردن متغیرهای مورد استفاده در closure به dependency array
رفع مشکل با اضافه کردن متغیرهای مورد استفاده در closure به dependency array

اینجوری میتونیم بگیم که هروقت مقدار a عوض شد، useCallback تابع جدیدی که ساخته میشه رو بهمون پس بده. یعنی انگار که توی مثال جاوااسکریپتی بالا، همیشه آخرین مقدار برگشتی از getLogger رو داشته باشیم.

خب برگردیم سر بحث خودمون. بیاید فرض کنیم بجای تابع logger همچین چیزی داریم:

مشکل closure در function component
مشکل closure در function component

این کد درست کار نمی‌کنه. توی مثال قبل یادتونه که تابع logger همیشه 0 رو لاگ میگرفت؟ توی این مثال هم، این تابع مقدار count = 0 رو حفظ کرده و هر دفعه که increment رو صدا میزنیم، انگار که داریم میگیم:

دلیل بوجود آمدن مشکل closure در function component
دلیل بوجود آمدن مشکل closure در function component

ینی همیشه داریم مقدار 1 رو بعنوان count ست می‌کنیم. برا همین دکمه Inc بعد از بار اول که مقدار count رو 1 می‌کنه، بنظر میرسه که دیگه کار نمی‌کنه.
راه حل چیه؟ ساده‌اس. دوتا کار میشه کرد، یکی اینکه مثل حالت قبل، به ری‌اکت بگیم که هردفعه یه تابع جدید برای increment بسازه:

حل مشکل closure با اضافه کردن count به dependency array
حل مشکل closure با اضافه کردن count به dependency array

اینجوری خیالمون راحته که هردفعه که کامپوننت با مقدار جدیدی برای count دوباره رندر میشه، یه تابع جدید ساخته میشه و این تابع جدید بعنوان روی button ست میشه.
و راه حل دوم اینه که بجای خوندن count از closure، به ری‌اکت بگیم خودش آخرین مقدار count رو بهمون بده:

حل مشکل closure با پاس دادن function به تابع setState
حل مشکل closure با پاس دادن function به تابع setState

اینجوری فقط یک بار تابع increment رو میسازیم ولی هربار که صداش میزنیم، آخرین مقدار count رو میگیره و یک واحد بهش اضافه می‌کنه.
البته ترجیح من برای ساخت همچین تابعی اینه:

(درسته که وقتی از useCallback استفاده نمی‌کنیم خیالمون راحته که همیشه آخرین مقدار count رو داخل closure داریم، اما قانون 80/20 یادتون نره و از function استفاده کنید)
(درسته که وقتی از useCallback استفاده نمی‌کنیم خیالمون راحته که همیشه آخرین مقدار count رو داخل closure داریم، اما قانون 80/20 یادتون نره و از function استفاده کنید)

دسته دوم:

اگر مورد بالا رو توی 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 رو به این شکل بازنویسی کنیم:

اگر بلافاصله بعد از setState سعی کنیم که مقدار state رو لاگ بگیریم متوجه میشیم که تغییر نکرده.
اگر بلافاصله بعد از setState سعی کنیم که مقدار state رو لاگ بگیریم متوجه میشیم که تغییر نکرده.

میبینید که هیچوقت user ای که اخیرا به state اضافه شده توی log دیده نمیشه. چرا؟ چون ری‌اکت setState رو همون لحظه انجام نمیده. درواقع با صدا زدن setState، ما ری‌اکت رو از تغییر باخبر می‌کنیم. اما تصمیم اینکه کی این تغییر انجام بشه با ری‌اکته و ری‌اکت معمولا ترجیح میده که بعد از تموم شدن تابع تغییرات رو اعمال کنه. چرا؟ چون اگه همون لحظه این‌کار رو بکنه، مجبوره یه دور درخت رو با استیت جدید رندر کنه و بعد اگه تو خط پایین یه setState دیگه داشته باشیم، مجبور میشه دوباره کل رندر کردن درخت رو تکرار کنه. برا همین صبر می‌کنه تا اجرای تابع تموم بشه، بعد همه تغییرات رو همزمان انجام میده.
تو مثال addUser اگر فرض کنیم که داریم:

بیاید روی object هایی که به setState پاس میدیم اسم بذاریم
بیاید روی object هایی که به setState پاس میدیم اسم بذاریم

کاری که ری‌اکت درنهایت انجام میده شبیه به اینه:

ری‌اکت setState های مارو تبدیل به همچین چیزی می‌کنه
ری‌اکت setState های مارو تبدیل به همچین چیزی می‌کنه

که معادله با:

شکل باز شده نتیجه setState های ما
شکل باز شده نتیجه setState های ما

و به وضوح میتونیم ببینیم که this.state.users هنوز به مقدار قبلی آرایه users اشاره می‌کنه، چرا که درحال اجرای setState هستیم و هنوز تغییر استیت انجام نشده و درنتیجه length آرایه users هم همیشه مقدار قدیمی رو نشون میده.

و راه حل این مشکل بازهم مثل همیشه استفاده از function برای setState عه. اینجوری به ری‌اکت میگیم که برای حساب کردن count به آخرین مقدار state نیاز داریم و ری‌اکت هم این مقدار رو بعنوان ورودی تابعمون بهمون میده.

حل مشکل obsolete state با پاس دادن function به setState
حل مشکل obsolete state با پاس دادن function به setState

والسلام.

(نکته اضافی)

شاید متوجه شده باشید که نکته دوم، ینی batch کردن setState ها، همیشه اتفاق نمیافته. دلیلش اینه که ری‌اکت درحال حاضر یسری محدودیت‌ها برای انجام دادن این‌کار داره و فقط setState‌های داخل تابع‌هایی رو batch می‌کنه که توسط ری‌اکت فراخوانی میشن. بعنوان مثال تابع تابعیه که ما به ری‌اکت میدیم و ری‌اکت هروقت که برروی المنت کلیک بشه صداش میزنه. و یا تابع componentDidMount تابعیه که توسط ری‌اکت صدا زده میشه و درنتیجه همه setStateهای داخلش باهمدیگه batch میشن.
اما اگه کدی مثل شکل زیر داشته باشیم:

ری‌اکت فقط setState هایی که توی تابع‌هایی که توسط ری‌اکت اجرا میشن رخ داده‌اند رو batch می‌کنه
ری‌اکت فقط setState هایی که توی تابع‌هایی که توسط ری‌اکت اجرا میشن رخ داده‌اند رو batch می‌کنه

متوجه میشیم که این دو setState بجای اینکه باهمدیگه batch بشن و منجر به ۱ رندر بشن، جدا جدا اعمال میشن و درخت ری‌اکت ۲ بار رندر میشه. دلیلش اینه که باوجود اینکه تابع componentDidMount توسط ری‌اکت صدا زده میشه، ولی تابع داخل then بصورت async اجرا میشه و در زمانی در آینده، توسط خود انجین جاوااسکریپت (و توسط Promise) صدا زده میشه. بنابراین کنترلش دست ری‌اکت نیست.
درواقع کاری که ری‌اکت می‌کنه شبیه به اینه:

کاری که ری‌اکت موقع صدا زدن تابع‌ها انجام میده
کاری که ری‌اکت موقع صدا زدن تابع‌ها انجام میده

و این توی مثال بالا کار نمی‌کنه چون then بعد از صدا زده شدن endBatching صدا زده میشه و درنتیجه setState ها بصورت batch نشده اتفاق میافتن.
راه حل چیه؟ درحال حاضر ری‌اکت یه تابع به اسم unstable_batchedUpdates اکسپورت می‌کنه که به ما اجازه میده خودمون دستی به ری‌اکت بگیم که setState هارو batch کنه.

میتونیم با استفاده از unstable_batchedUpdates همه setState هامون رو batch کنیم
میتونیم با استفاده از unstable_batchedUpdates همه setState هامون رو batch کنیم

میتونیم پیاده‌سازی این تابع رو معادل زیر فرض کنیم:

پیاده‌سازی فرضی تابع unstable_batchedUpdates
پیاده‌سازی فرضی تابع unstable_batchedUpdates

اما توی استفاده ازش بشدت محتاط باشید. این تابع همونطور که از اسمش معلومه unstable هست و ممکنه تو هر ورژنی حذف بشه یا تغییر کنه و باعث بشه ارتقا دادن ری‌اکتتون سخت‌تر از چیزی بشه که باید باشه.

ولی چرا ری‌اکت باید بخواد این تابع رو حذف کنه؟ دلیلش اینه که بعد از ریلیز شدن ورژن جدید ری‌اکت، با قابلیت concurrent mode (که درحال حاضر میتونید تحت فلگ experimental تستش کنید)، ری‌اکت اونقدری باهوش میشه که بتونه setState های دستی ما رو هم تشخیص بده و همرو با هم batch کنه و دیگه نیازی به این شکل استفاده کردن از unstable_batchedUpdates نخواهد بود.
اما اینکه ری‌اکت چجوری میتونه setState های ما رو متوجه بشه؟ موضوع یه مقاله دیگه‌اس :)

شاد و موفق باشید و امیدوارم از این به بعد توی setStateهاتون بیشتر از function استفاده کنید.



دیگر مقالات من:

https://vrgl.ir/gwLl7
https://vrgl.ir/2LzT9
https://vrgl.ir/WWPQ1