State ומחזור חיים
דף זה מציג את הקונספט של state ומחזור חיים בקומפוננטת React. תוכלו למצוא את פירוט ה-API של קומפוננטה כאן.
הביטו על דוגמת השעון המתקתק מאחד מהחלקים הקודמים. ברינדור אלמנטים, למדנו רק דרך אחת לעדכון ממשק המשתמש. אנו קוראים ל-ReactDOM.render()
כדי לשנות את הפלט שירונדר:
const root = ReactDOM.createRoot(document.getElementById('root'));
function tick() {
const element = (
<div>
<h1>Hello, world!</h1>
<h2>It is {new Date().toLocaleTimeString()}.</h2>
</div>
);
root.render(element);}
setInterval(tick, 1000);
בפרק זה נלמד כיצד להפוך את קומפוננטת ה-Clock
(שעון) לכזאת שבאמת מאפשרת שימוש חוזר ומכומסת (מאפשרת אנקפסולציה). היא תקבע טיימר משלה ותעדכן את עצמה בכל שנייה.
אנחנו יכולים להתחיל על ידי כימוס של תצוגת השעון:
const root = ReactDOM.createRoot(document.getElementById('root'));
function Clock(props) {
return (
<div> <h1>Hello, world!</h1> <h2>It is {props.date.toLocaleTimeString()}.</h2> </div> );
}
function tick() {
root.render(<Clock date={new Date()} />);}
setInterval(tick, 1000);
עם זאת, אנו מפספסים דרישה חיונית: העובדה ש-Clock
מייצר טיימר ומעדכן את ממשק המשתמש בכל שנייה צריכה להיות חלק מהמימוש של Clock
.
באופן אידיאלי, אנחנו רוצים לכתוב את זה פעם אחת וש-Clock
יעדכן את עצמו:
root.render(<Clock />);
כדי לממש את זה, עלינו להוסיף “state” לקומפוננטת ה-Clock
.
State זהה ל-props, אבל הוא פרטי ונשלט במלואו על ידי הקומפוננטה.
המרת פונקציה למחלקה
אתם יכולים להמיר קומפוננטת פונקציה כמו Clock
למחלקה בחמישה צעדים:
- צרו מחלקת ES6, עם שם זהה, שמרחיבה את
React.Component
. - הוסיפו לה מתודה אחת ריקה בשם
render()
. - העבירו את גוף הפונקציה לתוך מתודת
render()
. - החליפו את
props
עםthis.props
בגוף ה-render()
. - מחקו את הצהרת הפונקציה הריקה שנותרה.
class Clock extends React.Component {
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.props.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
Clock
מוגדר כעת כמחלקה במקום כפונקציה.
מתודת render
תקרא בכל פעם שמתרחש עדכון, אך כל עוד אנו מרנדרים את <Clock />
אל אותה צומת DOM, נעשה שימוש רק במופע אחד של המחלקה Clock
. זה מאפשר לנו לעשות שימוש בתכונות נוספות כגון state מקומי ומתודות מחזור חיים.
הוספת State מקומי למחלקה
נעביר את ה-date
מ-props ל-state בשלושה שלבים:
- החליפו את
this.props.date
עםthis.state.date
במתודת ה-render()
:
class Clock extends React.Component {
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2> </div>
);
}
}
- הוסיפו בנאי מחלקה שמבצע השמה ל-
this.state
ההתחלתי:
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()}; }
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
שימו לב כיצד אנו מעבירים את props
לבנאי הבסיסי:
constructor(props) {
super(props); this.state = {date: new Date()};
}
קומפוננטות מחלקה צריכות תמיד לקרוא לבנאי הבסיס עם props
.
- הסירו את ה-prop
date
מאלמנט ה-<Clock />
:
root.render(<Clock />);
לאחר מכן נוסיף את קוד הטיימר בחזרה לקומפוננטה עצמה.
התוצאה נראית כך:
class Clock extends React.Component {
constructor(props) { super(props); this.state = {date: new Date()}; }
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2> </div>
);
}
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Clock />);
בשלב הבא, נדאג ש-Clock
יגדיר טיימר משלו ויעדכן את עצמו בכל שנייה.
הוספת מתודות מחזור חיים למחלקה
באפליקציות עם קומפוננטות רבות, חשוב מאוד לשחרר משאבים שנלקחו על ידי הקומפוננטות כאשר הן מושמדות.
אנחנו רוצים להגדיר טיימר בכל פעם ש-Clock
מרונדר לתוך ה-DOM בפעם הראשונה. זה נקרא “mounting” ב-React.
אנחנו גם רוצים לנקות את אותו טיימר בכל פעם שה-DOM שמיוצר על ידי ה-Clock
מוסר. זה נקרא “unmounting” ב-React.
אנו יכולים להכריז על מתודות מיוחדות במחלקת הקומפוננטה כדי להריץ קוד כלשהו כאשר קומפוננטה עושה mount ו-unmount:
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
componentDidMount() { }
componentWillUnmount() { }
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
מתודות אלו נקראות “מתודות מחזור חיים”.
מתודת componentDidMount()
רצה לאחר שפלט הקומפוננטה רונדר ל-DOM. זה מקום טוב להגדיר טיימר:
componentDidMount() {
this.timerID = setInterval( () => this.tick(), 1000 ); }
שימו לב איך אנו שומרים על מזהה הטיימר על ה-this.timerID
.
בעוד ש-this.props
מוגדר על-ידי React עצמה ול-this.state
יש משמעות מיוחדת, אתם רשאים להוסיף שדות נוספים באופן ידני למחלקה אם עליכם לאחסן דבר כלשהו שאינו חלק מזרם הנתונים (כמו מזהה הטיימר).
אנו נחסל את הטיימר במתודת מחזור החיים componentWillUnmount()
:
componentWillUnmount() {
clearInterval(this.timerID); }
לבסוף, נממש מתודה הנקראת tick()
שתקרא על ידי הקומפוננטה Clock
בכל שנייה.
היא תשתמש ב-this.setState()
כדי לתזמן עדכונים ל-state המקומי של הקומפוננטה:
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
);
}
componentWillUnmount() {
clearInterval(this.timerID);
}
tick() { this.setState({ date: new Date() }); }
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Clock />);
כעת השעון מתקתק בכל שניה.
בואו נסכם בזריזות את מה שקורה ואת הסדר שבו מתודות נקראות:
- כאשר
<Clock />
מועבר ל-ReactDOM.render()
, React קוראת לבנאי של הרכיבClock
. מכיוון ש-Clock
צריך להציג את השעה הנוכחית, הוא מאתחל אתthis.state
עם אובייקט שכולל את הזמן הנוכחי. בהמשך נעדכן את ה-state הזה. - אז React קוראת למתודת
render()
של קומפוננטת ה-Clock
. באופן זה React לומדת מה צריך להיות מוצג על המסך. אז React מעדכנת את ה-DOM כדי שיהיה תואם לפלט שרונדר על ידיClock
. - כאשר הפלט של
Clock
מוכנס ל-DOM, React קוראת למתודת מחזור החייםcomponentDidMount()
. בתוכה, קומפוננטתClock
מבקשת מהדפדפן להגדיר טיימר כדי לקרוא למתודתtick()
של הקומפוננטה פעם בשנייה. - בכל שנייה הדפדפן קורא למתודה
tick()
. בתוכה, קומפוננטתClock
מתזמנת את עדכון ממשק המשתמש על ידי קריאה ל-setState()
עם אובייקט המכיל את הזמן הנוכחי. הודות לקריאה ל-setState()
, React יודעת שה-state השתנה, וקוראת למתודתrender()
שוב כדי ללמוד מה צריך להיות על המסך. הפעם,this.state.date
במתודתrender()
יהיה שונה, ולכן הפלט המרונדר יכלול את הזמן המעודכן. React מעדכנת את ה-DOM בהתאם. - אם הקומפוננטה
Clock
מוסרת מה-DOM, React קוראת למתודת מחזור החייםcomponentWillUnmount()
כך שהטיימר יופסק.
שימוש נכון ב-State
ישנם שלושה דברים שעליכם לדעת לגבי setState()
.
אל תשנו את State ישירות
למשל, זה לא ירנדר מחדש קומפוננטה:
// טעות
this.state.comment = 'Hello';
במקום, השתמשו ב-setState()
:
// נכון
this.setState({comment: 'Hello'});
המקום היחידי שבו אתם כן יכולים לבצע השמה ל-this.state
הוא הבנאי.
עדכוני State יכולים להיות אסינכרוניים
React עשויה לקבץ מספר קריאות ל-setState()
לתוך עדכון יחיד על מנת לשפר ביצועים.
מכיוון ש-this.props
ו-this.state
עשויים להתעדכן באופן אסינכרוני, עליכם לא להסתמך על הערכים שלהם לצורך חישוב ה-state הבא.
למשל, קוד זה עלול להכשל בעדכון המונה:
// טעות
this.setState({
counter: this.state.counter + this.props.increment,
});
כדי לתקן זאת, השתמש בסוג השני של setState()
שמקבל פונקציה במקום אובייקט. פונקציה זו תקבל את ה-state הקודם כארגומנט הראשון, ואת ה-props בעת החלת העדכון כארגומנט השני:
// נכון
this.setState((state, props) => ({
counter: state.counter + props.increment
}));
אנו משתמשים בפונקציית חץ למעלה, אבל זה עובד גם עם פונקציות רגילות:
// נכון
this.setState(function(state, props) {
return {
counter: state.counter + props.increment
};
});
עדכוני State ממוזגים
כאשר אתם קוראים ל-setState()
, React ממזגת את האובייקט שאתם מספקים ל-state הנוכחי.
למשל, ה-state שלכם עלול להכיל מספר משתנים בלתי תלויים:
constructor(props) {
super(props);
this.state = {
posts: [], comments: [] };
}
לאחר מכן תוכלו לעדכן אותם באופן בלתי תלוי באמצעות קריאות setState()
נפרדות:
componentDidMount() {
fetchPosts().then(response => {
this.setState({
posts: response.posts });
});
fetchComments().then(response => {
this.setState({
comments: response.comments });
});
}
המיזוג הוא רדוד, ולכן this.setState({comments})
משאיר את this.state.posts
ללא שינוי, אך מחליף לחלוטין את this.state.comments
.
הנתונים זורמים למטה
לא קומפוננטות הורים ולא קומפוננטות ילדים יכולות לדעת אם קומפוננטה מסוימת בעלת state או ללא state, ולא צריך לשנות להן אם היא מוגדרת כפונקציה או כמחלקה.
זו הסיבה ש-state נקרא לעתים קרובות מקומי או מוכמס. הוא אינו נגיש לאף קומפוננטה אחרת פרט לזו שהוא בבעלותה ומגדירה אותו.
קומפוננטה יכולה לבחור להעביר את ה-state שלה למטה בתור props לקומפוננטות הילדים שלה:
<FormattedDate date={this.state.date} />
הקומפוננטה FormattedDate
תקבל את ה-date
ב-props שלה ולא תדע אם הוא בא מה-state של Clock
, מה-props של Clock
או שהוקלד ידנית:
function FormattedDate(props) {
return <h2>It is {props.date.toLocaleTimeString()}.</h2>;
}
זה נקרא בדרך כלל זרימת הנתונים “מלמעלה למטה” או “חד כיווניות”. כל state הוא תמיד בבעלות קומפוננטה מסוימת, וכל נתון או ממשק משתמש הנגזר מה-state הזה יוכלו להשפיע רק על קומפוננטות שנמצאות “מתחתיהם” בעץ.
אם אתם מדמיינים עץ קומפוננטות כמפל של props, כל state של קומפוננטה היא כמו מקור מים נוסף שמצטרף אליו בנקודה שרירותית אבל גם זורם כלפי מטה.
כדי להראות שכל הקומפוננטות מבודדות באמת, אנו יכולים ליצור קומפוננטת App
המרנדרת שלושה <Clock>
-ים:
function App() {
return (
<div>
<Clock /> <Clock /> <Clock /> </div>
);
}
כל Clock
מקים טיימר משלו ומתעדכן באופן עצמאי.
באפליקציות React, ההחלטה אם רכיב הוא בעל state או חסר state נחשבת לפריט מימוש של הקומפוננטה שעשוי להשתנות לאורך זמן. אתם יכולים להשתמש בקומפוננטות חסרות state בתוך קומפוננטות בעלות state, ולהיפך.