در قسمت دوم آموزش React، موضوعات و مباحث مربوط به:
- اداره کننده رویداد ها
- Render شرطی
- لیست و کلید
- فرم ها
را بررسی و مشاهده کردیم که به چه صورتی می توان از این امکانات استفاده کرد.
در قسمت سوم و پایانی آموزش React، موضوع State های اشتراکی را شروع می کنیم و مشاهده خواهیم کرد که تبادل داده بین کامپوننت ها به چه صورت است و در ادامه، موضوع ترکیب کامپوننت ها با یکدیگر و استفاده مجدد از آنها را بررسی می کنیم.
در ادامه مطلب همراه من باشید.
انتقال State به یک سطح بالاتر
گاهی اوقات لازم است تا چندین کامپوننت، تغییر داده های خود را به یکدیگر انعکاس دهند. در چنین مواقعی می توانیم یک State اشتراکی در یک کامپوننت، که در لایه بالاتر از کامپوننت هایی که قصد تبادل داده های خود را دارند تعریف و از آن استفاده کنیم. که در ادامه این موضوع را بررسی می کنیم.
در این بخش می خواهیم یک کامپوننت ایجاد کنیم که مقدار دمایی را دریافت کند و مشخص کند که آب در درمای وارد شده به جوش می آید یا خیر. در ابتدا با یک کامپوننت به نام BoilingVerdict شروع می کنیم. این کامپوننت یک مقدار دما در واحد سلسیوس دریافت می کند و مشخص می کند که آیا این درجه برای به جوش آوردن آب کافی است یا خیر.
function BoilingVerdict(props) {
if (props.celsius >= 100) {
return <p>The water would boil.</p>;
}
return <p>The water would not boil.</p>;
}
سپس یک کامپوننت دیگر به نام Calculator ایجاد می کنیم که دارای یک input است که دما را در آن وارد کنیم و مقدار وارد شده را در this.state.temperature نگهداری می کند. علاوه بر این BoilingVerdict، را برای مقدار input رندر می کند.
class Calculator extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {temperature: ''};
}
handleChange(e) {
this.setState({temperature: e.target.value});
}
render() {
const temperature = this.state.temperature;
return (
<fieldset>
<legend>Enter temperature in Celsius:</legend>
<input
value={temperature}
onChange={this.handleChange} />
<BoilingVerdict
celsius={parseFloat(temperature)} />
</fieldset>
);
}
}
اضافه کردن Input دوم
یک input دیگر علاوه بر واحد Celsius برای واحد Farenheit در نظر می گیریم و باید توجه داشته باشیم که این دو input باید با هم همگام باشند بدین صورت که اگر مقدار یکی از آنها تغییر کرد مقدار input دیگری هم بر اساس آن تغییر کند. می توانیم کامپوننت TemperatureInput را از کامپوننت Calculator استخراج کنیم. همچنین یک prop به نام scale به کامپوننت اضافه می کنیم که مقدار آن می تواند “c” یا “f” باشد.
const scaleNames = {
c: 'Celsius',
f: 'Fahrenheit'
};
class TemperatureInput extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {temperature: ''};
}
handleChange(e) {
this.setState({temperature: e.target.value});
}
render() {
const temperature = this.state.temperature;
const scale = this.props.scale;
return (
<fieldset>
<legend>Enter temperature in {scaleNames[scale]}:</legend>
<input value={temperature}
onChange={this.handleChange} />
</fieldset>
);
}
}
حال در اینجا کامپوننت Calculator را به صورتی تغییر می دهیم که دو temperature input را نمایش دهد.
class Calculator extends React.Component {
render() {
return (
<div>
<TemperatureInput scale="c" />
<TemperatureInput scale="f" />
</div>
);
}
}
در حال حاضر ما دو input داریم اما زمانی که در یکی از آنها دما را وارد می کنیم، input دیگر بروز نمی شود که این با خواسته ما تفاوت دارد، زیرا ما می خواهیم هر دوی آنها با هم همگام باشند. همچنین نمی توانیم BoilingVerdict را داخل کامپوننت Calculator نمایش دهیم. کامپوننت Calculator دمای فعلی را نمی داند زیرا در TempratureInput به صورت state تعریف شده است و کامپوننت Calculator به آن دسترسی ندارد.
ایجاد توابع تبدیل
در این قسمت می خواهیم دو تابع ایجاد کنیم که Celsius را به Farenheit و بالعکس تبدیل کند.
function toCelsius(fahrenheit) {
return (fahrenheit - 32) * 5 / 9;
}
function toFahrenheit(celsius) {
return (celsius * 9 / 5) + 32;
}
این دو تابع برای تبدیل اعداد می باشند. ما یک تابع دیگر ایجاد می کنیم که یک مقدار رشته ای در قالب اولین آرگومان با نام temperature و یک تابع به عنوان آرگومان دوم با نام convert دریافت می کند، خروجی این تابع یک رشته می باشد. ما از این تابع برای محاسبه مقدار یک input بر اساس مقدار input دیگر استفاده می کنیم. در صورتی که مقدار آرگومان temperature معتبر نباشد این تابع یک رشته خالی بر می گرداند و خروجی را با سه رقم اعشار رند می کند.
function tryConvert(temperature, convert) {
const input = parseFloat(temperature);
if (Number.isNaN(input)) {
return '';
}
const output = convert(input);
const rounded = Math.round(output * 1000) / 1000;
return rounded.toString();
}
برای مثال، tryConvert(‘abc’, toClesius) رشته خالی را بر می گرداند و tryConvert(’10.22’, toFarenheit) مقدار ’50.396’ را بر می گرداند.
ارسال state به سطح بالاتر
در حال حاضر کامپوننت های TempratureInput به صورت مستقل مقادیرشان را در state مربوط به خودشان نگهداری می کنند:
class TemperatureInput extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {temperature: ''};
}
handleChange(e) {
this.setState({temperature: e.target.value});
}
render() {
const temperature = this.state.temperature;
// ...
در صورتیکه ما می خواهیم این دو input با یکدیگر sync باشند و زمانی که ما Celsius input را بروزرسانی می کنیم، Farenheit input دمای تبدیل شده را منعکس کند و بالعکس.
در React، اشتراک state بوسیله تغییر مکان آن به کامپوننتی که به آن نیاز دارد انجام می شود. که این کار “lifting state up” نامیده می شود. ما State محلی را از TempratureInput حذف می کنیم و آن را به Calculator منتقل می کنیم.
در حال حاضر کامپوننت Calculator، State اشتراکی را نگهداری می کند و برای مقدار دما ها در هر دو input، “source for truth” خوانده می شود و بدین ترتیب می تواند برای آنها مقادیری که با یکدیگر سازگار هستند در نظر بگیرد. به خاطر اینکه props هر دو کامپوننت TempratureInput از کامپوننت والد، یعنی Calculator می آید هر دو Input همیشه با یکدیگر همگام خواهند بود.
حال قدم به قدم بررسی می کنیم که چه اتفاقی در برنامه رخ داده است:
در ابتدا ما this.state.temprature را با this.props.temprature در کامپوننت TempratureInput جایگزین می کنیم. در حال حاضر ما props را از کامپوننت Calculator به کامپوننت TempratureInput ارسال نکردیم و بعدا اینکار را انجام می دهیم:
render() {
// Before: const temperature = this.state.temperature;
const temperature = this.props.temperature;
// ...
می دانیم که props ها فقط خواندنی هستند. زمانی که temperature در State محلی قرار داشت، کامپوننت TempratureInput برای تغییر آن فقط می توانست this.setState را فراخوانی کند و در حال حاضر مقدار temperature از کامپوننت والد به عنوان prop به کامپوننت TempratureInput فرستاده می شود که این کامپوننت نمی تواند هیچ کنترلی بر آن داشته باشد.
در React، این مشکل معمولا با کنترلی کردن کامپوننت حل می شود. همانطوری که در DOM، یک input می تواند prop های value و onChange داشته باشد، کامپوننت سفارشی TempratureInput هم می تواند مقادیر temperature و onTempratureChange را از کامپوننت والد یعنی Calculator دریافت کند.
زمانی که TempratureInput قصد بروزرسانی مقدار temperature را داشته باشد تابع this.props.onTempratureChange را فراخوانی می کند.
handleChange(e) {
// Before: this.setState({temperature: e.target.value});
this.props.onTemperatureChange(e.target.value);
// ...
نکته:
تفاوت خاصی بین نام های props، temperature و onTempratureCahnge وجود ندارد و می توانیم از نام هایی همچون value و onChange که به صورت قرار دادی می باشند استفاده کنیم.
onTempratureChange و temprature، prop هایی هستند که توسط کامپوننت Calculator که والد این کامپوننت است ارائه شده است. اداره کننده رویداد onTempratureChange تغییرات را با استفاده از ویرایش State محلی اداره می کند و هر دو input را با مقادیر جدیدشان render مجدد می کند.
قبل از اینکه تغییرات کامپوننت Calculator را مشاهده کنیم ، تغییرات کامپوننت TempratureInput را با هم بررسی می کنیم.
در ابتدا ما State محلی را از کامپوننت حذف کردیم و به جای آنکه مقادیر را از this.state.temprature بخواند از this.props.temprature می خواند و زمانی که تغییرات صورت می گیرد به جای آنکه از فراخوانی this.setState استفاده کند از this.props.onTempratureChange استفاده می کند که توسط Calculator ارائه می شود:
class TemperatureInput extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
}
handleChange(e) {
this.props.onTemperatureChange(e.target.value);
}
render() {
const temperature = this.props.temperature;
const scale = this.props.scale;
return (
<fieldset>
<legend>Enter temperature in {scaleNames[scale]}:</legend>
<input value={temperature}
onChange={this.handleChange} />
</fieldset>
);
}
}
حالا در این قسمت تغییرات کامپوننت Calculator را بررسی می کنیم.
ما temperature و scale که مربوط به input ها بود را در State محلی کامپوننت Calculator ذخیره کردیم. در پایین نمونه ای از state که برای رندر input ها لازم داریم را مشاهده می کنید:
برای مثال، اگر ما مقدار 37 را در Celsius input وارد کنیم، state کامپوننت Calculator به صورت زیر می شود:
{
temperature: '37',
scale: 'c'
}
و در صورتی که Farenheit input را 212 وارد کنیم، state کامپوننت Calculator به صورت زیر تغییر می کند:
{
temperature: '212',
scale: 'f'
}
ما می توانیم مقدار هر دو input را ذخیره کنیم اما این کار ضرورتی ندارد. فقط کافی است تا مقدار آخرین input و scale آن را نگهداری کنیم و مقدار input دیگر را بر اساس مقدار درجه فعلی که در دیگر input وارد شده است محاسبه کنیم.
با توجه به اینکه مقادیر Input ها بر طبق یک state محاسبه می شود همیشه با هم همگام هستند.
class Calculator extends React.Component {
constructor(props) {
super(props);
this.handleCelsiusChange = this.handleCelsiusChange.bind(this);
this.handleFahrenheitChange = this.handleFahrenheitChange.bind(this);
this.state = {temperature: '', scale: 'c'};
}
handleCelsiusChange(temperature) {
this.setState({scale: 'c', temperature});
}
handleFahrenheitChange(temperature) {
this.setState({scale: 'f', temperature});
}
render() {
const scale = this.state.scale;
const temperature = this.state.temperature;
const celsius = scale === 'f' ? tryConvert(temperature, toCelsius) : temperature;
const fahrenheit = scale === 'c' ? tryConvert(temperature, toFahrenheit) : temperature;
return (
<div>
<TemperatureInput
scale="c"
temperature={celsius}
onTemperatureChange={this.handleCelsiusChange} />
<TemperatureInput
scale="f"
temperature={fahrenheit}
onTemperatureChange={this.handleFahrenheitChange} />
<BoilingVerdict
celsius={parseFloat(celsius)} />
</div>
);
}
}
حالا دیگر مهم نیست که کدام input را ویرایش می کنیم، this.state.temprature و this.state.scale در Calculator بروزرسانی می شود. اگر در یکی از input ها مقداری را وارد کنیم، مقدار input دیگر بر اساس آن input بروز می شود.
در اینجا به صورت خلاصه آنچه که زمان ویرایش input رخ می دهد را بررسی می کنیم:
React تابعی که در onChange عنصر <input> مشخص کردیم فراخوانی می کند. در مثال ما متد handleChange در کامپوننت TempratureInput می باشد.
متد handleChange در کامپوننت TempratureInput، this.props.onTempratureChange را با مقدار مورد نظرش فراخوانی می کند. props این کامپوننت شامل onTempratureChange می باشد که توسط component والد یعنی Calculator ارائه می شود.
زمانی که کامپوننت رندر می شود، Calculator، متد onTempratureChange مربوط به Celsius را با متد handleCelsiusChange مشخص می کند و onTempratureChange مربوط به Farenheit را با متد handleFarenheitChange مشخص می کند. بنابراین این دو متد یعنی handleCelsiusChange و handleFarenheitChange که داخل کامپوننت Calculator هستند بر اساس input که تغییر کرده است فراخوانی می شوند.
در داخل این متد ها، کامپوننت Calculator با استفاده از فراخوانی متد this.setState() از React برای رندر مجدد خودش با مقادیر جدید input ها سوال می کند و scale جاری را ویرایش می کند.
React متد render کامپوننت Calculator را برای بروزرسانی UI فراخوانی می کند. مقدار هر دو input بر اساس دما و Scale جاری محاسبه شده و تبدیل دما در همین متد انجام می شود.
React، متد render کامپوننت TempratureInput را با prop های جدیدشان فراخوانی و UI را بروزرسانی می کند.
React، متد render کامپوننت BolingVerdict را فراخوانی می کند و دما را در واحد Celsius به عنوان props ارسال می کند.
ReactDOM، DOM را با BoilingVerdict و مقدار input ها مطابقت می دهد. Input که ما مقدارش را ویرایش کردیم مقدار جدیدش را دریافت می کند و input دیگر پس از تبدیل دما طبق مقدار input دیگر بروز می شود.
هر بروز رسانی در مقدار input بدین صورت انجام می شود و مقادیر input ها همگام باقی می مانند.
تا اینجا آموختیم که باید یک منبع برای هر داده ای که در React تغییر می کند وجود داشته باشد. به طور معمول، در ابتدا State به کامپوننتی که برای رندر به آن احتیاج داد اضافه می شود. سپس اگر کامپوننت های دیگر به آن نیاز داشته باشند، به جای اینکه سعی کنیم state ها را بین کامپوننت های مختلف، همگام نگه داریم می توانیم state را به یک لایه بالاتر انتقال دهیم.
انتقال state به یک لایه بالاتر باعث می شود تا کدهای ما کمتر مستعد تولید باگ باشند و می توانیم منطق های سفارشی را برای رد یا تبدیل ورودی کاربر در اختیار داشته باشیم.
اگر بتوانیم مقداری را هم از props و هم از state بخوانیم، احتمالا جایش در state نیست. برای مثال به جای ذخیره هر دو مقدار celsiusValue و farenheitValue، می توانیم آخرین مقدار temperature و scale را ذخیره کنیم و مقدار input، از این پس همیشه با مقدار آنها در متد render() محاسبه می شود.
زمانی که خطا یا اشکالی را در UI خود مشاهده کردیم، می توانیم از React DeveloperTools برای ردیابی props و state استفاده کنیم همچنین این ابزار به ما این امکان را می دهد که bug ها را تا محل وقوع آن ردیابی کنیم.
ترکیب و ارث بری
React دارای مدل ترکیب قدرتمندی است و معمولا از ترکیب به جای ارث بری به منظور استفاده مجدد در بین کامپوننت ها استفاده می شود.
در این بخش ما چندین مساله و مشکل را که توسعه دهندگان ممکن است در React با آن مواجه شوند را در نظر می گیریم و نشان می دهیم که چطور می توان آن ها را با ترکیب حل کرد.
در چنین کامپوننت هایی می توانیم از یک prop خاص به نام children برای فرستادن مستقیم عناصر فرزند به خروجی استفاده کنیم.
function FancyBorder(props) {
return (
<div className={'FancyBorder FancyBorder-' + props.color}>
{props.children}
</div>
);
}
این موضوع به دیگر کامپوننت ها این امکان را می دهد که فرزندان خود را توسط JSX در خروجی نمایش دهند.
function WelcomeDialog() {
return (
<FancyBorder color="blue">
<h1 className="Dialog-title">
Welcome
</h1>
<p className="Dialog-message">
Thank you for visiting our spacecraft!
</p>
</FancyBorder>
);
}
هرچیزی که داخل تگ <FancyBorder> موجود است به عنوان children prop به کامپوننت ارسال می شود. زمانی که FancyBorder، {props.children} را داخل یک <div> رندر می کند، عناصر فرستاده شده را در خروجی نمایش می دهد.
ممکن است گاهی اوقات فقط به چندین بخش از یک کامپوننت احتیاج داشته باشیم. در چنین مواقعی به جای فرزند باید از راه معمول استفاده کنیم.
function SplitPane(props) {
return (
<div className="SplitPane">
<div className="SplitPane-left">
{props.left}
</div>
<div className="SplitPane-right">
{props.right}
</div>
</div>
);
}
function App() {
return (
<SplitPane
left={
<Contacts />
}
right={
<Chat />
} />
);
}
عناصر React مانند <Contacts/> و <Chat/> به صورت شی هستند و می توانیم آنها را مانند دیگر مقادیر به صورت Props ارسال کنید.
ویژه سازی
گاهی اوقات ممکن است به نظر برسد که کامپوننت ها می توانند مورد خاصی از یک کامپوننت دیگر باشند. برای مثال، ممکن است بخواهیم کامپوننت WelcomeDialog یک مورد ویژه از Dialog باشد.
در React، این موضوع با ترکیب قابل انجام است، به طوریکه که یک کامپوننت ویژه، یک کامپوننت عمومی را را با prop هایش رندر می کند.
function Dialog(props) {
return (
<FancyBorder color="blue">
<h1 className="Dialog-title">
{props.title}
</h1>
<p className="Dialog-message">
{props.message}
</p>
</FancyBorder>
);
}
function WelcomeDialog() {
return (
<Dialog
title="Welcome"
message="Thank you for visiting our spacecraft!" />
);
}
همچنین موضوع ترکیب برای کامپوننت هایی که به صورت کلاس تعریف شده اند قابل انجام است.
function Dialog(props) {
return (
<FancyBorder color="blue">
<h1 className="Dialog-title">
{props.title}
</h1>
<p className="Dialog-message">
{props.message}
</p>
{props.children}
</FancyBorder>
);
}
class SignUpDialog extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.handleSignUp = this.handleSignUp.bind(this);
this.state = {login: ''};
}
render() {
return (
<Dialog title="Mars Exploration Program"
message="How should we refer to you?">
<input value={this.state.login}
onChange={this.handleChange} />
<button onClick={this.handleSignUp}>
Sign Me Up!
</button>
</Dialog>
);
}
handleChange(e) {
this.setState({login: e.target.value});
}
handleSignUp() {
alert(`Welcome aboard, ${this.state.login}!`);
}
}
درباره ارث بری
طبق توضیح توسعه دهندگان React، در فیسبوک از React در هزاران کامپوننت استفاده شده است، و در هیچ یک از موارد از ارث بری استفاده نشده است.
props و ترکیب در React انعطاف پذیری زیادی را به ما در سفارشی کردن ظاهر کامپوننت و رفتار آن می دهد. به یاد داشته باشید که کامپوننت ها به عنوان props می توانند:
- داده
- دیگر عناصر React
- یا توابع
را دریافت کنند.
اگر بخواهیم functionality غیر UI را بین کامپوننت ها استفاده کنیم می توانیم آنها را در یک ماژول جاوااسکریپت جدا قرار دهیم و کامپوننت ها را بدون extend کردن آن import و از توابع، اشیا یا کلاس استفاده کنیم.
فایل های آموزش React - قسمت سوم را می توانید از این لینک دانلود کنید.
منبع: reactjs.org
React | Javascript |