فکر کردن در چارچوب ریاکت
به عقیده ما، ریاکت بهترین راه برای ساخت وب اپلیکیشن هایی سریع و بزرگ، با استفاده از جاوااسکریپت است و برای ما در فیسبوک و اینستاگرام خیلی خوب جواب داده است.
ریاکت بخشهای خیلی خوب زیادی دارد، اما یکی از بهترین آنها، چگونگی نگرش به اپهایی است که مشغول به ساخت آنها هستید. در این سند (Document)، به فرایند تفکر در ساخت یک جدول محصولات با قابلیت جستجو خواهیم پرداخت، و برای ساخت این جدول از ریاکت استفاده میکنیم.
شروع با یک مدل
تصور کنید که ما از قبل یک JSON API و یک مدل، که توسط طراح آماده شده است را داریم. مدل آماده شده چیزی شبیه به این است:
و JSON API هم دادههایی را باز میگرداند که به صورت زیر است:
[
{category: "Sporting Goods", price: "$49.99", stocked: true, name: "Football"},
{category: "Sporting Goods", price: "$9.99", stocked: true, name: "Baseball"},
{category: "Sporting Goods", price: "$29.99", stocked: false, name: "Basketball"},
{category: "Electronics", price: "$99.99", stocked: true, name: "iPod Touch"},
{category: "Electronics", price: "$399.99", stocked: false, name: "iPhone 5"},
{category: "Electronics", price: "$199.99", stocked: true, name: "Nexus 7"}
];
قدم اول: رابط کاربری را به یک سلسله از کامپوننتها تقسیم کنید
اولین کاری که باید انجام دهید، این است که دور هر کدام از کامپوننتهای موجود خط بکشید و تمامی کامپوننتها را به همراه زیرمجموعههای آن (subcomponents) مشخص کنید و برای هر کدام یک نام در نظر بگیرید (هر کامپوننت ممکن است یک یا چند کامپوننت زیرمجموعه داشته باشد).
اگر به همراه یک طراح کار میکنید، ممکن است او قبلا همین کار را انجام داده باشد. نامگذاری آنها برای لایههای مختلف در فایل فتوشاپی که تهیه کردهاند، میتواند نام کامپوننتها در کد شما هم باشد!
اما چطور بفهمیم که یک بخش باید تبدیل به یک کامپوننت جداگانه شود؟ در این موارد بهتر است از همان تکنیکهایی استفاده کنیم که زمان تصمیمگیری برای تعریف یک تابع یا یک شیء جدید به کمک ما میآمدند. یکی از این تکنیکها اصل مسئولیت واحد (single responsibility principle) است. این اصل بیان میکند که هر کامپوننت، باید در حالت ایدهآل فقط یک کار را انجام دهد و اگر وظایف آن گسترش یافت، باید برای آن کامپوننتهای زیرمجموعه تعریف کرد و وظایف اضافی را به آنها سپرد.
از آنجایی که اغلب یک مدل داده JSON به کاربر نشان داده میشود، متوجه خواهید شد که اگر این مدل درست ساخته شدهباشد، رابط کاربری و ساختار کامپوننتهای شما هم به درستی قابل ترسیم و تعیین خواهد بود. این بدان دلیل است که مدل داده (data model) و رابط کاربری معمولا از یک معماری اطلاعات (information architecture) یکسان تبعیت میکنند. رابط کاربری را طوری به کامپوننتهای مختلف تقسیم کنید که هر کامپوننت با بخش خاصی از مدل داده مرتبط باشد.
در این تصویر میبینید که ما پنج کامپوننت در برنامه خود داریم و اطلاعاتی که هر کامپوننت نمایش میدهد را با حروف ایتالیک مشخص کردهایم:
FilterableProductTable
(نارنجی): شامل تمام برنامهSearchBar
(آبی): ورودیهای کاربر را دریافت میکندProductTable
(سبز): تمامی مجموعه اطلاعات را با توجه به ورودی کاربر نمایش داده و اطلاعات را فیلتر میکندProductCategoryRow
(فیروزه ای): یک تیتر را برای هر دسته نمایش میدهدProductRow
(قرمز): یک ردیف را برای هر محصول نمایش میدهد
اگر نگاهی به کامپوننت ProductTable
بیندازید، متوجه میشوید که تیترهای جدول (شامل “Name” و “Price”) کامپوننت جداگانهای ندارند که بیشتر یک موضوع سلیقهای است و دلایل و استدلالهایی برای استفاده یا عدم استفاده از یک کامپوننت جداگانه برای آنها وجود دارد.
در این مثال، آن را بخشی از ProductTable
قرار دادیم، چرا که جزئی از مجموعه داده ها data collection بوده و رندر کردن آن وظیفه ProductTable
است.
با این حال، اگر header جدول پیچیدهتر شود (مثلا اگر گزینهای برای مرتب کردن لیست محصولات اضافه میکردیم)، مطمئنا منطقیتر بود که آنها در یک کامپوننت جداگانه با نام ProductTableHeader
بگذاریم.
حالا که کامپوننتهای موجود در پروژه را مشخص کردیم، وقتش رسیده که سلسله مراتب آنها را نیز تعیین کنیم. کامپوننتهایی که با توجه به مدل، درون یک کامپوننت دیگر قرار میگیرند، فرزند آن محسوب میشوند:
-
FilterableProductTable
SearchBar
-
ProductTable
ProductCategoryRow
ProductRow
قدم دوم: یک نسخه ایستا (Static) در ریاکت بسازید
این بخش را در فکر کردن در چارچوب ریاکت: گام دوم روی CodePen ببینید.
بعد از اینکه سلسله مراتب را مشخص کردید، نوبت به پیادهسازی اپ می رسد. راحتترین راه این است که نسخهای بسازید که مدل داده ها را میگیرد و رابط کاربری را تولید میکند اما هیچ تعاملی با کاربر ندارد. بهتر است که این دو فرایند (نوشتن نسخه تعاملی و ایستا) را از هم جدا کنیم، به این دلیل که ساخت نسخه ایستا، به مقدار زیادی نوشتن و مقدار کمی فکر کردن نیاز دارد. اما اضافه کردن امکان تعامل و پویایی، احتیاج به مقدار زیادی تفکر دارد و نیاز آن به نوشتن بسیار کمتر است. دلیلش را در ادامه خواهیم دید.
برای ساخت یک نسخه ایستا که مدل داده را رندر کند و نمایش دهد، باید کامپوننتهایی بسازید که از بقیه کامپوننتها استفاده میکنند و دادهها را از طریق props انتقال میدهند. props امکانی است که بوسیله آن، دادهها از کامپوننت والد (parent) به کامپوننت فرزند (child) منتقل میشوند. اگر که با مفهوم state آشنایی دارید، هرگز از آن برای ساخت نسخه ایستا استفاده نکنید! state برای ایجاد تعامل طراحی شده و دادهای است که در طول زمان تغییر میکند و از آنجایی که فعلا روی نسخه ایستا کار میکنیم، نیازی به آن نخواهیم داشت.
میتوانید روند ساخت را از بالا به پایین، یا از پایین به بالا شروع کنید. به این معنا که هم میتوانید از بالاترین کامپوننت در سلسله مراتب قرار دارند (مثلا FilterableProductTable
) یا از کامپوننتهایی که در سطوح پایینتری قرار دارند (مثل ProductRow
) آغاز کنید. در مثالهای سادهتر، معمولا شروع از بالا به پایین راحتتر است. در حالیکه در پروژههای بزرگتر، بهتر است که از پایین به بالا پیش بروید و همزمان با ساخت اپ، برای آن تست نیز بنویسید.
در پایان این گام، شما کتابخانهای از کامپوننتها خواهید داشت که مدل داده را رندر میکند. از آنجایی که با نسخه ایستا سر و کار داریم، کامپوننتها تنها متد render()
خواهند داشت.
کامپوننتی که در راس سلسله مراتب قرار دارد (یعنی FilterableProductTable
) مدل دادهها را بعنوان یک prop دریافت میکند.
اگر تغییراتی را در مدل دادههای زیربنایی پروژه ایجاد کنید و دوباره ReactDOM.render()
را صدا بزنید، رابط کاربری بهروز رسانی خواهد شد و میتوانید ببینید که رابط کاربری چگونه و در کجا تغییر میکند.
جریان یکطرفه داده (one-way data flow) در ریاکت (که با نام binding یکطرفه نیز شناخته میشود) همه چیز را ماژولار و سریع نگه میدارد.
اگر برای اجرای این بخش به کمک نیاز داشتید، به React docs مراجعه کنید.
یک مکث مختصر: props در مقابل state
دو نوع داده “مدل” در ریاکت وجود دارد و خیلی مهم است که تفاوت این دو را بدانیم: اگر خیلی در این مورد مطمئن نیستید، نگاهی به مستندات رسمی ریاکت بیندازید.
علاوه بر این، میتوانید به بخش سوالات پرتکرار: تفاوت میان state و props چیست؟ مراجعه کنید.
قدم سوم: مشخص کردن یک نمونه حداقلی (اما کامل) از state های موردنیاز در رابط کاربری
برای تعاملی کردن رابط کاربری، باید بتوانید در مدل داده زیربنایی پروژه [همان دادههای اولیه] تغییر ایجاد کنید. اینکار در ریاکت از طریق state انجام میشود. برای ساخت اپ به صورت صحیح، ابتدا باید به مجموعهای حداقلی از دادههای قابل تغییر فکر کنید که در پروژه شما نیاز است. کلید این موضوع، اصل خودت را تکرار نکن است. تعیین کنید که در حال حاضر، مختصرترین مجموعه state که برنامه شما نیاز دارد چیست و بقیه موارد را به مرور، و در زمان لزوم محاسبه و تعیین کنید.
برای مثال، اگر یک برنامه برای لیست کارها (TODO List) طراحی میکنید، یک آرایه از آیتمهای موجود در لیست کارها را در دسترس نگه دارید. اما دیگر نیازی به تعیین یک state برای شمارش آیتمها ندارید. به جای آن، زمان رندر کردن تعداد، میتوانید از طول آرایه آیتمها استفاده کنید.
تمام تکههای داده را در مثال خودمان در نظر بگیرید:
- یک لیست اصلی از محصولات
- متنی که کاربر جستجو میکند
- مقدار checkbox
- لیست محصولات فیلتر شده پس از جستجو توسط کاربر
به طور جداگانه سراغ هر یک از این موارد میرویم تا ببینیم که کدامشان state هستند. درمورد هر بخش، این ۳ سوال را از خودتان بپرسید:
- آیا این داده از طرف یک کامپوننت والد و بوسیله props انتقال داده شده؟ اگر جواب مثبت است، پس احتمالا این داده state نیست.
- آیا در طول زمان بدون تغییر میماند؟ اگر اینطور است پس احتمالا باز هم state نیست.
- آیا میتوانید آن را بر اساس یک state یا props موجود محاسبه کنید؟ اگر اینطور است، پس حتما این داده state نیست.
لیست اولیه و اصلی محصولات، بعنوان یک props منتقل میشود، بنابراین state نیست. اما به نظر میرسد که متن جستجو و مقدار چک باکس state هستند چرا که در طول زمان تغییر میکنند و نمیتوان آنها را بهسیله داده دیگری محاسبه کرد. در نهایت، لیست محصولات فیلتر شده پس از جستجو هم state نیست، چرا که میتوان آن را از ترکیب لیست اصلی و اولیه با متن سرچ کاربر و مقدار چک باکس بدست آورد.
پس در مجموع، state این اپ عبارت است از:
- متنی که کاربر در فیلد جستجو وارد میکند
- مقدار checkbox
قدم چهارم: مشخص کنید که state در کجا باید قرار بگیرد
این بخش را در فکر کردن در چارچوب ریاکت: گام چهارم روی CodePen ببینید.
بعد از اینکه ما حداقل state مورد نیاز در پروژه را مشخص کردیم، نوبت این است که بدانیم هر state باید در کدام کامپوننت قرار بگیرد.
به یاد داشته باشید: ریاکت بر مبنای جریان یک طرفه و رو به پایین داده در سلسله مراتب کامپوننتها کار میکند. ممکن است این موضوع که کدام کامپوننت، کدام state را در خود جا میدهد، در اپتدا واضح نباشد. اغلب این مساله، چالش برانگیزترین بخش برای افرادیست که تازه با ریاکت آشنا شدهاند.
پس برای اینکه متوجه موضوع بشوید، این قدمها را برای هر state در برنامه خود دنبال کنید:
- کامپوننتهایی را که چیزی را براساس آن state رندر میکنند، مشخص کنید.
- یک کامپوننت مشترک صاحب state را پیدا کنید (کامپوننتی که در سلسله مراتب برنامه، بالاتر از دیگر کامپوننتهای استفاده کننده از state قرار میگیرد و خود نیز از آن state استفاده میکند)
- صاحب مشترک یا کامپوننت دیگری که در سلسله مراتب، بالاتر قرار میگیرد، باید صاحب state باشند.
- اگر نتوانستید کامپوننتی را پیدا کنید که قرار دادن state در آن منطقی باشد، یک کامپوننت جدید ایجاد کنید که منحصرا برای نگهداری state باشد و آن را جایی بالاتر از صاحب مشترک state در سلسله مراتب قرار دهید.
حالا این استراتژی را برای برنامه خودمان اجرا کنیم:
- کامپوننت
ProductTable
نیاز دارد که براساس state، لیست محصولات را فیلتر کند و کامپوننتSearchBar
هم نیاز دارد که متن جستجو و وضعیت checkbox را نمایش دهد. - صاحب مشترک در این مثال، کامپوننت
FilterableProductTable
است. - پس منطقی به نظر میرسد که state ها را در کامپوننت
FilterableProductTable
قرار دهیم.
به این ترتیب، در ابتدا ویژگی this.state = {filterText: '', inStockOnly: false}
را در بخش constructor
در کامپوننت FilterableProductTable
قرار میدهیم تا state ابتدایی برنامه شما را نشان دهد.
سپس، filterText
و inStockOnly
را به کامپوننتهای ProductTable
و SearchBar
بعنوان props انتقال میدهیم.
درنهایت، این props برای فیلتر وردی های محصولات در ProductTable
و مشخص کردن مقدار در checkbox موجود در SearchBar
به کار میروند.
حالا میتوانید ببینید که برنامه شما چطور عمل میکند: filterText
را به "ball"
تغییر بدهید و برنامه را دوباره بارگذاری کنید. خواهید دید که جدول دادهها به درستی تغییر میکند.
قدم پنجم: اضافه کردن جریان معکوس داده
این بخش را در فکر کردن در چارچوب ریاکت: گام پنجم در CodePen ببینید.
تا به اینجای کار، ما برنامهای ساختیم که به طور صحیح و به شکل تابعی از props و state، رندر میشد و جریان داده در آن از بالا به پایین بود.
اما حالا زمان آن است که برنامه، جریان داده را به شکل معکوس و رو به بالا پشتیبانی کند: فرمهای موجود در پایینترین بخش سلسله مراتب کامپوننتها، باید بتوانند state درون کامپوننت FilterableProductTable
را تغییر دهند.
ریاکت، این جریان داده را شفاف میکند تا به کمک آن، بتوانید طرز کار برنامهتان را درک کنید، اما این مساله احتیاج به نوشتن و تایپ بیشتری نسبت به روش سنتی data binding دو طرفه دارد.
در نسخه فعلی این مثال، اگه در فیلد جستجو تایپ کنید یا تیک checkbox را بزنید، خواهید دید که ریاکت، ورودی شما را نادیده میگیرد. این موضوع عمدی است، چرا که ما تعیین کردیم، value
در input
همواره با مقدار state
که از کامپوننت FilterableProductTable
منتقل میشود، برابر باشد.
بیایید به این فکر کنیم که میخواهیم چه اتفاقی بیفتد؟ میخواهیم مطمئن شویم که هر زمان کاربر، تغییراتی را در فرم (فیلد جستجو یا checkbox) اعمال میکند، state بهروز رسانی شده و تغییرات را منعکس کند.
از آنجایی که کامپوننتها فقط مجاز به تغییر state موجود در خودشان هستند، کامپوننت FilterableProductTable
callbackهایی را به کامپوننتSearchBar
انتقال میدهد تا هر وقت نیاز به تغییر state وجود داشت، فراخوانی شوند.
میتوانیم از رویداد onChange
برای ورودیها استفاده کنیم تا به این طریق، کامپوننت FilterableProductTable
از لزوم اجرای تغییر مطلع شود.
آن callback که از کامپوننت FilterableProductTable
انتقال مییابد، setState()
است و باعث تغییر و بهروز رسانی اپ میشود.
همین!
امیدواریم که این مطلب، به شما ایده داده باشد که چگونه باید درباره ساختن کامپوننتها و برنامهها در ریاکت فکر کنید. البته ممکن است که میزان نوشتن، بیشتر از حدی باشد که به آن عادت دارید، اما به یاد داشته باشید که کد، بیشتر از اینکه نوشته شود، خوانده میشود و خواندن این کد که به شکل ماژولار، ساده و شفاف نوشته شده است، بسیار راحتتر خواهد بود.
به محض اینکه ساخت کتابخانههای بزرگ متشکل از کامپوننتها را شروع کنید، بابت این شفافیت و ماژولار بودن سپاسگزار خواهید شد، و با وجود امکان استفاده مجدد از کدها، تعداد خطهای کد شما به مرور کمتر خواهد شد! :)