انتقال state به بالا
اغلب، تغییر یک داده، منجر به واکنش چندین کامپوننت میشود. پیشنهاد ما انتقال state مشترک میان آنها به نزدیکترین کامپوننت بالادستی است که آنهار در بر دارد. بیایید با هم ببینیم در عمل چگونه کار میکند. در این بخش، ما یک دماسنج میسازیم که محاسبه میکند در دمای داده شده به آن آب جوش آمده است یا خیر.
ما با کامپوننتی شروع خواهیم کرد که به آن BoilingVerdict
میگویند.
این کامپوننت celsius
را به عنوان prop دریافت میکند و درصورت کافی بودن آن برای به جوش آوردن آب آن را چاپ میکند.
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 دوم
نیاز جدید ما این است که برای ورودی سلیسوس یک مقدار فارنهایت فراهم کنیم. که با هم همگام باشند.
ما میتوانیم با استخراج کردن کامپوننت 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
را در دو دمای مجزا رندر کنیم.
class Calculator extends React.Component {
render() {
return (
<div>
<TemperatureInput scale="c" /> <TemperatureInput scale="f" /> </div>
);
}
}
ما در حال حاضر دو input داریم که اگر شما در یکی از آنها دما وارد کنید، دیگری آپدیت نمیشود. که با نیاز ما در تناقض است: ما میخواهیم با هم همگام باشند.
همچنین نمیتوانیم BoilingVerdict
از Calculator
را نمایش دهیم.
Calculator
دمای کنونی را نمیداند زیرا در TemperatureInput
مخفی است.
نوشتن تابع تبدیل
در ابتدا ما دو تابع مینویسیم که دما را از سیلسیوس به فارنهایت و برعکس تبدیل کند:
function toCelsius(fahrenheit) {
return (fahrenheit - 32) * 5 / 9;
}
function toFahrenheit(celsius) {
return (celsius * 9 / 5) + 32;
}
این دو تابع اعداد را تبدیل میکنند. ما دو تابع خواهیم نوشت که temperature
از جنس string و تابع تبدیل را به عنوان آرگومان دریافت میکند و یک string برمیگرداند. ما از آن برای محاسبه مقدار یک input باتوجه به مقدار input دیگر استفاده میکنیم.
که این روی مقدار نامعتبر temperature
، string خالی برمیگرداند و خروجی را تا سه رقم اعشار رند میکند:
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', toCelsius)
، string خالی برمیگرداند و tryConvert('10.22', toFahrenheit)
، string '50.396'
را برمیگرداند.
انتقال state به بالا
در حال حاضر، هر دو کامپوننت TemperatureInput
به صورت جداگانه مقادیر خود را در 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 با هم همگام باشند. هنگامی که مقدار input سلیسیوس را بهروز میکنیم، مقدار تبدیل شده فارنهایت نیز باید منعکس شود، و برعکس.
در React ، به اشتراک گذاری state به صورت حرکت دادن آن به نزدیکترین جد کامپوننتی که به آن نیاز دارد است. به این “انتقال state به بالا” گفته میشود. ما state محلی TemperatureInput
را پاک میکنیم و به جای آن در Calculator
میبریم.
اگر Calculator
state به اشتراک گذاشته شده را ازآن خود کند، به “منبع حقیقت” برای هر دو input تبدیل میشود. و اینها را برای داشتن مقادیر نامتناقض آگاه میکند. از آنجایی که propهای هر دو کامپوننت TemperatureInput
از کامپوننت پدری یکسان به اسم Calculator
میآیند، هر دو input همیشه با هم همگام هستند.
بیایید قدم به قدم ببینیم که چگونه کار میکند.
اول، this.state.temperature
را با مقدار this.props.temperature
در کامپوننت TemperatureInput
جایگزین میکنیم. فعلا فرض میکنیم که this.props.temperature
قبلا وجود داشته است، گرچه در آینده باید آن را به Calculator
انتقال بدهیم:
render() {
// قبلا : const temperature = this.state.temperature;
const temperature = this.props.temperature; // ...
ما میدانیم که propsها فقط خواندنی هستند. هنگامی که temperature
در state محلی بود، TemperatureInput
فقط میتوانست this.setState()
را برای تغییرش صدا کند. گرچه حالا temperature
از پدر به عنوان props میآید TemperatureInput
هیچ کنترلی روی آن ندارد.
در React این مسله معمولا با تبدیل کامپوننت به “کنترل شده” حل میشود. درست مثل DOM که <input>
prop، value
و onChange
را قبول میکند، پس TemperatureInput
شخصی ما نیز میتواند propهای temperature
و onTemperatureChange
را از پدر خودش Calculator
قبول کند.
حالا، وقتی که TemperatureInput
بخواهد دمای خودش را بهروز رسانی کند، میتواند this.props.onTemperatureChange
را صدا کند.
handleChange(e) {
// قبلا : this.setState({temperature: e.target.value});
this.props.onTemperatureChange(e.target.value); // ...
توجه:
propهای
temperature
یاonTemperatureChange
هیچ مفهوم خاصی در کامپوننت شخصی ندارند. ما هر چه بخواهیم میتوانیم نامگذاری کنیم, مانندvalue
وonChange
که قراردادی، مرسوم هستند.
propهای onTemperatureChange
و temperature
توسط پدرشان فراهم خواهند شد. که تغییرات را در state محلی خودش رسیدگی میکند، سبب رندر مجدد جفت inputها میشود. خیلی زود به پیادهسازی Calculator
نگاه میاندازیم.
قبل از اینکه به تغییرات داخل Calculator
بپردازیم، بیایید تغییراتی که روی TemperatureInput
دادهایم را جمع بندی کنیم. ما state محلی را از آن پاک کردیم، و به جای خواندن از this.state.temperature
، اکنون از this.props.temperature
میخوانیم .وقتی میخواهیم تغییری بدهیم به جای صدا کردن this.setState()
اکنون this.props.onTemperatureChange()
را صدا میکنیم، که توسط 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 داخلیاش ذخیره میکنیم. این همان stateای هست که از inputها “به بالا” انتقال دادیم که برای هر دوی آنها به عنوان “منبع حقیقت” عمل میکند. این کمترین تمثالی از تمام دادهای است که برای رندر input به آن نیاز داریم .
برای مثال، اگر ما ۳۷ را در input سلیسیوس وارد کنیم، state کامپوننت Calculator
میشود:
{
temperature: '37',
scale: 'c'
}
اگر بعدا فارنهایت را به ۲۱۲ تغییر دهیم، state Calculator
خواهد بود:
{
temperature: '212',
scale: 'f'
}
ما میتوانیم که مقدار هر دو input را ذخیره کنیم ولی به نظر میرسد که ضروری نباشد.
همین کافیست که مقدار input که آخرین بار تغییر کرده به همراه scale که نماینده آن است ذخیره شود. سپس میتوانیم مقدار input را به تنهایی با temperature
و scale
استنتاج کنیم.
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.temperature
و this.state.scale
در Calculator
بهروز رسانی میشوند. یکی از inputها که مقدار بگیرد هر ورودی کاربر محفوظ میشود و input دیگر با توجه به آن مقدارش محاسبه میشود.
بیایید آنچه در ویرایش input اتفاق میافتد را با هم جمعبندی کنیم.
- React تابعی که به عنوان
onChange
روی DOM<input>
مشخص شده است را صدا میکند. در این مورد متدhandleChange
در کامپوننتTemperatureInput
میباشد. - متد
handleChange
در کامپوننتTemperatureInput
،this.props.onTemperatureChange()
را با مقدار جدیدش صدا میزند. که propاش, شاملonTemperatureChange
میباشد، که توسط کامپوننت پدر که همانCalculator
است فراهم شده است. - قبلا که رندر شد
Calculator
مشخص کرد کهonTemperatureChange
ای که برایTemperatureInput
سلیسوس بود متدی بهنامhandleCelsiusChange
درCalculator
است وonTemperatureChange
ای که برای فارنهایت بود متدی بهنامhandleFahrenheitChange
درCalculator
است. پس این دو متد از Calculator بسته به اینکه کدام input ویرایش شده است فراخوانی میشود. - درون این متدها, کامپوننت
Calculator
با درخواستthis.setState()
از React میخواهد با مقدار جدید input و scale کنونی input که تازه ویرایش شده خودش را دوباره رندر کند. - React متد
render
کامپوننتCalculator
را فراخوانی میکند تا بفهمد که UI به چه شکل باید باشد. مقدار هر دو input با توجه به دمای کنونی و scale فعال دوباره محاسبه میشود. تبدیل دما در اینجا انجام میشود. - React متد
render
هر یک از کامپوننتهایTemperatureInput
با propهای جدیدی کهCalculator
مشخص کرده است صدا میزند. و. و میفهمد که UI هر یک به چه شکل باید باشد. - React متد
render
از کامپوننتBoilingVerdict
را صدا میزند, و دمای سلیسوس را به عنوان prop ارسال میکند. - React DOM ، DOM را با حکم جوش بهروز رسانی میکند تا مقادیر ورودی مورد نظر را تطبیق دهد. inputای که درحال حاضر ویرایش کردیم مقدار خودش را میگیرد و input دیگر دمای خودش را بعد از تبدیل بهروز رسانی میکند.
هر به روز رسانی این مراحل را طی میکند تا در نهایت این دو input با هم همگام باشند.
درسهایی که آموختیم
باید برای هر دادهای که در React تغییر میکند یک “منبع حقیقت” وجود داشته باشد. معمولا state در ابتدا به کامپوننت برای رندر مجدد اضافه میشود. و اگر کامپوننت دیگری بهآن نیاز داشت، شما میتوانید آن را به بالا و نزدیکترین جد مشترک انتقال دهید. به جای اینکه سعی کنید state را بین کامپوننت های مختلف همگام کنید، باید به حالت جریان داده از بالا-به-پایین تکیه کنید.
بالا بردن state شامل نوشتن بیشتر “boilerplate” نسبت به روش binding دو-طرفه است، ولی به عنوان مزیت، هزینه کمتری برای پیدا کردن و کپسوله کردن باگها دارد. از آنجایی که هر state در برخی کامپوننتها “زندگی” میکند و آن کامپوننت به تنهایی میتواند آن را تغییر دهد، سطح وسیعی از باگها به طور چشمگیری کاهش پیدا میکند. علاوه بر این ، شما میتوانید منطق سفارشی برای رد یا انتقال ورودی کاربر پیاده سازی کنید.
اگر چیزی میتوانست از props یا state ناشی شود ، احتمالا نباید در state باشد. برای مثال ، به جای ذخیره کردن celsiusValue
و fahrenheitValue
، ما فقط آخرین temperature
و scale
تغییر کرده را ذخیزه میکنیم. مقدار سایز inputها میتواند در متد render()
محاسبه شود. این به ما امکان می دهد بدون از دست دادن دقت در ورودی کاربر ، سایر فیلدها را پاک یا رند کنیم.
وقتی میبینید چیزی در UI اشتباه است, میتواند از ابزار توسعه React برای بازرسی propها استفاده کنید و در درخت اینقدر بالا بروید تا کامپوننتی که مسول بهروز رسانی state هست را پیدا کنید. این به شما امکان میدهد تا باگها را تا منبع دنبال کنید.