در
قسمت قبل، اولین کامپوننت React خود را ایجاد کردیم و سپس جزئیات بیشتری از عبارات JSX را مانند نحوهی تعریف المانهای مختلف و تنظیم مقادیر ویژگیهای آنرا بررسی کردیم. در ادامهی همان مثال، در این قسمت، نحوهی نمایش لیستها و تعریف و مدیریت رویدادها را در کامپوننتهای React، بررسی میکنیم.
نحوهی رندر لیستی از اشیاء در کامپوننتهای React
فرض کنید میخواهیم لیستی از تگها را رندر کنیم. برای این منظور ابتدا دادههای مرتبط را به خاصیت state کامپوننت، اضافه میکنیم:
class Counter extends Component {
state = {
count: 0,
tags: ["tag 1", "tag 2", "tag 3"]
};
اکنون میخواهیم tags را توسط المانهای ul و ui رندر کنیم. اگر با Angular کار کرده باشید، به همراه یک دایرکتیو ngFor است که توسط آن میتوان یک حلقه را در قالب جاری، پیاده سازی و رندر کرد. اما در React و عبارات JSX، چیزی به نام مفهوم حلقهها وجود خارجی ندارد؛ چون JSX یک templating engine نیست. فقط بیان سادهی المانهایی است که قرار است توسط کامپایلر Babel به کدهای جاوا اسکریپتی ترجمه شوند. بنابراین اکنون این سؤال وجود دارد که چگونه میتوان لیستی از عناصر را در اینجا رندر کرد؟
در مطلب «
React 16x - قسمت 3 - بررسی پیشنیازهای جاوا اسکریپتی - بخش 2» در مورد متد Array.map بحث شد. در اینجا میتوان توسط متد map، هر المان آرایهی تگها را به یک المان React تبدیل و سپس رندر کرد:
class Counter extends Component {
state = {
count: 0,
tags: ["tag 1", "tag 2", "tag 3"]
};
render() {
return (
<div>
<span className={this.getBadgeClasses()}>{this.formatCount()}</span>
<button className="btn btn-secondary btn-sm">Increment</button>
<ul>
{this.state.tags.map(tag => (
<li>{tag}</li>
))}
</ul>
</div>
);
}
در این مثال، داخل المان ul، با یک {} شروع میکنیم تا بتوان به صورت پویا به مقدار آرایهی this.state.tags دسترسی پیدا کرد. سپس متد map را بر روی این آرایه فراخوانی میکنیم. متد map، هر عضو آرایهی tags را به callback function آن ارسال کرده و خروجی آنرا به صورت یک عبارت JSX که در نهایت به یک المان جاوا اسکریپتی خالص ترجمه خواهد شد، تبدیل میکند. این فرآیند سبب رندر لیست tags میشود:
هرچند اکنون لیستی از تگها در مرورگر رندر شدهاند، اما در کنسول توسعه دهندگان مرورگر، یک اخطار نیز درج شدهاست. علت اینجا است که React نیاز دارد تا بتواند هر آیتم رندر شده را به صورت منحصربفردی شناسایی کند. هدف این است که بتواند در صورت تغییر state هر المان در DOM مجازی خودش، خیلی سریع تشخیص دهد که چه چیزی تغییر کرده و فقط کدام قسمت خاص را باید در DOM اصلی، درج و به روز رسانی کند. برای رفع این مشکل، ویژگی key را به هر المان li در کدهای فوق اضافه میکنیم:
البته در مثال ما تگها منحصربفرد هستند؛ بنابراین استفادهی از آنها به عنوان key، مشکلی را ایجاد نمیکند. در یک برنامهی مفصلتر، تگها میتوانند شیء بوده و هر شیء دارای خاصیت id باشد که در این حالت فرضی میتوان از tag.id به عنوان key استفاده کرد. همچنین باید دانست که این key فقط نیاز است در لیست ul، منحصربفرد باشد و نیازی نیست تا در کل DOM منحصربفرد باشد.
رندر شرطی عناصر در کامپوننتهای React
در اینجا میخواهیم اگر تگی وجود نداشت، پیام متناسبی ارائه شود؛ در غیراینصورت لیست تگها همانند قبل نمایش داده شود (رندر شرطی یا conditional rendering). برای انجام اینکار در React، برخلاف Angular، دارای دایرکتیوهای ساختاری if/else نیستیم؛ چون همانطور که عنوان شد، JSX یک templating engine نیست. به همین جهت برای رندر شرطی المانها در React، باید از همان جاوا اسکریپت خالص کمک بگیریم:
renderTags() {
if (this.state.tags.length === 0) {
return <p>There are no tags!</p>;
}
return (
<ul>
{this.state.tags.map(tag => (
<li key={tag}>{tag}</li>
))}
</ul>
);
}
یک روش حل این مساله، نوشتن متدی است که به همراه یک if/else است. در اینجا اگر آرایهی تگها، دارای عنصری نبود، یک پاراگراف متناظر نمایش داده میشود، در غیراینصورت همان قسمت رندر لیست تگها را که توسعه دادیم، بازگشت میدهیم. بنابراین این متد، دو خروجی JSX را بسته به شرایط مختلف میتواند داشته باشد. سپس از این متد به صورت {()this.renderTags} در متد render اصلی استفاده میکنیم:
render() {
return (
<div>
<span className={this.getBadgeClasses()}>{this.formatCount()}</span>
<button className="btn btn-secondary btn-sm">Increment</button>
{this.renderTags()}
</div>
);
}
برای آزمایش آن هم یکبار آرایهی tags را به نحو زیر خالی کنید:
state = {
count: 0,
tags: []
};
روش دوم حل این نوع مسالهها، استفاده از روش زیر است؛ در این حالت خاص، فقط یک if را داریم، بدون وجود قسمت else:
{this.state.tags.length === 0 && "Please create a new tag!"}
ابتدا شرط مدنظر نوشته میشود، سپس پیامی را که باید در این حالت ارائه شود، پس از && مینویسیم. در مثال فوق اگر آرایهی tags خالی باشد، پیامی نمایش داده میشود.
اما این روش چگونه کار میکند؟! در اینجا && را به دو مقدار مشخص اعمال کردهایم. یکی حاصل یک مقایسه است و دیگری یک مقدار رشتهای مشخص. در جاوا اسکریپت برخلاف سایر زبانهای برنامه نویسی، میتوان && را بین دو مقدار غیر Boolean نیز اعمال کرد. در جاوا اسکریپت، یک رشتهی خالی به false تعبیر میشود و اگر تنها دارای یک حرف باشد، true درنظر گرفته میشود. برای نمونه در ترکیب 'true && 'Hi، هر دو قسمت به true تفسیر میشوند. در این حالت موتور جاوا اسکریپت، دومین عبارت (آخرین عبارت && شده) را بازگشت میدهد. همچنین در جاوا اسکریپت عدد صفر به false تفسیر میشود. بنابراین ترکیب true && 'Hi' && 1 مقدار 1 را بازگشت میدهد؛ چون عدد 1 هم از دیدگاه جاوا اسکریپت به true تفسیر خواهد شد.
مدیریت رخدادها در React
همانطور که در تصویر فوق نیز مشاهده میکنید، رخدادهای استاندارد DOM، دارای خواص معادل React ای نیز هستند. برای مثال زمانیکه مینویسیم onClick، دقیقا متناظر است با یک خاصیت المان React در عبارات JSX. بنابراین این نامها حساس به کوچکی و بزرگی حروف نیز هستند.
روش تعریف متدهای رخدادگردان در اینجا، با ذکر فعل handle شروع میشود:
handleIncrement() {
console.log("Increment clicked!");
}
سپس ارجاعی از این متد را (نه فراخوانی آنرا)، به خاصیت برای مثال onClick ارسال میکنیم:
<button
onClick={this.handleIncrement}
className="btn btn-secondary btn-sm"
>
Increment
</button>
اگر دقت کنید، onClick، ارجاع this.handleIncrement را دریافت کردهاست (یعنی بدون () ذکر شدهاست) و نه فراخوانی این متد را (با ذکر ()).
اکنون اگر این فایل را ذخیره کرده و خروجی را در مرورگر بررسی کنیم، با هربار کلیک بر روی دکمهی Increment، یک console.log صورت میگیرد.
در ادامه میخواهیم در این رخدادگردان، مقدار this.state.count را افزایش دهیم. برای این منظور ابتدا مقدار this.state.count را به نحو زیر لاگ میکنیم:
handleIncrement() {
console.log("Increment clicked!", this.state.count);
}
پس از ذخیرهی فایل و اجرای برنامه، اینبار با کلیک بر روی دکمهی Increment، بلافاصله خطای «Uncaught TypeError: Cannot read property 'state' of undefined» در کنسول توسعه دهندههای مرورگر ظاهر میشود. عنوان میکند که شیء this در این متد، undefined است؛ بنابراین امکان خواندن خاصیت state از آن وجود ندارد.
bind مجدد شیء this در رخدادگردانهای React
در مورد this و bind مجدد آن در مطلب «
React 16x - قسمت 2 - بررسی پیشنیازهای جاوا اسکریپتی - بخش 1» مفصل بحث کردیم و در اینجا میخواهیم از نتایج آن استفاده کنیم.
همانطور که مشاهده کردید، در متد رویدادگران handleIncrement، به شیء this دسترسی نداریم. چرا؟ چون this در جاوا اسکریپت نسبت به سایر زبانهای برنامه نویسی، متفاوت رفتار میکند. بسته به اینکه یک متد یا تابع، چگونه فراخوانی میشود، this میتواند اشیاء متفاوتی را بازگشت دهد. اگر تابعی به عنوان یک متد و جزئی از یک شیء فراخوانی شود، this در این حالت همواره ارجاعی را به آن شیء باز میگرداند. اما اگر آن تابع به صورت متکی به خود فراخوانی شد، به صورت پیشفرض ارجاعی را به شیء سراسری window مرورگر، بازگشت میدهد و اگر strict mode فعال باشد، تنها undefined را بازگشت میدهد. به همین جهت است که در اینجا خطای undefined بودن this را دریافت میکنیم.
یک روش حل این مشکل که پیشتر نیز در مورد آن توضیح دادیم، استفاده از متد bind است:
constructor() {
super();
console.log("constructor", this);
this.handleIncrement = this.handleIncrement.bind(this);
}
زمانیکه شیءای از نوع کلاس جاری ایجاد میشود، متد constructor آن نیز فراخوانی خواهد شد. در این مرحله دسترسی کاملی به شیء this وجود دارد که نمونهی آنرا با console.log نوشته شده میتوانید آزمایش کنید. در اینجا چون کامپوننت جاری از کلاس Component مشتق شدهاست، پیش از دسترسی به شیء this، نیاز است سازندهی کلاس پایه توسط متد super فراخوانی شود. اکنون که به this دسترسی داریم، میتوان توسط متد bind، مقدار شیء this شیءای دیگر مانند this.handleIncrement را تنظیم مجدد کنیم (متدها نیز در جاوا اسکریپت شیء هستند). خروجی آن، یک وهلهی جدید از شیء handleIncrement است که this آن اینبار به وهلهای از شیء جاری اشاره میکند. به همین جهت خروجی آنرا به this.handleIncrement انتساب میدهیم تا مشکل تعریف نشده بودن this آن برطرف شود.
اکنون اگر برنامه را اجرا کنید، با کلیک بر روی دکمهی Increment، بجای this.state.count لاگ شده، مقدار آن که صفر است، در کنسول توسعه دهندههای مرورگر ظاهر میشود.
این یک روش است که کار میکند؛ اما کمی طولانی است و به ازای هر روال رویدادگردانی باید دقیقا به همین نحو تکرار شود. روش دیگر، تبدیل متد handleIncrement به یک arrow function است و همانطور که در قسمت دوم این سری نیز بررسی کردیم، arrow functionها، this شیء جاری را بازنویسی نمیکنند؛ بلکه آنرا به ارث میبرند. بنابراین ابتدا کدهای سازندهی فوق را حذف میکنیم (چون دیگر نیازی به آنها نیست) و سپس متد handleIncrement سابق را به صورت زیر، تبدیل به یک arrow function میکنیم:
handleIncrement = () => {
console.log("Increment clicked!", this.state.count);
}
به این ترتیب با کلیک بر روی دکمهی Increment، مجددا همان خروجی تصویر قبلی را دریافت میکنیم؛ این روش سادهتر و تمیزتر است و نیازی به rebind دستی تک تک رویدادگردانهای کامپوننت جاری در این حالت وجود ندارد.
به روز رسانی state در کامپوننتهای React
اکنون که در روال رویدادگردان handleIncrement به شیء this و سپس مقدار this.state.count آن دسترسی پیدا کردهایم، میخواهیم با هربار کلیک بر روی این دکمه، یک واحد مقدار آنرا افزایش داده و در UI نمایش دهیم.
در React، خواص شیء state را جهت نمایش آنها در UI، مستقیما تغییر نمیدهیم. به عبارت دیگر نوشتن یک چنین کدی در React برای به روز رسانی UI، مرسوم نیست:
handleIncrement = () => {
this.state.count++;
};
اگر تغییر فوق را اعمال و سپس برنامه را اجرا کنید، با کلیک بر روی دکمهی Increment ... اتفاقی رخ نمیدهد! رفتار React با Angular متفاوت است و در اینجا هرچند توسط فراخوانی {()this.formatCount} کار نمایش خاصیت count انجام میشود، اما به ظاهر، تغییرات مقدار count، به عبارات JSX متصل نیست. در کامپوننتهای Angular اگر مقدار خاصیتی را تغییر دهید و اگر این خاصیت در قالب آن کامپوننت، به آن خاصیت bind شده باشد، شاهد به روز رسانی آنی UI خواهید بود (Change Detection آنی و به ازای هر تغییری)؛ اما در React خیر. هرچند در همان Angular هم توصیه میشود که از حالت
changeDetection: ChangeDetectionStrategy.OnPush برای رسیدن به حداکثر کارآیی نمایشی کامپوننتها استفاده شود؛ حالت OnPush در Angular، به روش تشخیص تغییرات React که در ادامه توضیح داده میشود، بیشتر شبیه است.
در کدهای فوق هرچند با کلیک بر روی دکمهی Increment، مقدار count افزایش یافتهاست، اما React از وقوع این تغییرات مطلع نیست. به همین جهت است که هیچ تغییری را در UI برنامه مشاهده نمیکنید.
با اجرای قطعه کد فوق، یک چنین اخطاری نیز در کنسول توسعه دهندگان مرورگر ظاهر میشود:
Line 33:5: Do not mutate state directly. Use setState() react/no-direct-mutation-state
برای رفع این مشکل باید از یکی از متدهای به ارث برده شدهی از کلاس پایهی Component، به نام setState استفاده کرد. به این ترتیب به React اعلام میکنیم که state تغییر کردهاست (فعالسازی Change Detection، فقط در صورت نیاز). سپس React شروع به محاسبهی تغییرات کرده و در نتیجه قسمتهای متناظری از UI را برای هماهنگ سازی DOM مجازی خودش با DOM اصلی، به روز رسانی میکند.
زمانیکه از متد setState استفاده میکنیم، شیءای را باید به صورت یک پارامتر به آن ارسال کنیم. در این حالت مقادیر آن یا به خاصیت state جاری اضافه میشوند و یا در صورت از پیش موجود بودن، همان خواص را بازنویسی میکنند:
handleIncrement = () => {
this.setState({ count: this.state.count + 1 });
};
در اینجا به متد this.setState که از قسمت extends Component جاری به ارث رسیدهاست، یک شیء را با خاصیت count و مقدار جدیدی، ارسال میکنیم.
در این مرحله، فایل جاری را ذخیره کرده و پس از بارگذاری مجدد برنامه در مرورگر، بر روی دکمهی Increment کلیک کنید. اینبار ... کار میکند! چون React از تغییرات مطلع شدهاست:
وقتی state تغییر میکند، چه اتفاقاتی رخ میدهند؟
با فراخوانی متد this.setState، به React اعلام میکنیم که state یک کامپوننت قرار است تغییر کند. سپس React فراخوانی مجدد متد Render را در صف اجرایی خودش قرار میدهد تا در زمانی در آینده، اجرا شود؛ این فراخوانی async است. کار متد render، بازگشت یک المان جدید React است. در اینجا DOM مجازی React از چند المان، به صورت یک div و دو فرزند دکمه و span تشکیل شدهاست. در این حالت یک DOM مجازی قدیمی نیز از قبل (پیش از اجرای مجدد متد render) وجود دارد. در این لحظه، React این دو DOM مجازی را کنار هم قرار میدهد و محاسبه میکند که در اینجا دقیقا کدام المانها نسبت به قبل تغییر کردهاند. برای نمونه در اینجا تشخیص میدهد که span است که تغییر کرده، چون مقدار count، توسط آن نمایش داده میشود. در این حالت از کل DOM اصلی، تنها همان span تغییر کرده را به روز رسانی میکند و نه کل DOM را (و نه اعمال مجدد کل المانهای حاصل از متد render را).
این مورد را میتوان به نحو زیر آزمایش و مشاهده کرد:
در مرورگر بر روی المان span که شمارهها را نمایش میدهد، کلیک راست کرده و گزینهی inspect را انتخاب کنید. سپس بر روی دکمهی Increment کلیک نمائید. مرورگر قسمتی را که به روز میشود، با رنگی مشخص و متمایز، به صورت لحظهای نمایش میدهد:
ارسال پارامترها به متدهای رویدادگردان
تا اینجا متد handleIncrement، بدون پارامتر تعریف شدهاست. فرض کنید در یک برنامهی واقعی قرار است با کلیک بر روی این دکمه، id یک محصول را نیز به handleIncrement، منتقل و ارسال کنیم. اما در onClick={this.handleIncrement} تعریف شده، یک ارجاع را به متد handleIncrement داریم. بنابراین برای حل این مساله نمیتوان از روشی مانند onClick={this.handleIncrement(1)} استفاده کرد که در آن عدد فرضی 1 به صورت آرگومان متد handleIncrement ذکر شدهاست.
یک روش حل این مساله، تعریف متد دومی است که متد handleIncrement پارامتر دار را فراخوانی میکند:
doHandleIncrement = () => {
this.handleIncrement({ id: 1, name: "Product 1" });
};
و در این حالت برای مثال متد handleIncrement یک شیء را پذیرفتهاست:
handleIncrement = product => {
console.log(product);
this.setState({ count: this.state.count + 1 });
};
سپس بجای تعریف onClick={this.handleIncrement}، از متد doHandleIncrement استفاده خواهیم کرد؛ یعنی onClick={this.doHandleIncrement}
هرچند این روش کار میکند، اما بیش از اندازه طولانی شدهاست. راه حل بهتر، استفاده از یک inline function است:
onClick={() => this.handleIncrement({ id: 1, name: "Product 1" })}
یعنی کل arrow function مربوط به doHandleIncrement را داخل onClick قرار میدهیم و چون یک سطری است، نیازی به ذکر {} و سمیکالن انتهای آنرا هم ندارد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید:
sample-04-part02.zip