logo
داکیومنت ریکت - قسمت هفتم
ریکت

ریکت

07 شهریور 1402

سلام دوستان امیدوارم خوب باشید و همیشه درحال یادگیری🧑‍💻. توی این پست این چهار موضوع رو قرار هست بررسی کنیم:

  • تفاوت برنامه نویسی declarative و imperative و نحوه عملکرد بصورت declarative داخل ریکت
  • انتخاب ساختار استیت ها
  • share کردن استیت ها بین کامپوننت هامون و "Lifting state up"
  • کامپوننت های کنترل شده و غیر کنترل شده چی هستن؟

تفاوت برنامه نویسی declarative و imperative

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

  • وقتی که داخل فرم تایپ می‌کنه، دکمه "submit" باید فعال بشه
  • وقتی که روی دکمه "submit" کلیک میشه، فرم غیرفعال بشه و لودینگ نمایش داده بشه
  • اگه درخواست موفق بود یک پیامی نشون داده بشه
  • اگه درخواست ناموفق بود بازم پیام نشون داده بشه

به این حالت برنامه نویسی imperative (دستوری) گفته می‌شود. در برنامه نویسی دستوری (imperative programming) باید تک تک مراحل توسط برنامه نویس نوشته شود. فرض کنید کنار یک راننده نشسته اید و از مقصد خبر ندارد و بر اساس دستورات شما به راهش ادامه میدهد. بخاطر این imperative گفته می‌شود که باید تک تک مراحل رو براش تعریف کنیم و طبق دستورات ما پیش روی می‌کنه.


اگر بخوایم مثالی که بالا مطرح کردیم رو بنویسیم بصورت زیر میشه، این نکته رو در نظر بگیرید که از ریکت استفاده نکردیم:



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

ریکت از برنامه نویسی Declarative (اعلانی) استفاده می‌کنه. در برنامه نویسی اعلانی (Declarative programming) هدف مهم هست و شرح چگونگی انجام اهمیت ندارد. در ریکت شما یک کامپوننت رو بصورت مستقیم نمایش، غیرفعال یا hide نمی‌کنید، بجای اون فقط شما بیان (تعریف) می‌کنید که چی قرار هست نشون داده بشه و ریکت خودش آپدیت کردن UI رو هندل می‌کنه.

برفرض سوار تاکسی شده ایم و فقط مقصد را به راننده اعلام می‌کنیم بجای اینکه بگیم از کدوم مسیر برو تا به مقصد ما برسی.


برای اینکه بصورت Declarative بیایم داخل ریکت عمل کنیم لازم هست 5 مرحله رو انجام بدیم:

  1. حالت های مختلف کامپوننت مون رو بشناسیم.
  2. بشناسیم چه چیز هایی باعث تغییر کردن استیت ها میشن.
  3. استیت را به کمک useState داخل مموری قرار بدیم.
  4. استیت های غیر ضروری را حذف می‌کنیم.
  5. داخل event handler ها از تابع setter استیت ها استفاده می‌کنیم.

1. حالت های مختلف کامپوننت مون رو بشناسیم.

اگر با طراح ها کار کرده باشید، می‌‌بینید که حالت های مختلف رو "mock up" می‌کنند. دقیقا مثل "state machine"، که طراح تمام حالت های ممکن رو در نظر می‌گیره و برای همه اون حالت ها یک طرح "mock up" ایجاد می‌کنه.

پس ابتدا باید تمام حالت هایی که ممکن هست بوجود بیاد رو visualize کنیم:

  • Empty: باید دکمه "submit" غیرفعال باشه.
  • Typing: دکمه "submit" فعال بشه.
  • Submitting: فرم کاملا غیرفعال بشه و لودینگ نمایش داده بشه.
  • Success: باید موفقیت بودن درخواست نشون داده بشه.
  • Error: شبیه حالت Typing، اما پیام ناموفق بودن نشون داده بشه.

دقیقا شبیه یک طراح، قبل از ایجاد logic تمام حالت های ممکن رو "mock up" می‌کنیم. بطور مثال یک قسمت فرم رو mock می‌کنیم. یک prop کنترل کننده status داریم که مقدار پیشفرض 'empty' داره:



اگر مقدار status رو به status = 'success' تغییر بدیم، همینطور که می‌بینید پیام نشون داده می‌شود. Mocking به شما اجازه این رو میده که قبل اینکه logic رو اضافه کنید، خیلی سریع براساس حالت های مختلف UI رو تغییر بدین.

این هم نسخه کامل تر مثال بالا:


2. بشناسیم چه چیز هایی باعث تغییر کردن استیت ها میشن

دو چیز باعث trigger کردن آپدیت استیت ها میشن:

  1. کاربر: مثل کلیک دکمه، تایپ کردن داخل field
  2. کامپیوتر: مثل برگردوندن پاسخ یک درخواست یا timeout شدن درخواست

برای اینکه بصورت بصری ببینید هر کدوم از این دو داخل form مثالی که داشتیم چه نقشی دارند این flow خیلی بهتون کمک می‌کنه:


3. استیت را به کمک useState داخل مموری قرار بدیم

توی این مرحله لازم هست حالت های ممکن رو داخل مموری کامپوننت با کمک useState قرار بدیم. این نکته رو در نظر بگیرید که تا حد ممکن باید ساده باشه و هر پیچیدگی بیشتر باشه، باگ هم بیشتر هست.

خوب اول با استیت هایی شروع می‌کنیم که حتما باید وجود داشته باشند. مثل مقدار input که استیت answer هست، و error تا اگر خطایی وجود داره نشون داده بشه:

const [answer, setAnswer] = useState('');
const [error, setError] = useState(null);

بعد از اون لازم هست تمام حالت هایی که بالا visual کردیم رو با استفاده از useState بنویسیم. بهتره اول تمام useState ها رو بنویسیم بعدش بیایم استیت های غیرضروری رو حذف کنیم:

const [isEmpty, setIsEmpty] = useState(true);
const [isTyping, setIsTyping] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const [isError, setIsError] = useState(false);

ممکنه هست اولین ایده ایی که به ذهنتون میرسه بهترین نباشه، اما این مشکلی نداره. refactor کردن استیت ها رو میتونیم انجام بدیم.


4. استیت های غیر ضروری را حذف می‌کنیم

برای حذف استیت های غیر ضروری چند تا سوال باید از خودمون بپرسیم:

  1. آیا این استیت یک پارادوکس ایجاد می‌کنه؟ بطور مثال در استیت های بالا isTyping و isSubmitting هر دو همزمان نمیتونند درست باشن. در واقع پارادوکس یعنی استیت ما به اندازه کافی محدود نیستش. مثلا این موارد رو میتونیم به یک استیت status تبدیل کنیم که سه تا مقدار typing ,submitting و success باشه.

  2. آیا اطلاعات مشابه داخل استیت های دیگه موجود هست؟ بطور مثال در اینجا isEmpty اضافه هست در حالی که ما از answer.lenght === 0 هم میتونیم استفاده کنیم.

  3. آیا اطلاعات مشابه رو از معکوس کردن استیت هم میتونیم بدست بیاریم؟ مثلا در اینجا isError نیاز نیست چون با error !== null میتونیم بنویسیم.

در نهایت استیت های ما بصورت زیر می‌شود:

const [answer, setAnswer] = useState('');
const [error, setError] = useState(null);
const [status, setStatus] = useState('typing'); // 'typing', 'submitting', or 'success'

5. داخل event handler ها از تابع setter استیت ها استفاده می‌کنیم

در نهایت از تابع setter استیت ها در event hadler استفاده می‌کنیم و فرم نهایی بصورت زیر می‌شود:



انتخاب ساختار استیت ها

زمانی که داریم از استیت ها استفاده می کنیم بهتر هست تا حد ممکن بهینه باشه. اینجا چند تا قانون رو که بهتر هست رعایت کنیم بررسی می‌کنیم:

  1. استیت های مرتبط به هم رو یکی کنیم. اگر دو تا یا بیشتر استیت ایی وجود دارند که با همدیگه آپدیت میشن بهتر هست به یک استیت تبدیل بشن.
  2. جلو تناقض ها رو داخل استیت ها بگیریم. وقتی ساختار استیت ها به گونه ایی هستش که چندین حالت ممکن است با یکدیگر تناقض داشته باشند، جایی برای اشتباه باقی می گذاریم.
  3. استیت های اضافی تعریف نکنیم. اگر یکسری اطلاعات از طریق props به کامپوننت ما وارد میشود دیگه برای این props استیت جدیدی تعریف نمی کنیم.
  4. استیت های تکراری تعریف نکنیم. وقتی یکسری اطلاعات مشابه داخل استیت های مختلف باشند سخت هستش که این اطلاعات رو داخل تمام اطلاعات sync کنیم.
  5. از استیت های تو در تو عمیق خودداری کنیم. زمانی که استیت ها بصورت سلسله مراتبی تعریف شوند برای آپدیت کردن کار سخت می شود.

قوانینی که اینجا مطرح کردیم هدفش این هست که استیت ها رو بتونیم راحت تر آپدیت کنیم بدون اینکه اشتباهی انجام بدیم.


1. استیت های مرتبط به هم رو یکی کنیم

این دو استیت رو در نظر بگیرید:

const [x, setX] = useState(0);
const [y, setY] = useState(0);

اگر فرض کنیم، دو استیت x و y با همدیگه آپدیت بشن پس بهتر هستش که دو تا استیت رو به یکی تبدیل کنیم.

const [position, setPosition] = useState({ x: 0, y: 0 });

2. جلو تناقض ها رو داخل استیت ها بگیریم

به این مثال توجه کنید:



در اینجا ما دو تا استیت isSending و isSent داریم. اگر setIsSent و setIsSending رو فراموش کنیم که همزمان صدا کنیم ممکن هست که به موقعیت ایی برسیم که isSending و isSent هر دو true باشن. هر چقدر کامپوننت ما پیچیده تر بشه سخت تر هست که بفهمیم چه اتقاقی داره میوفته.

دو استیت isSending و isSent هرگز نباید هر دو true باشن در یک زمان. پس میاییم این ها رو با یک استیت به نام status عوض می‌کنیم که سه تا مقدار typing و sending و sent میگیرن. همچنین میتونیم دو تا متغیر هم بصورت زیر تعریف کنیم:

const isSending = status === 'sending';
const isSent = status === 'sent';

در نهایت کد ما بصورت زیر می‌شود:



3. استیت های اضافی تعریف نکنیم

اگه داریم یکسری اطلاعات رو از طریق prop یا استیت های موجود فعلی دریافت می‌کنیم، لازم نیست اون اطلاعات رو داخل استیت جدید قرار بدیم.

به این مثال دقت کنید:



این فرم سه تا استیت firstName, lastName و fullName رو داره. اما fullName یک استیت اضافی هست، چون میتونیم fullName رو از دو استیت firstName و lastName بدست بیاریم و بصورت زیر تغییر بدیم:

const fullName = firstName + ' ' + lastName;

4. استیت های تکراری تعریف نکنیم

در مثال پایین فرض کنید ما یک لیست آیتم داریم، که به استیت items دادیم و زمانی که یک استیت انتخاب میشه داخل استیت selectedItem اون آبجکت قرار میگیره. در واقع یک آبجکت مون اومده داخل دو استیت تکرار شده و این مشکل درست میکنه حالا کجا مشکل کجاست؟



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



برای حل این مشکل باید بیاییم آیدی اون آیتم ایی که کلیک شده رو داخل یک استیت ذخیره کنیم و بعدش با استفاده از اون آیدی بیاییم آیتم انتخاب شده رو پیدا کنیم.



در حالت قبلی duplicate بصورت زیر داشتیم:

  • items = [{ id: 0, title: 'pretzels'}, ...]
  • selectedItem = {id: 0, title: 'pretzels'}

اما بعد تغییر:

  • items = [{ id: 0, title: 'pretzels'}, ...]
  • selectedId = 0

توی این حالت مشکل duplicate حل شده و فقط استیت لازم رو نگه داشتیم.


5. از استیت های تو در تو عمیق خودداری کنیم

فرض کنید این لیست بالا بلند زیر، لیست سیاره ها و بعد قاره هر سیاره و برای هر قاره کشورها شو نوشته و در کل یک استیت خیلی بزرگ و تو در تو هستش حالا اگه بخوایم یک آیتم رو حذف کنیم و بعد استیت رو آپدیت کنیم چیکار باید بکنیم؟



یک روش برای اینکه این همه از تو در تو بودن در بیاییم این هست که بجای اینکه هر آیتم یک فرزندی داشته باشه که خودش یک آرایه هست، بیاییم id فرزند ها رو قرار بدیم و بعدش همه آیتم هایی که تو در تو بودن رو به بیرون میاریم و id رو به والد میدیم. در این حالت در واقع استیت ما normalized شده یا flat هست.



الان حذف کردن هم خیلی برامون راحت تر میشه فقط کافیه id ایی که قرار هست حذف بشه رو بگیریم و از لیست flat مون حذفش کنیم.



share کردن استیت ها بین کامپوننت هامون

بعضی وقت ها لازم هست استیت های دو کامپوننت همیشه با همدیگه تغییر کنند. برای اینکار باید استیت های هر دو کامپوننت رو حذف کنیم و به نزدیک ترین والد مشترک انتقال بدیم. به این کار "lifting state up" گفته می‌شود.

فرض یک کامپوننت Accordion داریم که از Panel دو بار استفاده کردیم و استیت هم داخل Panel تعریف شده. فرض کنید میخواید با باز شدن یک پنل، پنل دیگه بسته بشه برای اینکار باید استیت ها داخل والد کنترل بشن.



برای "Lifting state up" (استیت ها رو بالا بیاریم) سه مرحله وجود دارد:

  1. استیت رو از فرزند ها حذف کنیم.
  2. استیت رو به نزدیک ترین والد مشترک فرزند ها بدیم.
  3. استیت رو داخل والد قرار بدیم و به فرزند ها پاس بدیم.

انجام دادن مراحل بالا آنچنان پیچیدگی خاصی نداره. بعد اینکه استیت رو از فرزند حذف کردیم میاییم داخل والد استیت activeIndex رو تعریف می‌کنیم، که نشون دهنده این هست کدوم یک از Panel های ما فعال هستش. برای اینکه استیت رو به فرزند پاس بدیم از props کمک می‌گیریم.



در حالت اول که activeIndex صفر هست Panel اول isActive = true رو دریافت می‌کنه و در حالتی که activeIndex یک است Panel دوم isActive = true رو دریافت می‌کنه.

خوب حالا کامپوننت های کنترل شده و غیر کنترل شده چی هستن؟

به کامپوننت هایی که از استیت داخلی استفاده می‌کنند کامپوننت های غیر کنترل شده میگن. بطور مثال اولین کد همین بخش نگاه کنید یک کامپوننت غیر کنترل شده هست چون استیت isActive توسط والد کنترل نمیشه.

اما کامپوننت های کنترل شده اطلاعات مهم داخل کامپوننت رو از طریق props دریافت می‌کنند و والد کاملا بهش کنترل داره.

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


خوب بچه ها امیدوارم این قسمت براتون مفید بوده باشه. خوشحال میشم اگه انتقادی یا پیشنهادی دارید برام بنویسید تا محتوای بهتری تولید بشه ❤️

منابع:

این پست برات مفید بود؟

0

heart

0

like

0

happy

0

sad