پیش از مطالعه این مقاله اگر اطلاعات اولیه و مفاهیم پایه ای در مورد state, props که در React استفاده می شه، ندارید لازمه که پیش زمینه ای در این مورد بدست بیارید. سایت reactjs آموزش همراه با مثال خوبی ارایه کرده که تو این مقاله ما میخوایم بیشتر به بحث state که تو سایت هم این لینک مفاهیم رو کامل توضیح داده بپردازیم.
در React برای ساختن کامپوننت های قابل reuse از class و function استفاده می شه مهم ترین تفاوت این ها داشتن قابلیت state است به این معنی که کلاس ها در React بصورت stateful هستند و دارای متدهای برای مدیریت lifecycle هر کامپوننت که هر متد در زمانی خاصی از فرآیند mount شدن کامپوننت ها در DOM اجرا می شد. این تعریف تا نسخه 16.8 درست بود بعد از اون این امکان به function component ها داده شد بطوریکه میشه هرجاییکه قبلا از کلاس ها استفاده میکردیم با این function ها جایگزین کنیم که این اتفاق با استفاده از Hook ها میسر میشه در واقع بخوایم با جزییات بیشتری اگه نگاه کنیم React هوکی با نام useState ایجاد کرد که عملیات تعریف و بروز رسانی state را انجام می دهد و همینطور هوک هایی مثل useEffect معادل برخی متدهای lifecycle کلاس کامپوننت ها معرفی شد.
تو پروژه های React ای قطعا به دفعات زیاد از state استفاده می کنیم حالا چه از setState در کلاس یا useState در function. این مقاله هدفش گوشزد کردن نکاتی در خصوص بروز رسانی state هست که رعایت کردنش واقعا اهمیت داره و اگه جاهایی ندونیم داره چه اتفاقی میافته ممکنه برنامه رفتار غیر قابل انتظاری داشته باشه که مطمئنا ناشی از خطای برنامه نویسه.
پیش نکته: props ها معمولا به عنوان ورودی کامپوننت دریافت میشه که اطلاعاتشو از کامپوننت بالاسری یعنی جایی که فراخوانی شده میگیره. یک کامپوننت نمیتونه props خودشو update کنه و نباید هم اینکارو بکنه ولی میتونه props مربوط به children خودشو بروز کنه.
نکته: هیچ وقت state رو بصورت مستقیم update نکنید حتما از setState و یا useState استفاده کنید.
this.state.firstName = "Ali" //اشتباه
this.setState({firstName:"Ali"}) //صحیح
اگه روش اول رو انتخاب کنید موجب بروز باگ های عجیب مخصوصا توی بروزرسانی لیست ها میشه و در حالیکه state تغییر کرده ولی عملیات re-render شدن انجام نمیشه.
نکته: بلافاصله بعد از setState از مقدار state تو محاسبات بعدی استفاده نکنید بخاطر اینکه setState بعضی اوقات بصورت async اجرا میشه. React برای لحاظ کردن مسایل performanc ای چند update را به طور همزمان در یک setState انجام میده.
this.setState({counter: this.state.counter + this.props.increment}) //اشتباه
به جای روش بالا از wrapper function ها استفاده کنید تا از callback استفاده کند.
this.setState((state,props) => ({counter: state.counter + props.increment }))
اگر حتما مقدار را برای محاسبات بعدی لازم داریم می توانیم از callback ای که useState دارد استفاده کنیم.
this.setState({name: "Farid"}, () => this.handlexxx())
که در اینجا متد handlexxx از مقدار بروز شده state استفاده میکند. دقت داشته باشید در React نسخه جدید که از هوک useState استفاده می کند پارامتر دوم به عنوان callback نمیگیرد و بجای آن از هوک useEffect استفاده می کند. بنابراین اگر جایی نیاز است پس از تغییر مقدار state کاری انجام دهیم مثلا یه وبسرویس رست فراخوانی کنیم میتوان از useEffect استفاده کرد.
نکته: setState از خاصیت merge استفاده می کند به این معنی که اگر یه فیلد از یک object رو update کنیم مقدار بقیه فیلدهای اون object بدون تغییر می ماند و null نمی شود. البته اینکار در هوک useState اتفاق نمی افتد و باید state قبلی را spread کرد که از ... استفاده می شود.
this.state ={ firstName: "Ali", lastName: "Razeghi", age:32}
this.setState({age:50})
person = {firstName: "Ali", lastName: "Razeghi", age:50} //خروجی
برای زمانیکه از هوک useState استفاده می کنیم:
const [person,setPerson] = React.useState({firstName: "Ali", lastName: "Razeghi", age:32})
setPerson({...person,age:50})
بروزرسانی بالا state قبلی را spread می کند و فقط مقدار جدید عوض می شود.
نکته: props دریافتی را به state نسبت ندهید. با اینکار جریان اطلاعات که از parent به children که منتقل می شود شکسته خواهد شد و قطعا کامپوننت هایی که در children ها قرار است با تغییر props مربوط به parent بتواند re render شوند، اتفاق نمی افتد. البته می شود با استفاده از متدهای lifecycle دستی update کرد که بتواند re render شود ولی یقینا کد dirty خواهد شد.
یه مثال پرکاربرد از بروزرسانی state با استفاده از useState:
در صفحاتی react ای ممکنه زیاد اتفاق افتاده باشه که نیاز به استفاده از لیست یا آرایه داشته باشیم مثلا بخوایم تو یه کامپوننت select لیستی از شهرها را برای انتخاب نمایش بدیم یا لیستی از محصولات رو بصورت جدول دارای سطر و ستون در جواب نتیجه جستجو یه محصول خاص به نمایش گذاشته بشه و ممکنه این جدول اطلاعاتش تغییر کنه مثلا محصول جدیدی اضافه بشه یا حذف بشه یا نه یه رکورد خاص تغییر کنه و ویرایش بشه. درنظر بگیریم که یه آرایه از object های محصولات رو در state ذخیره کردیم و میخوایم عملیات بروزرسانی داشته باشیم و بالطبع بلافاصله re render شدنو ببینیم. اولین و مهمترین نکته اینه وقتی state رو تعریف میکنیم از عبارت const استفاده میکنیم و ممکنه یه مقدار اولیه هم بهش بدیم که نشون میدیم این متغیر تعریف شده immutable یعنی غیرقابل تغییر است و اگه قرار باشه مقدار جدیدی بهش داده بشه از طریق setState اون مقدار جدید بهش assign میشه. حالا برگردیم به تغییر یه آرایه فرض کنیم میخوایم به لیست یک مقدار جدید اضافه کنیم اولین راهی که به ذهنمون میرسه استفاده از push برای add کردن یه item جدید به لیسته مثلا:
setProducts(products.push(newItem)
خوب این راه قطعا خطا خواهد داد و به نتیجه نمیرسه نکته ای که وجود داره اینه که دستور push درسته که مقدار جدید رو اضافه میکنه به لیست ولی این تابع دوباره لیست جدیدی برنمیگردونه که ست بشه در واقع این متد طول ارایه جدیدو برمیگردونه که اینجا فایده ای نداره. حالا راه حل چیه: استفاده از دستور concat و یا spread کرده state قبلی.
setProducts(products => products.concat(item)) //use concat
setProducts(products => [...products , item]) //use spread
دو روش بالا یه مقدار جدید به انتهای لیست اضافه می کنه و بهترین حالت هم استفاده از ویژگی ... در es6 و همچنین wrapper function برای جلوگیری از ایجاد باگ هست.
اینکار بخاطر نقض نکردن خاصیت immutability مربوط به state است، react اجازه تغییر به صورت in-place را نمی دهد و باید مقدار جدید replace شود. که البته این خاصیت به دلایلی است که باعث جلوگیری از ایجاد باگ می شود و علاوه بر اون با این روش یه سیگنالی به virtual DOM می دهد که re render شود. همچنین از تغییرات ناخواسته پیشگیری می کند.
مثال دیگه ای رو اگه بخواین بررسی کنیم مثلا ویرایش یه item از آرایه که هرجایی میتونه باشه که اولا باید index یا اون element رو تو لیست پیدا کنیم بعد مقدارش رو ویرایش کنیم و دوباره ست شه تو لیست و با این فرآیند قطعا state تغییر کرده و باید کل لیست دوباره ست شود. برای همچین کاربردهایی کتابخانه ای با نام immutability-helper استفاده می شه که قبلا کتابخانه react-addons-update برای اینکار مرسوم بود. اگه تو لینک گیت هاب کتابخونه برید دستورات متنوعی برای بروزرسانی داخل آرایه میبینید مثل اضافه کردن، remove، merge و غیره. البته یه راه حلی هم برای deep copy با توجه به پیچیدگی هاش داره.
import update from "immutability-helper";
let dataRows = update(list, {$splice: [[rowIndex, 1, newData]]});
setList(dataRows);
مثال بالا در واقع ایندکس اون object مورد تغییر رو پیدا میکنه و داده قدیم رو با داده جدید جایگزین میکنه. استفاده از این کتابخانه خیلی از پیچیدگی های بروزرسانی لیست ها با توجه به mutation برامون اسون میکنه.
موارد بالا مثال ها و مواردی در خصوص استفاده از مهمترین ویژگی react یعنی state بود. در ادامه در مورد هوک دیگه ای با نام useReducer صحبت می کنیم که رفتاری مشابه با state داره و یه سری امکانات برای مدیریت و کنترل بیشتر روی state به ما میده. البته ناگفته نماند که در setState ای که ما استفاده می کنیم بصورت internal از reducer استفاده می کنه که پیاده سازیش از ما مخفیه.
هوکی به نام useReducer وجود داره که در واقع همون کار useState رو انجام میده ولی حالا چه فرقی میکنه؟ در حقیقت از لحاظ کارکردی فرقی نمیکنه و هدفش بروزرسانی state هست ولی موارد کاربرد متفاوته. زمانیکه نیاز به بروزرسانی یه state ساده باشه هوک useState جواب کارمونو میده ولی اگه state پیچیده باشه بصورت object های تو در تو اینجا useReducer میاد وسط (اگه با Redux آشنا باشید این مفهوم اونجا کامل پیاده سازی شده)، یکی دیگه از جاهاییکه میشه استفاده کرد زمانیه که بررسی state های قبلی و بعدی مهمه و اینکه state بعدی به مقدار قبلی نیاز داره یا تغییر یه state وابسته به state دیگه ایه. این هوک همونجوری که از اسمش پیداست کاهش دهندس یعنی در نهایت همیشه فقط یک مقدار رو برمیگردونه که البته اگه بخوایم دقیق تر نگاه کنیم یک pair از current state و متد dispatch برمیگردونه. اینجا میتونید نحوه پیاده سازی رو همراه با مثال ببینید.