راهنمای جامع React (بخش سوم) — از صفر تا صد
در بخش قبلی این سری از مطالب راهنمای جامع React به بررسی مفهوم اپلیکیشنهای تکصفحهای، رویکرد اعلانی، محض بودن، DOM مجازی و گردش داده غیر جهتدار در ریاکت پرداختیم. در ادامه این سری و در بخش سوم به بررسی مفاهیم عمیق React میپردازیم که فهرست آنها به شرح زیر است:
- جیاسایکس (JSX)
- کامپوننتها
- حالت (State)
- Props
- کامپوننتهای ارائهای در برابر کانتینری
- State در برابر Props
- انواع Prop
- فرگمان ریاکت (React Fragment)
- رویدادها (Events)
- رویدادهای چرخه عمر (Lifecycle Events)
JSX
JSX یک فناوری معرفی شده از سوی React است. با این که React بدون استفاده از JSX نیز کاملاً به خوبی عمل میکند؛ اما JSX یک فناوری ایدهآل برای کار با کامپوننتها محسوب میشود و از این رو در ریاکت استفاده زیادی از آن میکنیم. شاید در ابتدا به نظر بیاید که JSX مانند ترکیب کردن HTML و جاوا اسکریپت است؛ اما این حرف صحیح نیست. چون وقتی از JSX استفاده میکنیم، در واقع از یک ساختار اعلانی برای نوشتن UI یک کامپوننت بهره میگیریم. بدین ترتیب مشخص میشود که UI از رشتهها استفاده نمیکند؛ بلکه از جاوا اسکریپت استفاده میکند که کارهای جالب زیادی با آن میتوان انجام داد.
مقدمهای بر JSX
در این بخش یک تگ h1 تعریف میکنیم که شامل یک رشته است:
1const element = <h1>Hello، world!</h1>
این تگ شبیه به ترکیبی از جاوا اسکریپت و HTML است؛ اما در واقع کلاً در جاوا اسکریپت نوشته شده است. دلیل این که بخشی از آن شبیه به HTML به نظر میآید، آن است که دارای «فرم دگرگون یافته» (syntactic sugar) برای تعریف کردن کامپوننتها و قرار دادن آنها درون بخش markup است. درون یک عبارت JSX میتوان به سادگی «خصوصیات» (attributes) را درج کرد:
1const myId = 'test'
2const element = <h1 id={myId}>Hello، world!</h1>
کافی است توجه کنید هر جایی که در نام یک خصوصیت از «خط تیره» (-) استفاده شده است، باید به ساختار «حالت شتری» (camelCase) تبدیل شود و همچنین به دو نکته زیر نیز توجه داشته باشید:
- class به className تبدیل میشود.
- for به htmlFor تبدیل میشود.
چون اینها واژههای رزرو شده در جاوا اسکریپت هستند. در کد JSX زیر، دو کامپوننت درون تگ div پیچیده شدهاند:
1<div>
2 <BlogPostsList />
3 <Sidebar />
4</div>
در JSX تگها همواره باید بسته شوند، چون ساختار آنها در قیاس با HTML شباهت بیشتری به XML دارد. اگر با XHTML آشنا باشید، احتمالاً این ساختار برای شما آشنا خواهد بود؛ اما از زمان معرفی HTML5 این محدودیت چندان رعایت نمیشود. در این مورد از یک تگ self-closing استفاده شده است. اگر دقت کنید، متوجه میشوید که ما دو کامپوننت را درون یک div قرار دادهایم. دلیل این مسئله آن است که تابع ()render تنها یک گره منفرد را میتواند بازگشت دهد و از این رو در این مورد میخواهیم دو همنیا را بازگشت دهد و تنها یک والد داشته باشیم. دقت کنید که میتوان از هر تگ دیگری نیز به جز div استفاده کرد.
Transpile کردن JSX
یک مرورگر نمیتواند فایلهای جاوا اسکریپتی را اجرا کند که شامل کد JSX باشند. این کدها ابتدا باید به کد جاوا اسکریپت معمولی تبدیل شوند. به این کار Transpile کردن گفته میشود. قبلاً گفتیم که استفاده از JSX اختیاری است، زیرا هر خط JSX یک جایگزین متناظر ساده جاوا اسکریپت دارد و هنگام Transpile کردن کد به جاوا اسکریپت از این خصوصیت استفاده میکنیم. برای نمونه در ادامه دو ساختار ارائه شدهاند که معادل هم هستند:
جاوا اسکریپت ساده
1ReactDOM.render(
2 React.DOM.div(
3 { id: 'test' },
4 React.DOM.h1(null, 'A title'),
5 React.DOM.p(null, 'A paragraph')
6 ),
7 document.getElementById('myapp')
8)
کد JSX
1ReactDOM.render(
2 <div id="test">
3 <h1>A title</h1>
4 <p>A paragraph</p>
5 </div>,
6 document.getElementById('myapp')
7)
این مثال کاملاً ساده تنها یک نقطه شروع محسوب میشود؛ اما میتوانید از روی همین نمونه نیز متوجه شوید که کد جاوا اسکریپت تا چه حد پیچیدهتر از JSX است.
در حال حاضر متداولترین روش برای Transpile کردن کدهای JSX استفاده از Babel است که اگر اپلیکیشن خود را از مسیر create-react-app ساخته باشید، گزینه پیشفرض محسوب میشود. از این رو اگر از این گزینه برای ایجاد اپلیکیشن ریاکت استفاده کردهاید، جای هیچ نگرانی نیست، چون همه چیز به طور خودکار اجرا خواهد شد؛ اما اگر از create-react-app استفاده نکردهاید، باید Babel را به صوت دستی نصب و راهاندازی کنید.
جاوا اسکریپت در JSX
JSX هر نوع کد ترکیبی جاوا اسکریپت که برایش ارسال شود را میپذیرد. هر زمان که لازم باشد کدهای جاوا اسکریپت دیگری اضافه کنیم، کافی است آن را درون آکولاد قرار دهیم. برای نمونه در کد زیر شیوه استفاده از یک مقدار ثابت را میبینید که در جای دیگری تعریف شده است:
1const paragraph = 'A paragraph'
2ReactDOM.render(
3 <div id="test">
4 <h1>A title</h1>
5 <p>{paragraph}</p>
6 </div>,
7 document.getElementById('myapp')
8)
این یک مثال ساده است. آکولادها هر نوع کد جاوا اسکریپت را میپذیرند:
1const paragraph = 'A paragraph'
2ReactDOM.render(
3 <table>
4 {rows.map((row, i) => {
5 return <tr>{row.text}</tr>
6 })}
7 </div>,
8 document.getElementById('myapp')
9)
همان طور که میبینید ما کد جاوا اسکریپت را درون JSX قرار دادهایم و خود JSX نیز داخل کد جاوا اسکریپتِ تعریف شده درون JSX قرار دارد.
HTML در JSX
با این که ساختار JSX تا حدود زیادی به HTML شباهت دارد؛ اما در واقع ساختار آن شبیه به XML است. ما در نهایت کد HTML را رندر خواهیم کرد و از این رو لازم است با برخی تفاوتهای روش تعریف چیزها در HTML در برابر تعریف کردن آنها در JSX آشنا باشید.
همه تگها باید بسته شوند
همانند XHTML همه تگها در JSX باید بسته شوند. برای نمونه در صورت استفاده از <br> باید در انتها از تگ بستن <br /> استفاده شود.
از استاندارد جدید camelCase استفاده میشود
در HTML خصوصیات دارای حالت حروف کوچک هستند. برای مثال خصوصیتی مانند onchange داریم. در JSX این تگها به صورت معادل «حالت شتری» نوشته میشوند:
- onchange => onChange
- onclick => onClick
- onsubmit => onSubmit
class به className تبدیل میشود
به دلیل این واقعیت که JSX همان جاوا اسکریپت است و class یک کلمه رزرو شده محسوب میشود، نمیتوان کدی مانند زیر نوشت:
1<p class="description">
بلکه باید از کد زیر استفاده کنید:
1<p className="description">
همین موضوع در مورد for نیز صدق میکند که به صورت htmlFor ترجمه میشود.
CSS در React
JSX روشی جالب برای تعریف CSS عرضه کرده است. اگر تجربه اندکی با استایلهای درونخطی در HTML داشته باشید، در نگاه اول حس میکنید که به 10 تا 15 سال قبل پرتاب شدهاید. در آن زمان استفاده از CSS-های درونخطی امری کاملاً معمول بود. این وضعیت امروز دیگر متداول نیست و صرفاً به عنوان یک راهحل «اصلاح سریع» (Quick Fix) نگریسته میشود.
استایل JSX چنین نیست، قبل از هر چیز به جای پذیرش یک رشته شامل مشخصات CSS، خصوصیت style در JSX، یک شیء را میپذیرد. این بدان معنی است که باید مشخصات را در یک شیء تعریف کنیم:
1var divStyle = {
2 color: 'white'
3}
4ReactDOM.render(<div style={divStyle}>Hello World!</div>, mountNode)
یا به صورت زیر عمل کنیم:
1ReactDOM.render(<div style={{ color: 'white' }}>Hello World!</div>, mountNode)
مقادیر CSS که در JSX مینویسیم، اندکی متفاوت از CSS ساده هستند:
- نام مشخصات کلیدها به صورت «حالت شتری» (camelCased) هستند.
- مقادیر صرفاً به صورت رشته هستند.
- چندتاییها با کاما از هم جدا میشوند.
چرا این وضعیت به CSS / SASS / LESS ساده ترجیح داده میشود؟
CSS یک مسئله حلنشده است. از زمان ظهور آن، دهها ابزار پیرامون آن شکل گرفته و سپس نابود شدهاند. مشکل اصلی در مورد جاوا اسکریپت این است که هیچ نوع دامنهبندی ندارد و به سادگی ممکن است کد CSS بنویسیم که به هیچ ترتیبی پیادهسازی نمیشود. در این موارد، میتوان یک «اصلاح سریع» برای عناصری که تأثیر نپذیرفتهاند، نوشت.
JSX به کامپوننتها اجازه میدهد که استایل خودشان را به طور کامل کپسولهسازی کنند.
آیا این راهحل موقت است؟
استایلهای «درونخطی» (inline) در JSX در موارد زیر مفید هستند:
- نوشتن کوئریهای رسانه
- انیمیشنهای استایل
- ارجاع به شبه کلاسها (مانند hover:)
- ارجاع به شبه عنصرها (مانند first-letter::)
به طور خلاصه موارد ابتدایی را مدیریت میکنند؛ اما یک راهحل نهایی محسوب نمیشوند.
فرمها در JSX
JSX تغییراتی در طرز کار HTML ایجاد میکند و هدف آن این است که کارها برای توسعهدهنده آسانتر شود.
خصوصیتهای value و defaultValue
- خصوصیت value مقدار کنونی فیلد را نگهداری میکند.
- خصوصیت defaultValue مقدار پیشفرضی که هنگام ایجاد فیلد تعیین میشود را نگهداری میکند.
این وضعیت به حل برخی از رفتارهای عجیب در زمان کار با DOM معمولی و بررسی ('input.value و input.getAttribute('value کمک میکند و بین دو مقدار کنونی و مقدار پیشفرض فیلد تمایز قائل میشود. این وضعیت در مورد فیلد textarea نیز صدق میکند:
1<textarea>Some text</textarea>
اما به جای آن از کد زیر استفاده میکنیم:
1<textarea defaultValue={'Some text'} />
در مورد فیلدهای select به جای استفاده از کد زیر:
1<select>
2 <option value="x" selected>
3 ...
4 </option>
5</select>
از این کد استفاده میکنیم:
1<select defaultValue="x">
2 <option value="x">...</option>
3</select>
خصوصیت منسجمتر onChange
ارسال یک تابع به خصوصیت onChange باعث میشود که رویدادهایی روی فیلدهای فرم ثبت کنیم. این وضعیت به طور سازگاری روی فیلدها و حتی فیلدهای ورودی radio ،select و checkbox که یک رویداد onChange را اجرا میکنند، عمل میکند. onChange همچنین در مواردی که یک کاراکتر در فیلد input یا textarea وارد میکنید، فعال میشود.
Auto Escapes در JSX
JSX جهت کاهش ریسکهای همیشه موجود XSS، الزام میکند که از خصوصیت auto escapes در عبارتها استفاده کنیم. این به آن معنی است که وقتی گزارههای HTML را در یک عبارت رشتهای مورد استفاده قرار میدهید، با مشکلاتی مواجه خواهید شد. برای نمونه ما میخواهیم با کد زیر عبارت 2017 © را نمایش دهیم:
1<p>{'© 2017'}</p>
اما مشاهده میکنیم که عبارت copy; 2017& نمایش مییابد، زیرا رشته ما escape شده است. جهت اصلاح این وضعیت یا باید گزارههای HTML را به خارج از عبارت ببریم:
1<p>© 2017</p>
یا این که از یک ثابت استفاده کنیم که بازنمایی یونیکد متناظر با گزاره HTML را نمایش میدهد:
1<p>{'\u00A9 2017'}</p>
فاصله خالی در JSX
برای افزودن فاصله خالی در JSX دو روش وجود دارد:
قاعده 1: فاصله خالی افقی به اندازه 1 فاصله کاهش مییابد
اگر بین عناصر در یک خط، فواصل خالی وجود داشته باشد، همه آنها به یک فاصله خالی کاهش مییابند. بدین ترتیب کد زیر:
1<p>Something becomes this</p>
به صورت زیر درمیآید:
1<p>Something becomes this</p>
قاعده 2: فواصل خالی عمودی حذف میشوند
بدین ترتیب کد زیر:
1<p>
2 Something
3 becomes
4 this
5</p>
به صورت زیر درمیآید:
1<p>Somethingbecomesthis</p>
برای حل این مشکل، باید فاصلههای خالی را صریحاً با افزودن یک عبارت فاصله مانند زیر اضافه کنید:
1<p>
2 Something
3 {' '}becomes
4 {' '}this
5</p>
همچنین میتوانید رشتهای را در یک عبارت فاصله (Space) وارد کنید:
1<p>
2 Something
3 {' becomes '}
4 this
5</p>
افزودن کامنت در JSX
با استفاده از کامنتهای معمول جاوا اسکریپت درون یک عبارت میتوان کامنتهایی به کد JSX اضافه کرد:
1<p>
2 {/* a comment */}
3 {
4 //another comment
5 }
6</p>
توزیع خصوصیتها
یک عملیات متداول در JSX، انتساب مقادیر به خصوصیتها است. بدین ترتیب به جای این که این کار را به صورت دستی انجام دهیم:
1<div>
2 <BlogPost title={data.title} date={data.date} />
3</div>
میتوانیم از کد زیر استفاده کنیم:
1<div>
2 <BlogPost {...data} />
3</div>
مشخصات شیء data به لطف عملگر spread در ES6 به صورت خودکار به عنوان خصوصیتها استفاده میشود.
تعریف حلقه در JSX
اگر مجموعهای از عناصر داشته باشید و بخواهید روی آنها حلقهای اجرا کنید تا بخشی از JSX را تولید کنید، میتوانید یک loop ایجاد کنید و سپس JSX را به آرایه اضافه کنید:
1const elements = [] //..some array
2const items = []
3for (const [index, value] of elements.entries() {
4 items.push(<Element key={index} />)
5}
اینک زمانی که JSX را رندر کنید، میتوانید آرایه items را به سادگی با قرار دادن داخل آکولاد، جاسازی کنید:
1const elements = ['one', 'two', 'three'];
2const items = []
3for (const [index, value] of elements.entries() {
4 items.push(<li key={index}>{value}</li>)
5}
6return (
7 <div>
8 {items}
9 </div>
10)
همین کار را میتوان مستقیماً در JSX با استفاده از map به جای حلقه for اجرا کرد:
1const elements = ['one', 'two', 'three'];
2return (
3 <ul>
4 {elements.map((value, index) => {
5 return <li key={index}>{value}</li>
6 })}
7 </ul>
8)
کامپوننتها
یک کامپوننت در واقع بخش مجزایی از اینترفیس است. برای نمونه در صفحه اصلی یک وبلاگ معمولی میتوان کامپوننت سایدبار را مشاهده کرد و همچنین احتمالاً دارای یک کامپوننت فهرست مطالب بلاگ نیز هست. این کامپوننتها به نوبه خود از کامپوننتهای دیگری تشکیل یافتهاند و از این رو میتوان کامپوننتهایی برای فهرست مطالب وبلاگ داشت که هر مطلب خود یک کامپوننت بوده و هر یک دارای مشخصات خاصی است.
- ریاکت همه چیز را ساده ساخته است، چون در ریاکت همه چیز یک کامپوننت محسوب میشود.
- حتی تگهای ساده HTML نیز خود یک کامپوننت هستند و به صورت پیشفرض اضافه میشوند.
دو خط بعدی کد زیر معادل هم هستند و کار یکسانی انجام میدهد. یکی از آنها با JSX و دیگری بدون آن نوشته شده و <h1>Hello World!</h1> درون یک عنصر با id به نام app تزریق شده است.
1import React from 'react'
2import ReactDOM from 'react-dom'
3ReactDOM.render(<h1>Hello World!</h1>, document.getElementById('app'))
4ReactDOM.render(
5 React.DOM.h1(null, 'Hello World!'),
6 document.getElementById('app')
7)
میبینید که React.DOM یک کامپوننت h1 در اختیار ما قرار داده است. همه تگهای دیگر HTML نیز موجود هستند. آنها را میتوانید با وارد کردن React.DOM در کنسول مرورگر مشاهده کنید. کامپوننتهای داخلی جالب هستند؛ اما چندان به کار نمیآیند. نقطه قوت ریاکت در این است که به ما اجازه میدهد یک UI را با ترکیب کردن کامپوننتهای سفارشی بسازیم.
کامپوننتهای سفارشی
دو روش برای تعریف کردن یک کامپوننت در ریاکت وجود دارد:
کامپوننت تابع
1const BlogPostExcerpt = () => {
2 return (
3 <div>
4 <h1>Title</h1>
5 <p>Description</p>
6 </div>
7 )
8}
کامپوننت کلاس
1import React, { Component } from 'react'
2class BlogPostExcerpt extends Component {
3 render() {
4 return (
5 <div>
6 <h1>Title</h1>
7 <p>Description</p>
8 </div>
9 )
10 }
11}
تا همین اواخر کامپوننتهای کلاس تنها روش ما برای تعریف کردن یک کامپوننت بودند که دارای State باشد و بتواند به متدهای چرخه عمر دسترسی داشته باشد و کارهایی را در زمان رندر اولیه، بهروزرسانی یا حذف کامپوننت انجام دهد.
«قلابها» (Hooks) در ریاکت این وضعیت را تغییر دادهاند و بنابراین کامپوننتهای تابع ما اینک میتوانند بسیار قدرتمندتر از همیشه باشند و در آینده احتمالاً شاهد کامپوننتهای کلاس چندانی نخواهیم بود؛ هر چند کامپوننتهای کلاس هنوز هم روشی معتبر برای ایجاد کامپوننت خواهند بود. ساختار سومی نیز وجود دارد که از دستورهای ES6 بدون کلاس استفاده میکند:
1import React from 'react'
2React.createClass({
3 render() {
4 return (
5 <div>
6 <h1>Title</h1>
7 <p>Description</p>
8 </div>
9 )
10 }
11})
امروزه در کدهای مدرن ES6 دیگر شاهد چنین ساختارهایی نیستیم.
State
در این بخش در مورد مفهوم State در ریاکت توضیح خواهیم داد.
تعیین State پیشفرض برای یک کامپوننت
this.state را در سازنده کامپوننت مقداردهی اولیه میکنیم. برای نمونه کامپوننت BlogPostExcerpt میتواند یک State به صورت clicked داشته باشد:
1class BlogPostExcerpt extends Component {
2 constructor(props) {
3 super(props)
4 this.state = { clicked: false }
5 }
6 render() {
7 return (
8 <div>
9 <h1>Title</h1>
10 <p>Description</p>
11 </div>
12 )
13 }
14}
دسترسی به State
State به نام clicked میتواند با ارجاع به this.state.clicked مورد دسترسی قرار گیرد:
1class BlogPostExcerpt extends Component {
2 constructor(props) {
3 super(props)
4 this.state = { clicked: false }
5 }
6 render() {
7 return (
8 <div>
9 <h1>Title</h1>
10 <p>Description</p>
11 <p>Clicked: {this.state.clicked}</p>
12 </div>
13 )
14 }
15}
تغییرپذیر ساختن State
یک State را هرگز نباید با کد زیر تغییر داد:
1this.state.clicked = true
به جای آن؛ همواره باید از ()setState استفاده کرد و آن را به یک شیء ارسال کرد:
1this.setState({ clicked: true })
این شیء میتواند شامل یک زیرمجموعه یا یک ابرمجموعه (superset) از State باشد. تنها مشخصاتی که ارسال میکنید، تغییر خواهند یافت و آنهایی که نادیده گرفته شوند، در State کنونی باقی میمانند.
چرا همواره باید از ()setState استفاده کنیم؟
دلیل این مسئله آن است که در این متد، ریاکت میداند که State تغییر یافته است. سپس شروع به یک سری از رویدادها میکند که منجر به رندر گیری مجدد از کامپوننت و همچنین بهروزرسانی DOM میشود.
گردش داده غیر جهتدار
یک State همواره تحت مالکیت یک کامپوننت است. هر دادهای که از این State تأثیر بپذیرد، تنها کامپوننتهای زیرش یعنی فرزندانش را تحت تأثیر قرار میدهد.
تغییر دادن State یک کامپوننت هرگز بر والدین آن یا همنیاهایش و یا هر کامپوننت دیگر در اپلیکیشن تأثیر نمیگذارد و صرفاً فرزندانش را تحت تأثیر قرار میدهد.
به همین دلیل است که State در اغلب موارد به درخت کامپوننت منتقل میشود.
انتقال دادن State به درخت
به دلیل وجود قاعده گردش غیر جهتدار داده، اگر دو کامپوننت نیاز به اشتراک State داشته باشند، این State باید به جد مشترک آنها انتقال یابد. در اغلب موارد نزدیکترین جد، بهترین مکان برای مدیریت State است؛ اما این یک قاعده اجباری نیست. State به کامپوننتهایی که به آن نیاز دارند از طریق props ارسال میشود:
1class Converter extends React.Component {
2 constructor(props) {
3 super(props)
4 this.state = { currency: '€' }
5 }
6 render() {
7 return (
8 <div>
9 <Display currency={this.state.currency} />
10 <CurrencySwitcher currency={this.state.currency} />
11 </div>
12 )
13 }
14}
در این مرحله State میتواند به وسیله کامپوننت فرزند از طریق ارسال یک تابع تغییردهنده (mutating) به صورت یک prop تغییر یابد:
1class Converter extends React.Component {
2 constructor(props) {
3 super(props)
4 this.state = { currency: '€' }
5 }
6 handleChangeCurrency = event => {
7 this.setState({ currency: this.state.currency === '€' ? '$' : '€' })
8 }
9 render() {
10 return (
11 <div>
12 <Display currency={this.state.currency} />
13 <CurrencySwitcher
14 currency={this.state.currency}
15 handleChangeCurrency={this.handleChangeCurrency}
16 />
17 </div>
18 )
19 }
20}
21const CurrencySwitcher = props => {
22 return (
23 <button onClick={props.handleChangeCurrency}>
24 Current currency is {props.currency}. Change it!
25 </button>
26 )
27}
28const Display = props => {
29 return <p>Current currency is {props.currency}.</p>
30}
Props
Props به شیوه دریافت مشخصات از سوی کامپوننتها گفته میشود. اگر از کامپوننت فوقانی شروع کنیم، هر کامپوننت فرزند، prop-های خود را از والد دریافت میکند. در یک کامپوننت تابع، props همه آن چیزی است که ارسال میشود و با افزودن props به عنوان آرگومان تابع قابل دسترسی هستند:
1const BlogPostExcerpt = props => {
2 return (
3 <div>
4 <h1>{props.title}</h1>
5 <p>{props.description}</p>
6 </div>
7 )
8}
در یک کامپوننت کلاس، props به صورت پیشفرض ارسال میشوند. نیازی به افزودن هیچ چیز خاصی نیست و از طریق this.props در یک وهله از کامپوننت در دسترس هستند:
1import React, { Component } from 'react'
2class BlogPostExcerpt extends Component {
3 render() {
4 return (
5 <div>
6 <h1>{this.props.title}</h1>
7 <p>{this.props.description}</p>
8 </div>
9 )
10 }
11}
ارسال props به کامپوننتهای فرزند، روشی عالی برای ارسال مقادیر درون یک اپلیکیشن محسوب میشود. یک کامپوننت یا دادهها را نگهداری میکند، یعنی دارای State است و یا دادهها را از طریق props خود دریافت میکند.
در موارد زیر این وضعیت پیچیده میشود:
- اگر بخواهیم به State یک کامپوننت از یک فرزند دسترسی داشته باشیم که چند سطح پایینتر است. چون همه کامپوننتهای میانی باید پست سر گذاشته شوند، هر چند به دانستن State نیازی نداشته باشند و این موجب پیچیدهتر شدن امور میشود.
- در مواردی که نیاز باشد State یک کامپوننت از یک کامپوننت کاملاً نامرتبط گرفته شود.
مقادیر پیشفرض برای Props
اگر هر مقداری مورد نیاز نباشد، باید در صورتی که در زمان مقداردهی اولیه مقداری تعیین نشده باشد، یک مقدار پیشفرض برای آن تعیین کنیم:
1BlogPostExcerpt.propTypes = {
2 title: PropTypes.string,
3 description: PropTypes.string
4}
5BlogPostExcerpt.defaultProps = {
6 title: '',
7 description: ''
8}
برخی کیت ابزارها مانند ESLint (+) توانایی الزام تعریف کردن defaultProps برای یک کامپوننت با برخی propTypes که صریحاً مورد نیاز نیستند را فراهم ساختهاند.
Props چگونه ارسال میشوند؟
هنگامی که یک کامپوننت مقداردهی اولیه میشود، props به روشی مشابه خصوصیتهای HTML ارسال میشود:
1const desc = 'A description'
2//...
3<BlogPostExcerpt title="A blog post" description={desc} />
ما عنوان را به صورت یک رشته ساده و توضیح را به صورت یک متغیر ارسال میکنیم.
فرزندان (Children)
یک prop خاص وجود دارد که نام آن Children است. این prop شامل مقدار هر چیزی است که در body کامپوننت ارسال میشود. برای نمونه:
1<BlogPostExcerpt title="A blog post" description="{desc}">
2 Something
3</BlogPostExcerpt>
در این حالت، درون BlogPostExcerpt میتوانیم با بررسی this.props.children به Something دسترسی داشته باشیم.
با این که props به کامپوننت امکان میدهد که مشخصاتش را از والدینش دریافت کند و برای نمونه جهت نمایش برخی دادهها دستوراتی دریافت کند؛ اما State به کامپوننت اجازه میدهد که خودش حیات پیدا کند و مستقل از محیط پیرامون خودش باشد.
کامپوننتهای ارائهای در برابر کامپوننتهای کانتینر
در ریاکت، کامپوننتها غالباً به دو دسته بزرگ تقسیم میشوند که شامل «کامپوننتهای ارائهای» (Presentational) و «کامپوننتهای کانتینری» (container) هستند. هر کدام از این انواع دارای مشخصات منحصر به خود هستند.
کامپوننتهای ارائهای غالباً در مورد ایجاد برخی کدهای «نشانهگذاری» (markup) که در خروجی ارائه میشوند مورد استفاده قرار میگیرند. این کامپوننتها هیچ نوع State مگر آنها که برای ارائه استفاده میشوند را مدیریت نمیکنند.
کامپوننتهای کانتینر غالباً به عملیات «بکاند» (Back-end) مرتبط هستند. این کامپوننتها State کامپوننتهای فرعی مختلف را مدیریت میکنند. آنها میتوانند چندین کامپوننت ارائهای را در خود جای دهند. این وضعیت با استفاده از Redux قابل پیادهسازی به صورت اینترفیس است.
برای این که این تمایز را به بیانی سادهتر مطرح کنیم، میتوانیم بگوییم که کامپوننتهای ارائهای به نمای اپلیکیشن مرتبط هستند و کامپوننتهای کانتینر به طرز کار بخشهای مختلف ارتباط دارند.
برای نمونه، کامپوننت زیر از نوع ارائهای است و دادهها را از props خود میگیرد و صرفاً روی نمایش یک عنصر متمرکز است:
1const Users = props => (
2 <ul>
3 {props.users.map(user => (
4 <li>{user}</li>
5 ))}
6 </ul>
7)
از سوی دیگر کامپوننت زیر از نوع کانتینر است و به مدیریت و ذخیرهسازی دادههای خود پرداخته و از کامپوننت ارائهای برای نمایش آن استفاده میکند:
1class UsersContainer extends React.Component {
2 constructor() {
3 this.state = {
4 users: []
5 }
6 }
7 componentDidMount() {
8 axios.get('/users').then(users =>
9 this.setState({ users: users }))
10 )
11 }
12 render() {
13 return <Users users={this.state.users} />
14 }
15}
State در برابر Props
در یک کامپوننت React، منظور از props متغیرهایی هستند که از سوی کامپوننت والد ارسال میشوند. در سوی دیگر state نیز متغیر است، اما مستقیماً از سوی کامپوننت مقداردهی شده و مدیریت میشود.دقت کنید که state میتواند به وسیله props مقداردهی شود.
برای نمونه یک کامپوننت والد ممکن است کامپوننت فرزندش را با فراخوانی کد زیر شامل شود:
1<ChildComponent />
در این وضعیت، والد یک prop را با استفاده از ساختار زیر ارسال میکند:
1<ChildComponent color=green />
درون سازنده ChildComponent میتوانیم به صورت زیر به prop دسترسی داشته باشیم:
1class ChildComponent extends React.Component {
2 constructor(props) {
3 super(props)
4 console.log(props.color)
5 }
6}
و هر متد دیگر در این کلاس میتواند با استفاده از this.props به props اشاره کند. props میتوانند به صورت زیر برای تعیین state درونی بر اساس یک مقدار prop در سازنده مورد استفاده قرار گیرند:
1class ChildComponent extends React.Component {
2 constructor(props) {
3 super(props)
4 this.state.colorName = props.color
5 }
6}
البته یک کامپوننت میتواند state را بدون بررسی props آن نیز مقداردهی کند. این وضعیت فایده چندانی ندارد؛ اما تصور کنید قرار است بر اساس مقدار prop کارهای متفاوتی انجام دهید، احتمالاً تعیین مقدار یک state ، بهترین کار است. prop-ها هرگز نباید در یک کامپوننت فرزند تغییر یابند، بنابراین اگر چیزی در آن باشد که بخواهد متغیری را تغییر دهد، آن متغیر باید به state کامپوننت تعلق داشته باشد.
prop-ها میتوانند برای ایجاد امکان دسترسی کامپوننتهای فرزند به متدهای تعریف شده در کامپوننت والد نیز مورد استفاده قرار گیرند. این یک روش مناسب برای مدیریت متمرکز state در کامپوننت والد است. در این وضعیت دیگر لازم نیست که کامپوننتهای فرزند state خاصی برای خود داشته باشند.
اغلب کامپوننتها صرفاً نوعی اطلاعات را بر مبنای prop-ی که دریافت کردهاند نمایش میدهند و از این رو «بیحالت» (stateless) هستند.
انواع Prop
از آنجا که جاوا اسکریپت زبانی با نوعبندی دینامیک است، در عمل نیازی به داشتن روشی برای الزام تعیین نوع یک متغیر در زمان کامپایل نداریم و اگر از انواع نادرستی استفاده کنیم، صرفاً در زمان اجرا با مشکل مواجه میشویم. در صورتی که انواع ارسالی سازگار اما غیر مورد انتظار باشند، نتایج عجیبی در زمان اجرا به دست خواهد آمد.
در این زمینه Flow و TypeScrip کمک زیادی میکنند؛ اما ریاکت روشی برای کمک مستقیم در مورد انواع props دارد و این کار را حتی پیش از اجرای کد انجام میدهد. ابزارهای ما (ویرایشگرها و linter-ها میتوانند زمانی که مقادیر نادرستی ارسال میکنیم، آن را تشخیص دهند:
1import PropTypes from 'prop-types'
2import React from 'react'
3class BlogPostExcerpt extends Component {
4 render() {
5 return (
6 <div>
7 <h1>{this.props.title}</h1>
8 <p>{this.props.description}</p>
9 </div>
10 )
11 }
12}
13BlogPostExcerpt.propTypes = {
14 title: PropTypes.string,
15 description: PropTypes.string
16}
17export default BlogPostExcerpt
از چه نوعهایی میتوانیم استفاده کنیم؟
در ادامه انواع بنیادینی که میتوانیم بپذیریم را مشاهده میکنید:
- PropTypes.array
- PropTypes.bool
- PropTypes.func
- PropTypes.number
- PropTypes.object
- PropTypes.string
- PropTypes.symbol
ما میتوانیم یکی از دو نوع زیر را بپذیریم:
1PropTypes.oneOfType([
2 PropTypes.string,
3 PropTypes.number
4]),
ما میتوانیم یکی از مقادیر متعدد را بپذیریم:
1PropTypes.oneOf(['Test1'، 'Test2'])،
ما میتوانیم یک وهله از کلاس را بپذیریم:
1PropTypes.instanceOf(Something)
ما میتوانیم هر گره React را بپذیریم:
1PropTypes.node
یا حتی به طور کلی هر نوعی را بپذیریم:
1PropTypes.any
آرایهها ساختار خاصی دارند که میتوانیم برای پذیرش یک آرایه از نوع خاص استفاده کنیم:
1PropTypes.arrayOf(PropTypes.string)
ما میتوانیم مشخصات شیء را با استفاده از کد زیر ترکیب کنیم:
1PropTypes.shape({
2 color: PropTypes.string,
3 fontSize: PropTypes.number
4})
الزام مشخصات
الصاق به هر گزینه PropTypes موجب خواهد شد که ریاکت در موارد فقدان آن مشخصه، یک خطا بازگشت دهد:
1PropTypes.arrayOf(PropTypes.string).isRequired,
2PropTypes.string.isRequired,
فرگمان React
دقت کنید که چگونه مقادیر بازگشتی را در یک div قرار میدهیم. دلیل این امر آن است که یک کامپوننت میتواند صرفاً یک عنصر منفرد بازگشت دهد و اگر بخواهید بیش از یکی داشته باشید، باید آن را درون یک تگ کانتینر دیگر قرار دهید. با این وجود، این وضعیت منجر به وجود یک div غیر ضروری در خروجی میشود. شما میتوانید با استفاده از React.Fragment از این وضعیت اجتناب کنید:
1import React, { Component } from 'react'
2class BlogPostExcerpt extends Component {
3 render() {
4 return (
5 <React.Fragment>
6 <h1>{this.props.title}</h1>
7 <p>{this.props.description}</p>
8 </React.Fragment>
9 )
10 }
11}
ضمناً این وضعیت ساختار خلاصه بسیار زیبایی به صورت </><> دارد که صرفاً در نسخههای اخیر و Babel 7 به بعد پشتیبانی میشود:
1import React, { Component } from 'react'
2class BlogPostExcerpt extends Component {
3 render() {
4 return (
5 <>
6 <h1>{this.props.title}</h1>
7 <p>{this.props.description}</p>
8 </>
9 )
10 }
11}
رویدادها (Events)
ریاکت یک روش آسان برای مدیریت رویدادها ارائه کرده است. بدین ترتیب دیگر باید با addEventListener خداحافظی کنیم. در بخش قبلی در مورد state، مثال زیر را مشاهده کردیم:
1const CurrencySwitcher = props => {
2 return (
3 <button onClick={props.handleChangeCurrency}>
4 Current currency is {props.currency}. Change it!
5 </button>
6 )
7}
اگر تاکنون از جاوا اسکریپت استفاده کرده باشید، متوجه میشوید که این کد تا حدود زیادی شبیه به «دستگیرههای رویداد» (event handlers) در جاوا اسکریپت است، به جز این که اقدام به تعریف کردن همه چیز در جاوا اسکریپت میکنیم و با HTML کاری نداریم و یک تابع؛ و نه یک رشته را ارسال میکنیم. نامهای رویداد واقعی کمی متفاوت هستند، چون در ریاکت از حالت شتری برای همه چیز استفاده میکنیم و از این رو onclick به onClick و onsubmit به onSubmit تبدیل میشوند.
صرفاً جهت اطلاع، باید بگوییم که این یک HTML به سبک قدیم است که با رویدادهای جاوا اسکریپت همراه شده است:
1<button onclick="handleChangeCurrency()">...</button>
Event Handler
این یک قرارداد مرسوم است که دستگیرههای رویداد را به صورت متدهایی روی کلاس کامپوننت تعریف کنیم:
1class Converter extends React.Component {
2 handleChangeCurrency = event => {
3 this.setState({ currency: this.state.currency === '€' ? '$' : '€' })
4 }
5}
همه دستگیرهها یک شیء رویداد را دریافت میکنند که به آن الصاق شوند و این وضعیت بر اساس دستورالعمل «W3C UI Events» (+) یک خصوصیت بین مرورگری است.
اتصال this در متدها
اگر از کامپوننتهای کلاس استفاده کنید، نباید متدهای bind را فراموش کنید. متدهای کلاسهای ES6 به صورت پیشفرض bind نشدهاند. این بدان معنی است که this تعریف نشده است؛ مگر این که متدهایی به صورت تابعهای Arrow تعریف کنید:
1class Converter extends React.Component {
2 handleClick = e => {
3 /* ... */
4 }
5 //...
6}
این وضعیت زمانی که از ساختار مقداردهی مشخصه به همراه Babel استفاده میکنیم مورد نیاز است. در موارد دیگر باید به صورت دستی در سازنده آن را «متصل» (bind) کرد:
1class Converter extends React.Component {
2 constructor(props) {
3 super(props)
4 this.handleClick = this.handleClick.bind(this)
5 }
6 handleClick(e) {}
7}
مرجع رویدادها
رویدادهای زیادی در ریاکت پشتیبانی میشوند و در ادامه یک فهرست خلاصه از آنها ارائه میکنیم:
کلیپ بورد
- onCopy
- onCut
- onPaste
ترکیببندی
- onCompositionEnd
- onCompositionStart
- onCompositionUpdate
صفحهکلید
- onKeyDown
- onKeyPress
- onKeyUp
فوکوس
- onFocus
- onBlur
فرم
- onChange
- onInput
- onSubmit
ماوس
- onClick
- onContextMenu
- onDoubleClick
- onDrag
- onDragEnd
- onDragEnter
- onDragExit
- onDragLeave
- onDragOver
- onDragStart
- onDrop
- onMouseDown
- onMouseEnter
- onMouseLeave
- onMouseMove
- onMouseOut
- onMouseOver
- onMouseUp
انتخاب
- onSelect
لمس
- onTouchCancel
- onTouchEnd
- onTouchMove
- onTouchStart
رابط کاربری
- onScroll
چرخ ماوس
- onWheel
رسانه
- onAbort
- onCanPlay
- onCanPlayThrough
- onDurationChange
- onEmptied
- onEncrypted
- onEnded
- onError
- onLoadedData
- onLoadedMetadata
- onLoadStart
- onPause
- onPlay
- onPlaying
- onProgress
- onRateChange
- onSeeked
- onSeeking
- onStalled
- onSuspend
- onTimeUpdate
- onVolumeChange
- onWaiting
تصویر
- onLoad
- onError
انیمیشن
- onAnimationStart
- onAnimationEnd
- onAnimationIteration
گذار (Transition)
- onTransitionEnd
رویدادهای چرخه عمر
کامپوننتهای کلاس ریاکت میتوانند برای چندین رویداد چرخه عمر قلاب داشته باشند.
قلابها به کامپوننتهای تابع امکان دسترسی به روشی متفاوت را میدهند. در طی چرخه عمر یک کامپوننت یک سری از رویدادها هستند که فراخوانی میشوند و برای هر رویداد میتوانید قلابی داشته باشید که کارکردهای سفارشی خاصی را ارائه کند. در ادامه بررسی میکنیم که کدام قلاب برای کدام کارکرد مناسبتر است. ابتدا باید اشاره کنیم که در چرخه عمر کامپوننتهای ریاکت سه مرحله داریم:
- فعال شدن (Mounting)
- بهروزرسانی (Updating)
- غیر فعال شدن (Unmounting)
Mounting
در زمان فعال شدن کامپوننت با 4 متد چرخه عمر مواجه هستیم که پیش از این که کامپوننت روی DOM سوار شود فعال میشوند: constructor ،getDerivedStateFromProps ،render و componentDidMount.
- سازنده (Constructor)
این نخستین متدی است که هنگام فعال شدن کامپوننت فراخوانی میشود.
ما معمولاً از سازنده برای راهاندازی State اولیه با استفاده از ...= this.state بهره میگیریم.
- ()getDerivedStateFromProp
هنگامی که State به props وابسته باشد، getDerivedStateFromProps میتواند برای بهروزرسانی State بر مبنای مقدار props مورد استفاده قرار گیرد. این متد در نسخه 16.3 ریاکت و باهدف جایگزینی برای متد منسوخ componentWillReceiveProps عرضه شده است.
در این متد ما به this دسترسی نداریم، زیرا یک متد استاتیک است. از سوی دیگر این یک متد «محض» (pure) است و از این رو نباید موجب هیچ گونه عارضه جانبی شود و همچنین نباید هنگامی که چندین بار با ورودیهای یکسان فراخوانی میشود، خروجیهای متفاوتی ارائه دهد. این متد یک شیء با عناصر بهروزرسانی شده State بازگشت میدهد. در حالتی نیز که حالت تغییر نیافته باشد، تهی خواهد بود.
- ()render
این متد JSX را بازگشت میدهد که اینترفیس کامپوننت را میسازد. این متد نیز یک متد محض است و از این رو موجب هیچ عوارض جانبی نشده و در موارد فراخوانی مکرر با ورودیهای یکسان، خروجیهای یکسانی ارائه خواهد کرد.
- ()componentDidMount
این متد برای اجرای فراخوانیهای API مورد استفاده قرار میگیرد یا عملیات روی DOM را مورد پردازش قرار میدهد.
مرحله به روز رسانی (Updating)
زمانی که مشغول بهروزرسانی کامپوننت هستیم 5 متد چرخه عمر وجود دارد که پیش از فعال شدن کامپوننت در DOM تعریف شدهاند: getDerivedStateFromProps، shouldComponentUpdate، render، getSnapshotBeforeUpdate و componentDidUpdate.
- ()getDerivedStateFromProps
در بخش قبل در این مورد توضیح داده شد.
- ()shouldComponentUpdate
این متد یک مقدار بولی به صورت true یا false بازگشت میدهد. از این متد در مواردی استفاده میکنیم که میخواهیم به ریاکت بگوییم باید اقدام به رندرگیری بکند یا نه و مقدار پیشفرض آن true است. هنگامی که رندرگیری پرهزینه باشد و یا کنترل بیشتری در مورد زمان وقوع آن مورد نیاز باشد از مقدار false استفاده میشود.
- ()render
در بخش قبل در این مورد توضیح داده شد.
- ()getSnapshotBeforeUpdate
در این متد ما به props و State رندر قبلی و همچنین رندر کنونی دسترسی پیدا میکنیم. موارد استفاده از این متد معدود هستند و جزو متدهایی هستند که احتمالاً سر و کار شما بسیار کم به آن خواهد افتاد.
- ()componentDidUpdate
این متد زمانی فراخوانی میشود که کامپوننت در DOM بهروزرسانی شده باشد. از این متد برای اجرای API شخص ثالث DOM که باید در هنگام تغییرات DOM بهروزرسانی شود استفاده میکنیم. این متد متناظر با متد ()componentDidMount در مرحله فعالسازی کامپوننت است.
غیر فعالسازی کامپوننت (Unmounting)
در این مرحله ما تنها یک متد داریم که componentWillUnmount است و در ادامه توضیح دادهایم:
- ()componentWillUnmount
این متد زمانی فراخوانی میشود که کامپوننت از DOM حذف شود. از این متد برای اجرای هر نوع پاکسازی مورد نیاز، استفاده میشود.
دقت کنید که اگر از هر یک از متدهای componentWillMount ،componentWillReceiveProps یا componentWillUpdate استفاده میکنید، این موارد در نسخه 16.3 منسوخ شدهاند و باید به متدهای جدیدتر چرخه عمر مراجعه کنید.
بدین ترتیب به پایان این مطلب در خصوص مفاهیم عمیق React رسیدهایم؛ اما توجه داشته باشید که این مفاهیم به این موضوعات محدود نمی شوند و در بخش بعدی (+) به پیگیری ادامه این بحث خواهیم پرداخت.
برای مطالعه بخش بعدی این مجموعه مطلب روی لینک زیر کلیک کنید:
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای برنامه نویسی
- مجموعه آموزشهای طراحی و برنامه نویسی وب
- مجموعه آموزشهای مهندسی نرم افزار
- آموزش React.js در کمتر از ۵ دقیقه — از صفر تا صد
- واکشی (Fetch) کردن داده ها در اپلیکیشن های React — به زبان ساده
- راهنمای جامع React (بخش اول) — از صفر تا صد
==
بسیار عالی
مطالب وبلاگ فوق العاده با ارزشه ممنون از شما