حسین شیره جونی
حسین شیره جونی
خواندن ۴۰ دقیقه·۵ سال پیش

راهنمای کامل useEffect در React

سلام از اولین پستی که در ویرگول گذاشتم دو سال گذشت و می خواستم مطالب بیشتری بنویسم ولی زمان فرصت نداد(بیشترش تنبلی خودم بود?) اما تلاش می کنم بیشتر فعال باشم ولی سعی کردم با یه پست خیلی عالی برگردم این مقاله بیان ترجمه شده از مقاله A Complete Guide to useEffect از Dan Abramov یکی از برنامه نویسای معروف React که مطالب عالی و خوبی در بلاگ شخصی اش می نویسد است.

پیش نیاز مقاله آشنایی با React و همچنین انتظار میره تجربه کار با هوک ها را قبلا داشته باشید مقاله طولانی خواهد بود ولی به شما قول می دهم بعد از خواندن مقاله دید شما نسبت به useEffect و نحوه صحیح بکارگیری آن تغییر کند. شما در این مقاله دید عمیقی نسبت به این تابع پیدا خواهید کرد در انتها نظرتون رو در نحوه بیان من و کیفیت مقاله به اشتراک بگذارید.


شما تعدادی کامپوننت با هوک نوشته اید حتی ممکنه اپلیکیشن کوچیکی باشه. و به احتمال زیاد راضی هستید و با کار با API راحت هستید و ممکن حتی یه سری Custom Hooks هم برای جلوگیری از تکرار نوشته باشید و به دوستانتون نشون داده باشید و اونا هم بگن:"کارت عالیه"

اما بعضی موقع ها که از useEffect استفاده می کنید ممکنه کار نکنه! و تو دلتون بگید آیا من جایی اشتباهی انجام دادم، این که خیلی شبیه Class Lifecycles هست... اما آیا واقعا هست؟ ممکنه این سوالات را هم از خود بپرسید:

  • چجوری می تونم از useEffect به جای componentDidMount استفاده کنم؟
  • چجوری میشه با استفاده از useEffect از API داده ها را بگیرم؟ اصلا [ ]، چی هست؟
  • آیا می تونم از توابع برای وابستگی های effect استفاده کنم؟
  • چرا بعضی موقع ها درون حلقه بی نهایت fetch کردن داده ها می افتم؟
  • چرا بعضی موقع ها state و prop قدیمی را به من نشون میده؟

وقتی که من استفاده از هوک ها را شروع کردم با همین سوالات موجه شدم!؟ حتی موقعی که داشتم راهنمای اولیه(initial docs) هوک را می نوشتم درک عمیقی از این نکات ریز و مهم نداشتم؟ این مقاله به سوالات بالا که ممکنه برای شما بدیهی باشه پاسخ میده.

برای دیدن جواب سوالات، اول باید یه قدم به عقب برگردیم. قرار نیست این مقاله یه سری نکات را کنار هم بزار و سرسری بیاد نام ببره، قرار یه درک عمیقی از هوک useEffect به شما بده. در اصل شما چیز زیادی یاد نمیگیرید بلکه فراموش می کنید اشتباهاتی را که یادگرفتید!(چه جمله ی سنگینی!!!)

توجه کنید که فقط زمانی که به useEffect در قالب class lifecycles نگاه نکنید همه چیز درست میشه.


Unlearn what you have learned. — Yoda
فراموش کن هرچی یاد گرفته ای. — یودا

این مقاله واقعا طولانی هست و در اصل یک کتابچه است. فرمتی که من ترجیحش میدم

اگه راحت نیستید با این مقاله طولانی، به احتمال زیاد، شما باید منتطر بمونید تا جای دیگه ای درموردش توضیح داده بشه. مثل React که سال ۲۰۱۳ که اومد زمان برد تا برنامه نویسان طرز فکرشون رو تغییر بدن و یاد بگیرند و یاد بدهند.

دوستان من توصیه می کنم حتما این مقاله را کامل بخونید چون واقعا نیازتون میشه و بدردتون خواهد خورد. — حسین شیره جونی




هر رندری State و Props خودش را خواهد داشت

قبل از اینکه بتونیم در مورد Effects صحبت کنیم باید درمورد Render بگیم.

این یک شمارنده هست به خط هایلایت شده دقت کنید:


خب این کد چه معنی میده؟ آیا count نگاه میکنه به state مون و یجوری خودش را اتوماتیک اپدیت می کنه؟ این نگاه ممکنه اول کار که داریم React را یاد میگیریم کمک کنه، اما مدل ذهنی دقیقی نیست.

تو این مثال count یه عدد هست. داده ی عجیبی و غریبی مثل watcher, proxy و ... نیست. فقط یه عدد نقلی خیلی قشنگ مثل این:

اولین بار که کامپوننت مون رندر میشه، متغییر count که از useState(0)، میگیریم مقدارش صفر هست. وقتی که setCount(1)، را صدا میزنیم، React کامپنوننتمون دوباره صدا میزنه، اما اینبار مقدار count، یک خواهد بود و ... .

هر بار که state مون اپدیت می کنیم، React کامپوننت ما را صدا میزنه و هر رندری counter را برابر با مقدار ثابت از state خودش میبینه. یعنی counter یه متغییر ثابت درون تابع خواهد بود که مقدارش برابر با state value اون رندر(اوکی:-\)

پس این خط هیچ کار ویژه ای برای Data Binding انجام نمیده:

کاری که میکنه اینه که یه مقداری عدد را درون خروجی رندر میزاره. این مقدار عددی، توسط React فراهم میشه. یعنی وقتی setCount، صدا زده میشه، React کامپوننتمون با مقدار جدید برای count، صدا میزنه. سپس React محتوای DOM را بروزرسانی میکنه تا با نتیجه آخرین رندرمون هم خوانی داشته باشه.

نکته ای که هست اینه که مقدار count، در هر رندری در طول زمان تغییر نمی کنه. بلکه این کامپوننت ما هست که دوباره صدا زده می شود؛ و در هر رندری مقدار count خودش را میبینه و این count مربوط به خودش هست نه رندر دیگه ای.


هر رندری Event Handler خودش را خواهد داشت

تا کنون خوب بود. حالا در مورد Event Handler قضیه چی میشه؟

به این مثال نگاه کن. این مقدار count را با پیغام alert بعد از ۳ ثانیه نشون میده.

خب بیایید بگیم که من این کار ها را میکنم:

  • مقدار count را تا سه افزایش میدم(با کلیک روی "Click me")
  • روی دکمه "show alert" کلیک می کنم
  • قبل از اینکه ۳ ثانیه تموم بشه مقدار count را به ۵ افزایش میدم

انتظار دارید پیغام alert چه عددی را به شما نشون بده؟ آیا ۵ رو که مقدار state شمارنده مون خواهد بود، نشون میده؟ یا عدد ۳ را که وقتی روی دکمه کلیک شد مقدار state مون بود، نشون میده؟



بهتره خودتون امتحان کنید.







پیغام alert، مقدار ۳ را نشون خواهد داد همان مقداری را که وقتی کلیک شد برای خودش در نظر گرفت.

(روش های مختلفی نیز وجود دارد که عدد های دیگه ای را نشان دهد، اما تمرکز ما روی همین رفتار پیش فرض خواهد بود. و موقعی که داریم یه مدل ذهنی می سازیم، خیلی مهم که تشخیص بدیم از مسیر یادگیری با مقاومت کمتر استفاده کنیم تا به نتیجه مطلوب برسیم.)


اما چگونه این کار میکند؟

ما صحبت کردیم که مقدار count، یه مقدار ثابت هست در هر رندری که ما تابع مون را صدا میزنیم هست. تابع ما چندین بار صدا زده می شود(در هر رندر یکبار)، در هر کدام از این زمان ها، count داخل تابع ثابت و برابر با یه مقداری خاصی هست (state برای همون رندر).

این فقط به React مربوط نمیشه یا خاص React نیست. توابع معمولی هم همین جوری انجام میشند:

در این مثال، مقدار someone چندین بار تغییر میکند.(دقیقا مثل زمان هایی در React، که state کامپوننت مون تغییر کنه) اما داخل sayHi، یه متغییر محلی name وجود داره که مربوط به person زمان صدا زدن(call) خودش خواهد بود. این مقدار ثابت، محلی خواهد بود(لوکال) و در نتیجه وقتی که timeout تمام می شود، هر پیغام alert، مقدار name مربوط به خودش را خواهد داشت.


این کد پایین به ما توضیح میده چجوری event handler مون مقدار count مربوط به زمان کلیک شدن را میگیره. اگر همین قاعده را برای اینجا قائل شویم متوجه میشیم که هر رندر مقدار count خودش را می بینه:

بنابرین میشه نتیجه گرفت، هر رندری ورژن خودش را از handlerAlertClick خواهد داشت. هر کدوم از این ورژن ها مقدار count خودش را خواهد داشت:

این دلیل اینه که چرا هر event handlers مربوط به رندر خاصی خواهد بود، و شما کلیک می کنید، تلاش می کنه تا counter مربوط به همان رندر را بخواند.

در هر رندری، props و state برای همیشه ثابت خواهد ماند. بنابرین هر چیزی که داره از این مقادیر استفاده می کنه مربوط به همون رندر خواهد بود و حتی توابع async هم همان مقدار را خواهند دید.

هر رندری effect خودش را خواهد داشت

قرار بود که این پست درمورد effect ها صحبت کند اما تا الان چیزی در موردش نگفتیم. اینو بدونید که واقعا effect هم فرقی با بقیه نمی کند و همین قوانین را خواهد داشت.

بهتره یه مثال بزنیم:

این جا یه سوال از شما می پرسم: چگونه effect آخرین مقدار count را خواهد خواند؟

ممکنه یه کد پیچیده ی عجیب و غریبی باشه که مقدار count را چک میکنه و درون تابع effect قرار میده؟ ممکنه مقدار count، از نوع Mutable باشه بنابرین کامپوننت ما همیشه آخرین مقدار count را خواهد دید؟


نخیر!


ما تا الان دیگه فهمیدیم که مقدار count، یه مقدار ثابت مربوط به آن رندر می باشد. event handler مقدار count مربوط به رندری که درونش اصدا زده میشوند خواهد دید. چون count یه متغییر هست درون scope که دسترسی دارند بهش. این قاعده برای useEffect هم درست است.

مسئله این نیست که count چجوری داخل محیط ثابت تغییر می کند. بلکه خود تابع effect هست که در هر رندر تغییر می کند.

هر نسخه از رندر، مقدار count مربوط به آن را خواهد دید:

کامپوننت React، تابع Effect را که نوشتی بعد از رها کردن(flushing) تغییرات به DOM(ایجد تغییر ات در DOM) و اجازه به مرورگر برای تغییر در صفحه، اجرا میکند.

بنابراین حتی اگه در مورد تابعی که کلا یک کار انجام میده(مثل تغییر عنوان document) صحبت کنیم، این تغییرات در هر رندر، توسط تابع Effect آن رندر انجام خواهد شد و هر کدام از این توابع state و props مربوط به رندری که درونش هست را خواهد دید.

به صورت کلی، میشه گفت که توابع Effect یه بخشی از رندر هستند

بازم میگم، توابع Effect مربوط به همان رندر خواهند شد دقیقا مثل Event Handlers.

برای اینکه مطمئن شیم که فهمیدیم و کامل درکش کردیم بهتره اولین رندری که انجام میشه را یه نگاهی بندازیم:

فریم ورک React: رابط کاربری(UI) که وقتی state صفر هست را بده.

کامپوننت ما:

  • بفرما این نتیجه رندر من خواهد بود:<p>You clicked 0 times</p>
  • همچنین یادت باشه بعد از اینکه کارت رو انجام دادی این Effect را اجرا کنی:() => { document.title = 'You clicked 0 times' }

فریم ورک React: باشه چشم، دارم UI را اپدیت میکنم. مرورگر، یه سری چیز به DOM اضافه کردم.

مرورگر: اوکی، الان صفحه را تغییر میدم.

فریم ورک React: خب الان میرم سراغ اجرای Effect که به من دادی.

  • اجرا: () => { document.title = 'You clicked 0 times' } .



خب بیایید ببینیم چه اتفاقی می افته اگر کلیک کنیم:

کامپوننت ما: هی React، مقدار state را به یک تغییر بده.

فریم ورک React: رابط کاربری(UI) که وقتی state یک هست را بده.

کامپوننت ما:

  • بفرما این نتیجه رندر من خواهد بود:<p>You clicked 1 times</p>
  • همچنین یادت باشه بعد از اینکه کارت رو انجام دادی این Effect را اجرا کنی:() => { document.title = 'You clicked 1 times' }

فریم ورک React: باشه چشم، دارم UI را اپدیت میکنم. مرورگر، من DOM را تغییر دادم.

مرورگر: اوکی، الان تغییرات جدید صفحه را انجام میدم.

فریم ورک React: خب الان میرم سراغ اجرای Effect که مربوط میشه به رندری که انجام دادم.

  • اجرا: () => { document.title = 'You clicked 1 times' } .



هر رندری، هر چیز مربوط به خودش را خواهد داشت

خب تا الان ما فهمیدیم که هر Effect، اجرا میشه بعد از رندر و خودش جزء ی از خروجی کامپوننت مون هست و props و state مربوط به رندر خودش را می بیند.

بیایید یه امتحانی انجام بدیم، این کد را در نظر بگیرید:

من چند بار پشت سر هم کلیک میکنم، نتیجه که لاگ میشه را حدس بزنید؟



یه چند خط لاگ میبینیم که هر کدام مربوط به یه رندر خاصی می باشند و count خودشان راخواهند داشت می تونید اینجا چک کنید

ممکنه شما بگید: که این جواب بدیهیه و آیا انتظار نتیجه دیگه ای داری؟

خب، بهتره بگم این مورد در مورد this.state که توی کلاس با هاش کار میکنیم. اینجوری نیست و خیلی راحت ممکنه به اشتباه فکر کنیم که این روش معادلش کد ریز هست:

چون این کد یعنی this.state.count همیشه به آخرین count مان اشاره میکند نه مربوط به آن رندر که صدا زده می شود، بنابراین شما عدد ۵ را چندین بار می بینید.


من فکر می کنم Hooks بر اساس کلوژر در جااواسکریپت عمل میکنه در حالی که روشی که در کلاس بکار بردیم از یه باگ منقضی شده در کلوژر رنج می بره. و این بخاطر اینه که React اشاره می کنه به this.state Mutate در کلاس که همیشه به آخرین تغییرات اشاره میکنه.

کلوژر برای زمان هایی مناسب هست که متغییر های ثابتی داشته باشیم چون کمک می کنه براحتی در موردش فکر کنیم چون مقادیر ثابت خواهند بود. و همونجوری که صحبت کردیم prop و state هرگز تغییر نمی کنه در یک رندر خاص. در مورد کلاس ها هم می تونیم مشکلشون رو با همین کلوژر ها حل کنیم.

شنا کردن برعکس جهت جریان رود خانه

یه این دو مثال اگه دقت کنید مشاهده می کنید باهم برابراند:

در داخل کامپوننت در زمان یک رندر، این مهم نیست که شما مقایر را از state یا props می خونید چون ان ها تغییری نمی کند.

البته ممکنه، بعضی مواقع شما بخواهید به آخرین مقدار متغییر در داخل callback که درون Effect استفاده می کنید دسترسی پیدا کنیم، که یکی از آسون ترین روش ها استفاده از refs است. که در این مقاله درموردش توضیح داده شده است.

توجه کنید که وقتی می خواهید به جدید ترین مقداری از متغییر که در داخل رندر های گذشته است دسترسی پیدا کنید دارید برخلاف جهت جریان شنا می کنید. این اشتباه نیست(حتی بعضی مواقع ضروری هست) اما استفاده از این روش که خلاف جهت جریان کدتون هست باعث کثیف شدن کدتون خواهد شد.

این یه نسخه از کدمون هست که روشی مشابه روش کلاس(دیدن آخرین تغییرات) را خواهد داشت:

حالا درمورد Cleanup چی؟

بعضی از افکت ها (effect) ممکنه مشکل Cleanup داشته باشند. مخصصوصا مواقعی که هدف اندو کردن(undo) یه effect برای مواردی مثل سابسکرایب(subscribe) باشد.

اگه بیایم props را برای رندر اول {id: 10} در نظر بگیریم و برای رندر دوم {id: 20} . شما ممکنه فکر کنید چنین اتفاقاتی خواهد افتاد.

  • فریم ورک React افکت(effect) شما را برای {id: 10} تمیز می کند.(Clean Up)
  • فریم ورک React کامپوننت شما را برای {id: 20} رندر می کند.
  • فریم ورک React افکت (effect) شما را برای {id: 20} اجرا می کند.

این تقریبا اون چیزی نیست که اتفاق می افتد.

با این مدل ذهنی که شما فرض کردید، شما ممکنه فکر کنید که Cleanup مقدار قدیمی props یعنی {id: 10} را خواهد دید و قبل از رندر مجدد اجرا خواهد شد. و سپس effect جدید مقدار جدید props را یعنی {id: 20} خواهد دید چونکه بعد از رندر اجرا خواهد شد. این دقیقا مدل ذهنی هست که بخاطر class life cycles برای شما به وجود آمده است. و دقیقا چیزی نیست که اتفاق می افتد. بزارید یه نگاهی بندازیم.

فریم ورک React توابع Effect را فقط بعد از اینکه مرورگر تغییرات صفحه را اعمال کرد، اجرا خواهد شد. این کمک می کنه که برنامه شما سریعتر اجرا شود و نیاز نداشته باشد که تغییرات صفحه را بلاک کند. Cleanup توابع Effects هم با کمی تاخیر انجام می شوند. Cleanup تابع Effect قبلی بعد از رندر مجدد کامپوننت اجرا خواهد شد.

  • فریم ورک React کامپوننت شما را برای {id: 20} رندر می کند.
  • مرورگر تغییرات صفحه را انجام میدهد در نتیجه ما UI برای {id: 20} را خواهیم دید.
  • فریم ورک React افکت(effect) شما را برای {id: 10} تمیز می کند.(Clean Up)
  • فریم ورک React افکت (effect) شما را برای {id: 20} اجرا می کند.

شما ممکنه تعجب کنید: که چگونه ممکنه تابع cleanup افکت قبلی هنوز بتونه مقدار قبلی props یعنی را {id: 10} ببینه بعد از اینکه به {id: 20} تغییر کرده است؟

فکر کنم ما قبلا اینجا بودیم نه ...?

این نقل قول از پاراگراف های قبلی هست:

هر تابعی در داخل کامپوننت ما(شامل Event Handlers, effects, timeout و API ها) مقدار props و state رندری را که در ان تعریف شده بود میگره.


خب جواب الان واضح است، تابع Cleanup افکت اخرین مقدار props را نخواهد دید. بلکه مقدار props که در ان رندر تعریف شده است را خواهد خواند.

این همون کاری هست که React با افکت ها انجام می دهد. و برنامه را سریعتر خواهد کرد و مقادیر قدیمی ازبین نخواهند رفت اگر کد ما به اون ها نیاز داشته باشد.

همگام سازی، چرخه زندگی نیست

یکی از محبوب ترین ویژگی های React برای من اینه که رندری اولیه و رندر ها تغییرات را یکی کرده است. و ورودی برنامه را کاهش داده است.

الان کامپوننت مون را اینگونه در نظر بگیرید:

این فرقی نمی کنه که کامپوننت ما ابتدا این <Greeting name="Dan" /> باشد و بعد به این <Greeting name="Yuzhi" /> تبدیل شود یا اینکه کلا از اول این <Greeting name="Yuzhi" /> باشد. در نهایت ما “Hello, Yuzhi” را خواهید دید.

مردم میگند: سفر کردن مهمه، نه مقصد. اما در React، این قضیه برعکس هست. همه چیز مقصد هست و نه خود سفر. این تفاوت، مثل صدا کردن توابع $.addClass و $.removeClass در کتابخانه jQuery(سفرمان) و مشخص کردن این که چه کلاسی باید درون کد React باشد.(مقصدمان)

فریم ورک React، محتوای DOM را با استفاده از props و states فعلی همگام سازی میکند. در موقع رندر کردن هیچ تفاوتی بین mount و update نیست.

درمورد افکت ها هم باید همین گونه فکرکنید. useEffect به شما کمک میکنه بر اساس states و props مان، خارج از درخت React، محتوایمان را همگام سازی کنیم.

این نحوه رفتار افکت ها با مدل mount/update/unmount کاملا متفاوت هست. و بهتر است متوجه این موضوع بشوید و با آن کنار بیاید. اگه شما افکتی می نویسی که که رفتار متفاوتی را بر اساس اینکه آیا دفعه اول هست که رندر می شود یا خیر، کاملا دارید برخلاف جریان رود، شنا میکنید. ما در همگام سازی شکست می خوریم اگر به سفر اهمیت بدهیم نه مقصد.

به React یاد بده که تفاوت های effects را بشناسه!

ما از قبل یاد گرفته ایم که React تنها بخشی از DOM که نیاز به بروزرسانی دارد را تغییر میدهد.

وقتی که شما بروزرسانی میکنید از:

به:

فریم ورک، دوتا ابجکت را مشاهده میکنه:

با نگاه کردن به ابجکت ها، متوجه میشه که مقدار خصوصیت children تغییر کرده و نیاز داره که DOM اپدیت بشه، اما مقدار خصوصیت className تغییر نکرده، بنابراین می تونه به این صورت انجام بده:

آیا می تونیم با effects هم همین کار بکنیم؟ خیلی خوب میشه اگه اجراشون کنیم وقتی که نیاز هست افکت تاثیر داده بشه و نه بیشتر.

برای مثال، ممکن کامپوننت ما دوباره رندر شود بخاطر اینکه state مون تغییر کرده:

اما افکت ما از counter استفاده نمی کنه. افکت ما عنوان سند را document.title با مقدار name که از props میگیره، همگام سازی میکنه، اما در رندر مجدد که این مقدار یکسان هست، همگام سازی مجدد document.title در هر بار که مقدار counter تغییر میکنه، به نظر خوب نمیاد.


باشه، اما آیا React، متوجه تفاوت effect هامون میشه؟

نه نمی تواند، React نمی تونه متوجه یکسان بودن افکت هامون بشه تا موقعی که صداشون نکرده.

این دلیلی هست برای اینکه اگه بخوای از اجرای مجدد غیر ضروری افکت ها جلوگیری کنید، باید از آرایه های وابستگی(که بهشون میگیم وابستگی از این به بعد) به عنوان آرگومان دوم در تابع useEffect استفاده کنید.

الان کد بالا مثل این میمونه که به React بگیم: هی ری اکت، میدونم که نمی تونی داخل تابع را ببینی چی هست اما به جون خودم، فقط داره از name استفاده میکنه و چیز دیگه ای از اسکوپ رندر، درون خود استفاده نمی کنه.

بنابراین اگر تمامی ایتم های آرایه وابستگی که برای این افکت نوشته ای با مقادیر قبلی که در رندر قبلی برای افکت فرستادیم یکسان بود. React، این افکت را اجرا نمی کنه و نادیده میگیرد:

اما اگه حتی یکی از این ایتم های آرایه یکسان نبود، React نمی تونه این افکت را نادیده بگیره و اجراش میکنه.

به React، در مورد وابستگی ها دروغ نگو!

دروغ گفتن به React، درمود وابستگی ها عواقب بدی خواهد داشت.(یاد این جمله افتادم: هر گونه کپی بدون اجازه صاحب اثر، پیگر قانونی خواهد داشت.:-) ). کاملا واضح هست، اما من خیلی دیدم که برنامه نویسانی، با مدل ذهنی که هنوز از کلاس دارند، با useEffect کار می کنند سعی می کنند این قوانین را دور بزنند.(اوایل من خودم هم انجام میدادم)

بخش سوالات متداول هوک در سایت React در مورد جایگزین های این کار توضیح داده البته ما هم پایین تر درموردش صحبت میکنیم
بخش سوالات متداول هوک در سایت React در مورد جایگزین های این کار توضیح داده البته ما هم پایین تر درموردش صحبت میکنیم

شما قطعا میگید: اما من می خوام فقط برای بار اول(mount) انجام بشه. فعلا، اینو یادت باشه: اگه وابستگی مشخص کردی، باید تمام متغییر هایی که درون کامپوننت هستند و داری درون افکت استفاده میکنی رو اونجا بنویسی.

اما بعضی مواقع که این کار را انجام میدید، براتون مشکل ایجاد میکنه. برای مثال ممکن درون یه حلقه بی نهایت Fetch Data یا دوباره ساختن بیش از حد کانکشن ساکت(socket) گیر کنید، اما راه حل حذف کردن وابستگی ها نیست. ما درموردش صحبت خواهیم کرد.

چه اتفاقی میفته وقتی به وابستگی اشتباه بگیم

اگه وابستگی به درستی مشخص کنه تمام مقادیر که درون افکت استفاده میکنید را، اون وقتی React متوجه میشه که باید دوباره افکت را اجرا کنه:

مقادیر وابستگی تغییر کرده اند در نتیجه افکت را دوباره اجرا میکنیم
مقادیر وابستگی تغییر کرده اند در نتیجه افکت را دوباره اجرا میکنیم

اما اگه ما برای این افکت وابستگی خالی را []، یعنی هیچ وابستگی نداشته باشه، مشخص کنیم:

در این مورد ممکنه مشکل کاملا واضح باشد، اما توجه بهش می تونه شما را از نادیده گرفتن این مسئله در مورد های مشابه باز دارد.

برای مثال، اگه بخواهیم یک شمارنده بسازیم که هر ثانیه بشمارد. با مدل ذهنی کلاس می تونیم اینجوری پیاده سازی کنیم: فقط یکبار interval بساز، اخر کار هم ازبین ببرش. این جا راه حل این مسئله با استفاده از Class کامپوننت پیاده سازی شده است. اگر بخواهیم همین را به useEffect تبدیل کنیم، ما از روی عادت وابستگی را [] در نظر میگیریم. "من می خواهم یکبار اجرا شود":

اما این شمارنده تنها یکبار اجرا می شود.

اگه مدل ذهنی شما این باشه که وابستگی مشخص میکنه که باید افکت اجرا شود، این مشکل ممکنه برای شما بحران به وجود آورد. شما می خواهید افکت یکبار اجرا شود چون که یک نوع interval دارید، خب پس چرا این مشکل به وجود می آید؟

الان کاملا برای شما واضح است که وابستگی ها زبانی برای صحبت کردن با React، درمورد تمام چیز هایی هست که افکت درون خود از اسکوپ رندر استفاده میکند. افکت داره از count استفاده می کنه اما شما به دروغ گفته اید [] از هیچ چیزی استفاده نمیکند. الان که متوجه میشید که چرا نباید دروغ بگید.

در رندر اول count برابر ۰ است.بنابراین، setCount(count + 1) در اجرای افکت پس از رندر اولیه معادل setCount(0 + 1) است. بنابراین چون ما هیچ موقع دیگه افکت را صدا نمی زنیم بخاطر نبود وابستگی، در هر ثانیه افکت ما برابر این setCount(0 + 1) خواهد بود.

ما به React، دروغ گفته ایم که افکت ما به هیچ یک از متغییر های کامپوننت مان وابسته نیست. درحالی که هست.

افکت ما داره از count استفاده میکنه، متغییری که درون کامپوننت هست و بیرون از تابع افکت ما.

بنابرین مشخص نکردن وابستگی و استفاده از []، باعث ایجاد باگ درون برنامه React ما می شود. React وابستگی هامون رو مقایسه میکنه و متوجه میشه که باید از اجرای مجدد این افکت صرف نظر کند.

متوجه مشکلاتی شبیه این شدند، سخت هست. بنابراین من پیشنهاد میدم که مشخص کردن وابستگی ها را یک قانون برای خودتون قرار بدید. و همیشه همه این وابستگی ها را مشخص کنید.(ما یک ابزاری ساختیم اگه بخواهی مجبور کنید تیمتون رو به رعایت این موضوع، کمکتون میکنه)

دو راه برای اینکه در مورد وابستگی ها صادق باشیم

دو راه برای اینکه درمورد وابستگی ها صادق باشیم وجود دارد شما باید راه حل اولی را انتخاب کنید و بعد راه حل دوم را اگر نیاز شد.

راه حل اول، تمامی وابستگی ها را مشخص کنید، که شامل تمامی متغییر هایی از کاپوننت مان می شود که درون افکت استفاده میکنیم. بزارید count به عنوان وابستگی قرار بدیم.

این باعث میشه وابستگی ها مون به درستی مشخص بشه، ممکنه ایده آل نباشه اما اولین کاری هست که باید با این مشکل انجام بدهیم. حالا یه تغییر توی count باعث اجرای مجدد افکت مان خواهد شد، که در هر بار interval درون رندرمان،به مقدار حال حاضر count ارجاع می دهد setCount(count + 1):

این مشکل مون رو حل می کنه اما در هر بار رندر که مقدار count تغییر میکند، interval مان پاک می شود و دوباره ساخته می شود. که ممکنه خوش آیند نیاید:

راه حل دوم اینه که کد درون تابع افکت مان را تغییر بدهیم که بیشتر اون چیزی که بخواهیم بهش احتیاجی نباشد یعنی وابستگی نداشته باشد. ما نمی خواهیم در مورد وابستگی دروغ بگویم بلکه می خواهیم افکت مان را تغییر بدهیم که وابستگی تعداد کمتری از متغییر های داخلی کامپوننت داشته باشد.

بزارید یه نگاه کنیم به راه حل هایی برای حذف وابستگی در افکت مان.



افکت مان را بی نیاز کنیم

ما می خواهیم از شر وابستگی به count خلاصه شویم.

برای این کار ما باید از خودمون بپرسیم اصلا count برای چه می خواهیم؟ اگه فقط برای استفاده درون setCount بخواهیم که می تونیم از فرم Functional که از state قدیم مون هم استفاده میکنه استفاده کنیم:


با این setCount(c => c + 1) دقیقا مشخص میکنید که چجوری state مون باید تغییر کنه. البته کاربرد های دیگه ای هم داره مثلا وقتی بخواهید چند بخش از state مون را اپدیت کنیم.

توجه کنید که ما هیچ تقلبی نکردیم و به صورت کاملا درست وابستگی را حذف کردیم، و تابع افکت مان هیچ موقع مقدار counter از درون اسکوپ رندرمان نخواهد خواند.

شما می تونید خودتون انجام بدید اینجا.

بروزرسانی فانکشنال و Google Docs

یکی از کار هایی که در همگام سازی انجام میدهیم، آگاه سازی و بروزرسانی سیستم مان از تغییراتی که در state الان مان هست. برای مثال، ویرایش یک سند Google Docs هیچ موقع کل صفحه برای سرور ارسال نمی شود. این خیلی هزینه بر خواهد بود. اما می توان به سرور کار هایی که کاربر می خواهد انجام دهد ارسال کرد.

در حالی که مورد مثال مان کاملا متفاوت هست، اما از یه قاعده پیروی میکند. این کمک میکنه تا حد امکان کمترین اطلاعاتی که لازم هست را از افکت به کامپوننت ارسال کنیم. بروزرسانی به صورت Functional Form مانند setCount(c => c + 1) اطلاعات کمتری را نسبت به setCount(count + 1) منتقل می کند. چرا که تحت شعاع مقدار فعلی count نیست و فقط یک action هست.فکر کردن به سبک React شامل قانون پیدا سازی کمترین تعداد state هم می شود. این هم همان قانون هست اما برای اپدیت.

انکود کار هایی که کاربر کرده و ارسال آن به گوگل داکس در موقع ویراش سند درحالی که روش کشش آوری هم هست. Functional Updated هم همین نقش را در React دارد.

اما استفاده از setCount(c => c + 1) محدود هست. چون اگر بخواهیم یک state بر اساس state دیگری تغییر کند نمی تواند به ما کمک کند البته یه خواهر بزرگتری هم دارد که می توان از آن استفاده کردuseReducer.

جدا سازی اکشن بروزرسانیمان

بزارید مثال قبلی را تغییر بدهیم استفاده کینم از دو تا state درون کامپوننت مان count و step. تابع interval مان مقدار count را بر اساس step زیاد می کند.

دقت کنید که ما کدمان را دور نمی زنیم چون از step استفاده کردیم پس در وابستگی مان مشخص میکنیم و کد مان به درستی اجرا می شود.
این کد به درستی رفتار میکند و اگر step تغییری کنید دوباره افکت اجرا می شود چون که یکی از وابستگی های این افکت هست. و در اکثر مواقع همون چیزی هست که مدنظر شماست. هیچ اشتباهی نیست که اگر step تغییر کرد افکت دوباره اجرا شود و interval از سر گرفته شود و ما نباید از این روش کار پرهیز کنیم مگر اینکه دلیل خوبی داشته باشیم.

اما، بزارید در نظر بگیریم که ما نخواهیم interval مان از سرگرفته شود وقتی که step تغییری میکند. چگونه ما وابستگی step را از افکت مان حذف میکنیم؟
وقتی شما یک state را تغییر میدهید بر اساس مقدار یک state دیگر، بهتره که useReducer را امتحان کنید.
بیاید وابستگی step را افکت مان حذف کنیم با استفاده از dispatch درون کدمان:

ممکنه شما از خودتون بپرسید چجوری این کد می تونه بهتر از قبلی باشه؟ باید بگم که React تضمین کرده که تابع dispatch همیشه ثابت باشه در طول زمان کامپوننت مان، بنابراین افکت مان هیچ موقع مججد صدا زده نمی شود و interval دوباره subscribe نمی شود.

خب ما مشکل را حل کردیم!

و الان اگه بخواهیم کد reducer مان را ببینیم:

این هم لینک کد هست
این هم لینک کد هست

چرا useReducer حالت تقلب برای Hooks است

ما تا الان تونسیم وابستگی را حذف کنیم در افکت هایی که وابسته به state قبلی یا یک state دیگر هست. اما اگه ما نیاز داشته باشیم که props محاسبه کنه وضعیت state بعدی مان را چیکار باید کرد؟ ممکن API کامپوننت ما به این صورت <Counter step={1} /> نوشته شده باشد. مطمئنان نمیشه توی این مورد props.step را از وابستگی حذف کرد؟

در حقیقت ما می تونیم! می تونیم reducer را درون کامپوننت مان بنویسم تا بتونیم حذفش کنیم:

این روش یکمی از نظر optimazation مشکل داره و نباید هرجایی از آن استفاده کرد اما در کل شما می توانی به props دسترسی داشته باشی در تابع reducer اگر نیاز شد.(اینم دمو)

البته در این شرایط هم باز dispatch تضمین میشه که ثابت خواهد ماند بین رندر ها. بنابراین شما می توانید از وابستگی افکت تان حذف کنیدش. و باعث اجرای مجدد افکت نمی شود.

شما ممکنه تعجب کنید چجوری این کد می تونه کارکنه؟ چجوری میشه reducer به props دسترسی داشته باشه حتی وقتی که درون یک افکت صدا زده می شود؟ هنگامی که شما dispatch می کنید، React فقط اکشن را یادش میمونه اما صدا خواهد زد reducer را در طول رندر بعدی، بنابراین props جدید درون اسکوپ reducer قابل دسترسی خواهد بود و شما درون افکت نخواهید بود.

این دلیلی برای اینکه چرا من میگم useReducer تقلب برای هوک ها هست. چون علاوه بر اینکه جداسازی می کنه منطق اپدیت مان را از اتفاقی که رخ داده، کمک می کنه به حذف وابستگی ها و کمک می کنه به جلوگیری از اجرای مجدد افکت ها وقتی که نیازی بهشون نیست.

انتقال تابع به درون Effect

یک اشتباه متداول دیگر این هست که فکر می کنند تابع نمی تونه وابستگی به حساب بیاد. برای مثال به نظر میرسه این کد کار میکنه:

اگه بخوایم کلی نگاه کنیم بله این تابع کار میکند. اما مسئله اینه که اگه توابع لوکال را به عنوان وابستگی در نظر نگیریم، با رشد کامپوننت و افزایش پیچیدگی آن، مدیریت وابستگی ها در مورد افکت مون سخت تر می شود.
در نظر بگیریم کد مون یه چنین حالتی باشه ولی توابع تعداد خط کد های بیشتری داشته باشن(مثلا ۵ برابر اینا باشند):

حالا مثلا بعدا بیایم به ریفکتور داشته باشیم و از Props و State درون یکی از این توابع استفاده کنیم:

اگه ما فراموش کنیم که وابستگی ها مون رو اپدیت کنیم، افکت مان با تغییر props و state کامپوننت مان همگام سازی نمی شود و به نظر مشکل به وجود میاد.

خوشبختانه، یه راه راحت برای حل این مسئله وجود داره، اگه این توابع که درون افکت مان صدا زده می شوند فقط دورن افکت مان استفاده شوند می توان آنها را مستقیم به افکت انتقال دهیم.

خب مزیت این کار چیه؟ ما دیگه نیاز نیست در مورد وابستگی های غیر مستفیم فکر کنیم. الان آرایه وابستگی به ما دروغ نمی گوید. ما در حقیقت هیچی از اسکوپ کامپوننت مان درون افکت استفاده نمی کنیم.

اگه ما بعدا بخوایم درون تابع getFetchUrl از query درون state کامپوننت مان استفاده کنیم. ما بیشتر دقت خواهیم کرد و متوجه می شویم که چون این تابع درون افکت هست باید وابستگی های این افکت را بروزرسانی کنیم.

با اضافه کردن این وابستگی، ما نه تنها React رو خوشحال میکنیم، بلکه خیلی راحت متوجه می شویم که با تغییر کوئری مجددا داده ها را fetch میکنیم. اصلا طراحی useEffec، ما را بیشتر مجبور به دقت در مورد تغییراتی درون data flow مان می افتد میکنید و اینکه افکت مان چگونه باید بروز شود همراه با این تغییرات — به جای اینکه نادیده بگیریم و منتظر بمانیم تا کاربران برنامه این باگ رو گزارش کنند.

شما می تونید با استفاده از لینت exhaustive-deps که درون ابزار eslint-plugin-react-hooks هست، کد ادیتورتون شما را در هنگام کد نویسی متوجه این خطا کند، و به شما بگه که کدوم وابستگی در نظر گرفته نشده است یا توسط شما درون افکت handle نشده است.

خیلی عالیه!

اما من نمی تونم این تابع رو درون افکت قرار بدم!

بعضی وقت ها شما نمی خواهید، که تابع تان را درون افکت قرار بدید مثلا از یک تابع در چند افکت استفاده میکنید یا اصلا اون تابع به عنوان props برای کامپوننت شما، فرستاده شده است.

آیا باید این تابع را درون وابستگی های در نظر نگیریم، من چنین چیزی رو فکر نمی کنم.

دوبار میگم، افکت نباید در مور وابستگی ها دروغ به گوید. یه راه بهتری که هست، یه تصور غلطی که وجود دارد اینه که تابع هرگز تغییر نمی کنه. اما الان به خاطر چیزی که از این مقاله یاد گرفتیم، این نمی تونه واقعیت باشه. در اصلا تابعی که درون کامپوننت مان تعریف می شود، در هر رندر تغییر میکند.

این خودش یه مشکل به وجود میاره. این دو افکت را در نظر بگیرید که از تابع getFetchUrl استفاده میکنند:

توی این مورد شما ممکن دوسته نداشته باشید که تابع getFetchUrl درون توابع افکت مان قرار دهید، بخاطر اینکه اشتراک گذاری کد رو ازدست میدید.

اما اگه شما بخواهید تابع getFetchUrl را به عنوان وابستگی افکت مان قرار بدید. چون هر دور افکت مان بستگی به این تابع دارند در هر بار رندر افکت مان صدا زده می شود و آرایه وابستگی بیهوده خواهد بود.

یه راه حل دیگه اینه که اونو نادیده بگیریم و به عنوان وابستگی قرار ندیم. اما فکر نکنم راه حل خوبی باشه. این باعث میشه کمتر توجه کنیم به وابستگی های افکت مان در مواقع ایجاد تغییر درون این کد و همان باگ بروز نشدن interval خواهد شد که قبلا دیدیم:

در عوض دو تا راه حل وجود داره که ساده ترند.

تابعی که چیزی درون اسکوپ کامپوننت مان استفاده نمی کند. می تونیم بیرون از کامپوننت تعریق کنیم و از آن استفاده کنیم:

نیاز به تعیین هیچ وابستگی در آرایه نیست بخاطر اینکه درون اسکوپ رندر مان نیست و نمی تواند تاثیر برای دیتا فلوی کامپوننت مان بگذارد و نمی توان به صورت اتفاقی props و state مان را تغییر دهد.

شما همچنین می توانید درون هوک useCallback قرار بدهید:

شما می تونید useCallback رو یه لایه اضافه برای چک کردند وابستگی در نظر بگیرید. این مسئله را از روی دیگر مشکل مون حل میکند - به جای اینکه جلوگیری کند از وابستگی تابع مون، باعث میشه تابع مون تغییر کنه تنها وقتی که نیاز هست.

بزارید این راه حل رو امتحان کنیم. قبلا، مثال مون دوتا نتیجه جستجو داشت(کوئری برای 'react' و 'redux' ). اما بزارید یه input قرار بدیم و شما برای هر کوئری که خواستید جستجو کنید. بنابراین بجای اینکه کوئری مون رو از آرگومان تابع getFetchUrl بگیریم، از state مون میگیریم.

به محض انجام این کار شما پیغام خطای ازدست دادن وابستگی را مشاهده میکنید:

اما اگه من در وابستگیuseCallback کوئری مون قرار بدم، هر افکتی که تابع getFetchUrl رو در وابستگی هاش استفاده میکنه دوباره اجرا می شود با هر تغییر کوئری:

دمت گرم useCallback. اگه کوئری مون ثابت باشه تابع getFetchUrl دوباره ساخته نمیشه و افکت مان دوباره صدا زده نمی شود. اما اگه کوئری مون تغییر کنه، تابع getFetchUrl هم تغییر میکنه و دوباره داده ها را در افکت مان Fetch خواهد شد.

این روش برای تابعی که از طریق Props ، برای کامپوننت فرزند ارسال می شود نیز جواب میدهد:

از اونجایی که تابع fetchData درون کامپوننت پدر، تنها فقط با تغییر کوئری تغییر میکند در نتیجه کامپوننت فرزند تا کوئری تغییر نکند داده ها را فچ نمی کند.

آیا توابع جزئی از جریان داده ها(Data Flow) هستند؟

جالبه که این الگو توسط کلاس ها شکسته شده است به روشی که تفاوت افکت (Effect) و چرخه زمان (Life cycle) ها را بهتر مشاهده میکنید:

شما ممکنه بگید: بابا دست بردار ما همه، میدونیم که useEffect ترکیب هر دو تابع componentDidMount و componentDidUpdate است. اما باید بگم که این کد حتی با componentDidUpdate هم کار نمی کند:

البته که، fetchData یه متود کلاس هست! این هیچ موقع تغییر نمیکنه بخاطر تغییر state مون. بنابراین this.props.fetchData با prevProps.fetchData برابر می مونه و داده ها دوباره فچ نمی شوند.بزارید شرط رو حذف کنیم:

یه لحظه صبر کن، این که همیشه فچ می شود در هر رندری. خب بزارید بایندش کنیم با یه کوئری خاص:

اما this.props.fetchData !== prevProps.fetchData همیشه درست هست باز در هر رندر فچ کردند رو داریم.

تنها راه حل این مشکل پیچیده که با کلاس ها به وجود میاد فرستادن کوئری به عنوان props به کامپوننت فرزند و مشخص کردن زمان فچ کردن دوباره، با استفاده از کوئری هست:

پس از سالها کار کردن با ری اکت [من نمیگم Dan میگه]، من مجبور بودم که این کار رو انجام بدم و هفته پیش فهمیدم چرا مجبورم انجام بدم.

در کلاس ها، متود ها خودشون واقعا جز جریان داده ها نیستند. آن ها محدود هستن به یک this، که از نوع mutable هست.

با useCallback ، توابع دقیقا در جریان داده ها هستند. ما می تونیم بگیم که اگر ورودی تابع تغییر کند، خود تابع هم تغییر میکند، وگرنه ثابت می ماند. دستت درد نکن useCallback که این امکان رو برای ما فراهم آوردی که تغییرات در props.fetchData کاملا باعث تاثیر اتوماتیک در کامپوننت مان می شود.

مشابه آن،useMemo است که می تونیم برای یک ابجکت پیچیده بکار ببریم:

البته می خوام تاکید کنم که قرار دادن هوک useCallback برای هر تابعی کاملا نادرست است.

البته من در مثال های بالا ترجیح میدم تابع ام رو یا درون افکت قرار بدم یا به عنوان کاستوم هوک استفاده کنم یا اینکه import کنم. من میخوام که افکت ها ساده باشند و callback تو این قضیه کمکی نمی کنه.

صحبت در مورد Race Conditions

نحوه ی ساده فچ کردن دیتا ها در کلاس کامپوننت ها به این صورت هست:

همینجوری که میدونید این کد مشکل داره و با بروزرسانی id داده ها دوباره فچ نمی شوند.

مطمئنا این کاملا بهتر هست، اما باز هم مشکل داره و ترتیب ریکوئست هامون ممکنه بهم بریزه. مثلا اگه ما بیایم داده ها را برای {id: 10} بگیریم، و تغییر بدیم به {id: 20} اما ریکوئست {id: 20} نتیجه آن زودتر بیاید و ریکوئستی که زودتر فرستاده بودیم و قدیمی تر بود نتیجه اش دیرتر بیاید به صورت کاملا اشتباه state مون رو تغییر میدند.

به این مشکل Race conditions میگند که معمولا در توابع async / await رخ میدهد. افکت ها به صورت عجیب غریب کد شما رو برای این مورد درست نمی کند بلکه به شما هشدار میدهد اگه بخواهید مستقیما دون افکت از توابع async استفاده کنید.

اگه این تابع async یه راهی برای کنسل کردن هم داشته باشد. خیلی عالیه و میتوان در تابع cleanup آن را کنسل کرد.

یه راه خیلی ساده کنسل کردن میتونه به این صورت پیاده سازی بشه:


پایان

حالا شما هر آنچه را که من درمود افکت ها میدونم میدونید.


بالاخره تموم شد چند روزی سرم شلوغ بود و زمان ها کوتاهی فرصت میکردم که این مقاله را ادامه اش رو بنویسم ارزشش رو داشت حداقل برای خودم که عمیق تر فهمیدم انتشارش میدم که بقیه هم اگه خواستند استفاده کنند البته من ، مترجم نیستم و این مقاله نیاز به خیلی اصطلاحات تخصصی داشت که تا اونجایی که بود سعی کردم ساده توضیح بدهم اگه بد ترجمه شده بود ببخشید اگه جایی به نظرتون ایراد داره حتما بگید و اینکه اگه جایی معادل بهتر پیدا کردید حتما بگید تا جایگزین کنم سوالی هم اگه بود تا اوناجایی که دانش بنده جواب بده جواب میدم.



reactreact useeffectjavascriptreact performanceprogramming
Software Engineer | مثل اینکه کد میزنم!
شاید از این پست‌ها خوشتان بیاید