הרמת ה-State למעלה
These docs are old and won’t be updated. Go to react.dev for the new React docs.
These new documentation pages teach modern React and include live examples:
לעתים קרובות, מספר קומפוננטות צריכות לשקף את אותם נתונים משתנים. אנו ממליצים להרים את ה-state המשותף עד לאב הקדמון הקרוב ביותר. בואו נראה איך זה עובד.
בחלק זה, ניצור מחשבון טמפרטורה המחשב אם המים ירתחו בטמפרטורה נתונה.
נתחיל עם קומפוננטה שנקראת BoilingVerdict. היא מקבלת את הטמפרטורה ב-celsius בתור props, ומדפיסה אם הטמפרטורה מספיקה כדי להרתיח את המים:
function BoilingVerdict(props) {
if (props.celsius >= 100) {
return <p>המים ירתחו.</p>; }
return <p>המים לא ירתחו.</p>;}לאחר מכן, ניצור קומפוננטה שנקראת Calculator. היא מרנדרת <input> המאפשר לכם להזין את הטמפרטורה, ושומר על הערך שלה ב-this.state.temperature.
בנוסף, היא מרנדרת את BoilingVerdict עבור ערך הקלט הנוכחי.
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>הכנס טמפרטורה בצלזיוס:</legend>
<input value={temperature} onChange={this.handleChange} /> <BoilingVerdict celsius={parseFloat(temperature)} /> </fieldset>
);
}
}הוספת קלט שני
הדרישה החדשה שלנו היא, בנוסף לקלט בצלזיוס, אנו מספקים קלט בפרנהייט, והם נשארים מסונכרנים.
אנחנו יכולים להתחיל על ידי חילוץ קומפוננטת TemperatureInput מתוך Calculator. אנו נוסיף אליה prop חדש scale שיכול להיות "c" או "f":
const scaleNames = { c: 'צלזיוס', f: 'פרנהייט'};
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>הכנס טמפרטורה ב{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>
);
}
}יש לנו שני קלטים עכשיו, אבל כאשר אתם מכניסים את הטמפרטורה באחד מהם, השני אינו מתעדכן. זה סותר את הדרישה שלנו: אנחנו רוצים לשמור אותם מסונכרנים.
אנחנו גם לא יכולים להציג את BoilingVerdict מ-Calculator. ה-Calculator אינו יודע את הטמפרטורה הנוכחית משום שהיא מוסתרת בתוך ה-TemperatureInput.
כתיבת פונקציית המרה
ראשית, נכתוב שתי פונקציות כדי להמיר מצלזיוס לפרנהייט ובחזרה:
function toCelsius(fahrenheit) {
return (fahrenheit - 32) * 5 / 9;
}
function toFahrenheit(celsius) {
return (celsius * 9 / 5) + 32;
}שתי הפונקציות ממירות מספרים. נכתוב פונקציה אחרת שלוקחת מחרוזת temperature ופונקציית המרה כארגומנטים ומחזירה מחרוזת. נשתמש בה כדי לחשב את הערך של קלט אחד על סמך קלט אחר.
היא מחזירה מחרוזת ריקה עבור טמפרטורה (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', toCelsius) מחזיר מחרוזת ריקה, ו-tryConvert('10.22', toFahrenheit) מחזיר '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; // ... למרות זאת, אנחנו רוצים ששני הקלטים האלו יהיו מסונכרנים אחד עם השני. כאשר אנו מעדכנים את קלט צלזיוס, קלט פרנהייט צריך לשקף את הטמפרטורה המומרת, ואותו דבר בכיוון השני.
ב-React, שיתוף ה-state נעשה על ידי העברתו אל האב הקדמון המשותף הקרוב ביותר של הקומפוננטות הזקוקות לו. זה נקרא “הרמת ה-state למעלה”. אנו נסיר את ה-state המקומי מ-TemperatureInput ולנעביר אותו לתוך Calculator במקום.
אם Calculator הוא הבעלים של ה-state המשותף, הוא הופך להיות “מקור האמת” (“source of truth”) לטמפרטורה הנוכחית בשני הקלטים. זה יכול להנחות את שניהם להשתמש בערכים כך שיהיו עקביים אחד עם השני. מכיוון שה-props של שתי קומפוננטות TemperatureInput מגיעים מאותה קומפוננטת אב Calculator, שני הקלטים יהיו תמיד מסונכרנים.
בואו נראה איך זה עובד צעד אחר צעד.
ראשית, אנו מחליפים את 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 מגיע מההורה בתור prop, ל-TemperatureInput אין שליטה עליו.
ב-React, זה בדרך כלל נעשה על ידי הפיכת קומפוננטה ל-”נשלטת”. בדיוק כמו ב-DOM <input> מקבל גם value וגם prop של onChange, כך יכול גם ה-TemperatureInput המותאם אישית לקבל גם temperature וגם props של onTemperatureChange מההורה שלו Calculator.
עכשיו, שה-TemperatureInput רוצה לעדכן את הטמפרטורה שלו, הוא יקרא ל-this.props.onTemperatureChange:
handleChange(e) {
// קודם לכן: this.setState({temperature: e.target.value});
this.props.onTemperatureChange(e.target.value); // ...שימו לב:
אין משמעות מיוחדת לשמות ה-props
temperatureאוonTemperatureChangeבקומפוננטות מותאמות אישית. יכולנו לקרוא להם כל דבר אחר, כמו לקרוא להםvalueו-onChangeשהיא קונבנציה נפוצה.
ה-prop onTemperatureChange יועבר יחד עם ה-prop temperature על ידי קומפוננטת ההורה Calculator. היא תטפל בשינוי על ידי שינוי ה-state המקומי שלה, ובכך תרנדר מחדש את שני הקלטים עם ערכים חדשים. אנו נסתכל על המימוש החדש של 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>הכנס טמפרטורה ב{scaleNames[scale]}:</legend>
<input value={temperature}
onChange={this.handleChange} />
</fieldset>
);
}
}עכשיו בואו נפנה לקומפוננטת Calculator.
אנו נשמור את הקלטים הנוכחיים temperature ו-scale ב-state המקומי שלה. זהו ה-state ש-”הרמנו” מהקלטים, והוא ישמש “מקור האמת” עבור שניהם. זהו ייצוג מינימלי של כל הנתונים שאנחנו צריכים לדעת על מנת לרנדר את שני הקלטים.
לדוגמה, אם נכניס 37 לתוך הקלט של צלזיוס, ה-state של קומפוננטת Calculator יהיה:
{
temperature: '37',
scale: 'c'
}אם מאוחר יותר נערוך את השדה פרנהייט כך שיהיה 212, ה-state של Calculator יהיה:
{
temperature: '212',
scale: 'f'
}היינו יכולים לאחסן את הערך של שני הקלטים אבל מסתבר שזה יהיה מיותר. זה מספיק לאחסן את הערך של הקלט האחרון שהשתנה, ואת המדד שהוא מייצג. לאחר מכן אנו יכולים להסיק את הערך של הקלט האחר בהתבסס על ערכי temperature ו-scale הנוכחיים בלבד.
הקלטים נשארים מסונכרנים מכיוון שהערכים שלהם מחושבים מאותו 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>
);
}
}עכשיו, לא משנה איזה קלט אתם עורכים, this.state.temperature ו-this.state.scale ב-Calculator מתעדכנים. אחד הקלטים מקבל את הערך כפי שהוא, ולכן כל קלט משתמש נשמר, וערך הקלט האחר תמיד מחושב מחדש על בסיס הערך.
בואו נסכם את מה שקורה בעת עריכת קלט:
- React קוראת לפונקציה שצוינה כ-
onChangeעל ה-<input>ב-DOM. במקרה שלנו, זוהי המתודהhandleChangeבקומפוננטהTemperatureInput. - המתודה
handleChangeבקומפוננטהTemperatureInputקוראת ל-this.props.onTemperatureChange()עם הערך הרצוי החדש. ה-props שלה, כוללonTemperatureChange, סופקו על ידי רכיב האב שלה,Calculator. - כאשר הוא רונדר קודם לכן,
Calculatorציין כיonTemperatureChangeשלTemperatureInputצלזיוס היא המתודהhandleCelsiusChangeשלCalculator, ו-onTemperatureChangeשלTemperatureInputפרנהייט היא המתודהhandleFahrenheitChangeשלCalculator. אז אחת משתי מתודות אלה שלCalculatorתקרא כתלות בקלט אשר ערכנו. - בתוך המתודות הללו, הקומפוננטה
Calculatorמבקשת מ-React לרנדר מחדש את עצמה על ידי קריאה ל-this.setState()עם ערך הקלט החדש והמדד הנוכחי של הקלט שערכנו זה עתה. - React קוראת למתודת
renderשל קומפוננטתCalculatorכדי ללמוד איך ממשק המשתמש צריך להיראות. הערכים של שני הקלטים מחושבים מחדש בהתאם לטמפרטורה הנוכחית ולמדד הפעיל. ההמרה של הטמפרטורה מבוצעת כאן. - React קוראת למתודות
renderשל כל אחת מקומפוננטותTemperatureInputעם ה-props החדשים שלהן שמצויינים על ידיCalculator. היא לומדת איך ממשק המשתמש שלהם צריך להיראות. - React קוראת למתודת
renderשל קומפוננטתBoilingVerdict, כשהיא מעבירה את הטמפרטורה בצלזיוס כ-props שלה. - React DOM מעדכן את ה-DOM עם הכרעת מצב הרתיחה ועם התאמת הערכים הרצויים בקלט. הקלט שערכנו זה עתה מקבל את הערך הנוכחי שלו, והקלט האחר מתעדכן לטמפרטורה לאחר ההמרה.
כל עדכון עובר את אותם השלבים כך שהקלטים יישארו מסונכרנים.
לקחים שנלמדו
צריך להיות “מקור אמת” יחיד עבור כל נתון המשתנה באפליקציית React. בדרך כלל, ה-state מתווסף לראשונה לקומפוננטה הזקוקה לו. לאחר מכן, אם קומפוננטות אחרות גם צריכות אותו, אתם יכולים להרים אותו אל האב הקדמון המשותף הקרוב ביותר. במקום לנסות לסנכרן את ה-state בין קומפוננטות שונות, עליכם להסתמך על זרימת הנתונים מלמעלה למטה.
הרמת ה-state כרוכה יותר בכתיבת קוד “boilerplate” מאשר בגישות binding דו-כיווני, אך הרווח הוא שנדרשת פחות עבודה כדי לאתר ולבודד באגים. מאחר שכל state “חי” בתוך איזשהי קומפוננטה וקומפוננטה זו בלבד יכולה לשנות אותו, שטח הפנים לבאגים מופחת באופן משמעותי. בנוסף, באפשרותכם לממש כל לוגיקה מותאמת אישית כדי לדחות או לשנות קלט משתמש.
אם אנחנו יכולים לגזור משהו מה-props או מה-state, זה כנראה לא צריך להיות ב-state. לדוגמה, במקום לאחסן גם את celsiusValue וגם את fahrenheitValue, אנו מאחסנים רק את הטמפרטורה (temperature) האחרונה ואת המדד (scale) שלה. הערך של הקלט האחר יכול תמיד להיות מחושב מהם במתודה render(). זה מאפשר לנו לנקות או להחיל עיגול לשדה האחר מבלי לאבד כל דיוק בקלט המשתמש.
כאשר אתם רואים משהו שגוי בממשק המשתמש, תוכלו להשתמש בכלי הפיתוח של React כדי לבדוק את ה-props ולעבור למעלה בעץ עד שתמצאו את הקומפוננטה האחראית לעדכון ה-state. זה מאפשר לכם לעקוב אחר הבאגים עד למקור שלהם:
