همانطور که برای ساخت کامپوننت ها (creation) یک چرخه داریم (lifecycle)، برای ویرایش یا بروزرسانی آن ها نیز یک چرخه دیگر وجود دارد. در این چرخه اگر props یا state تغییر کنند react کامپوننت مربوطه را دوباره ارزیابی می کند بنابراین در صورت تغییر props یا state وارد چرخه زندگی جدیدی می شویم.
چرخه بروزرسانی کامپوننت ها با اجرای متد getDerivedStateFromProps شروع می شود که به ندرت از آن استفاده می شود. می توان از این متد برای initialize کردن state یک کامپوننت، بر اساس props های جدید دریافتی استفاده کرد. بنابراین استفاده اصلی آن برای همگام سازی state با props است اما به هیچ عنوان side-effect (مانند ارسال درخواست های HTTP) ایجاد نکنید.
getDerivedStateFromProps(props, state)
بعد از اجرای این متد، متد بعدی به نام shouldComponentUpdate اجرا خواهد شد. این متد به شما اجازه می دهد که فرآیند بروزرسانی را متوقف کنید! بنابراین در این قسمت می توانید به react بگویید که آیا کامپوننت را ارزیابی کرده و آن را بروزرسانی کند یا خیر. ممکن است بپرسید چرا باید چنین کاری انجام دهیم. سوالتان هم درست است؛ دلیل متوقف کردن فرآیند بروزرسانی معمولا به دلیل بهینه سازی عملکرد برنامه است که به طور مفصل در قسمت های بعدی در مورد آن صحبت خواهیم کرد.
shouldComponentUpdate (nextProps, nextState)
بعد از اجرای این متد، متد render صدا زده می شود. render کدهای JSX را گرفته و با آن virtual DOM خود را می سازد (بعدا در مورد آن صحبت می کنیم) سپس با مقایسه آن با DOM اصلی میگوید که آیا باید کامپوننت مربوطه بروزرسانی شود یا خیر. مثل همیشه، render روی تمام کامپوننت های فرزند نیز اعمال می شود.
Render()
حالا نوبت اجرای متد getSnapshotBeforeUpdate است. این متد props و state قبلی را می گیرد و یک شیء snapshot را برمی گرداند که می توانیم به راحتی آن را ویرایش کنیم. این متد نیز به ندرت استفاده می شود و معمولا برای عملیات های لحظه آخر است، مثلا دریافت موقعیت اسکرول کاربر. فرض کنید می خواهید عناصر جدیدی را در DOM ایجاد کنید اما می خواهید پس از render شدن این عناصر جدید، کاربر موقعیت فعلی خود را در صفحه حفظ کند. در این حالت می توان با استفاده از getSnapshotBeforeUpdate موقعیت اسکرول کاربر را ذخیره کرد، DOM را بروزرسانی کرده و در آخر کاربر را به موقعیت قبلی خود انتقال داد.
آخرین مرحله نیز مربوط به اجرای متد componentDidUpdate است. این متد اعلام می کند که کامپوننت ما بروزرسانی شده است و متد render به طور کامل اجرا شده است. در این قسمت می توانید side-effect ایجاد کنید اما به هیچ عنوان state را تغییر ندهید چرا که باعث re-render شدن های متعدد می شود.
بیایید این تغییرات را به صورت عملی مشاهده کنیم. برای این کار باید کامپوننت های Person و Persons را تبدیل به کامپوننت های کلاس محور کنیم تا بتوانیم Lifecycle hooks را در آن ها استفاده کنیم. ابتدا با Person.js شروع می کنیم:
import React, { Component } from 'react'; import classes from './Person.css'; class Person extends Component { render() { console.log('[Person.js rendering...'); return ( <div className={classes.Person}> <p onClick={this.props.click}>I'm {this.props.name} and I am {this.props.age} years old!</p> <p>{this.props.children}</p> <input type="text" onChange={this.props.changed} value={this.props.name} /> </div> ) } }; export default Person;
تبدیل کامپوننت کاربردی به کلاس-محور بسیار ساده است. باید ابتدا یک کلاس بسازیم که معمولا نام آن با حرف بزرگ شروع می شود. سپس تمامی props ها را به this.props تغییر دهیم و در آخر، هنگام export کردن کامپوننت، حواستان باشد که نام کامپوننت export شده را با حرف بزرگ بگذارید (البته اگر نام کلاس را با حرف بزرگ شروع کرده اید).
همین کار را برای Persons.js انجام می دهیم:
import React, { Component } from 'react'; import Person from './Person/Person'; class Persons extends Component { render() { console.log('[Persons.js] rendering...'); return this.props.persons.map((person, index) => { return ( <Person click={() => this.props.clicked(index)} name={person.name} age={person.age} key={person.id} changed={(event) => this.props.changed(event, person.id)} /> ); }); } } export default Persons;
حالا اگر به مرورگر بروید می بینید که هیچ مشکلی نیست و کدها مثل قبل کار می کنند.
حالا با همین فایل Persons.js شروع می کنیم و متدهای مختلف را بررسی می کنیم. ابتدا با getDerivedStateFromProps شروع می کنیم:
class Persons extends Component { static getDerivedStateFromProps (props, state) { return state; }
این متد باید state را برگرداند بنابراین قسمت return را باید بنویسیم (گرچه فعلا state ای نداریم و مقدار برگردانده شده یک شیء خالی خواهد بود). حالا برای اینکه تغییرات نمایان باشد می توانیم ازconsole.log استفاده کنیم:
static getDerivedStateFromProps(props, state) { console.log('[Persons.js] getDerivedStateFromProps'); return state; }
بعد از این متد، متد shouldComponentUpdate اجرا می شود. بنابراین پایین تر از متد getDerivedStateFromProps می گوییم:
shouldComponentUpdate(nextProps, nextState) { return true; }
این متد باید یکی از دو مقدار true یا false را برگرداند. حتما برایتان واضح است که هیچ وقت نباید آن را به شکل کد بالا و دستی بنویسیم بلکه باید مقدار prop) prop فعلی و قبل از بروزرسانی) را با nextProp (یعنی prop بعد از بروزرسانی) مقایسه کنید (مثلا در یک شرط if) و اگر مقادیر متفاوت بودند آنگاه true برگردانید تا کامپوننت دوباره render شود. فعلا برای سادگی بحث من آن را به شکل true باقی می گذاریم. باز هم برای واضح تر بودن عملیات آن را log می کنیم:
shouldComponentUpdate(nextProps, nextState) { console.log('[Persons.js] shouldComponentUpdate'); return true; }
پس از این متد، نوبت متد getSnapshotBeforeUpdate است:
getSnapshotBeforeUpdate() { console.log('[Persons.js] getSnapshotBeforeUpdate'); return null; }
از آنجایی که چیزی برای return کردن نداریم فعلا null را برگردانده ایم.
حالا نوبت اجرای render است که پایین تر آماده است و از قبل log را هم برایش نوشته ایم:
render() { console.log('[Persons.js] rendering...'); return this.props.persons.map((person, index) => { return ( <Person click={() => this.props.clicked(index)} name={person.name} age={person.age} key={person.id} changed={(event) => this.props.changed(event, person.id)} /> ); }); }
پس از اجرا شدن render روی تک تک Person ها و اجرا شدن چرخه بروزرسانی (Update Lifecycle) برای تک تک این Person ها نوبت متد componentDidUpdate است:
componentDidUpdate() { console.log('[Persons.js componentDidUpdate'); }
با اینکه این متد را بالای متد render نوشته ایم اما تا زمانی که render کاملا تمام نشود این متد اجرا نخواهد شد (در قسمت Console خواهیم دید). به قسمت console در dev tools مرورگر خود بروید و مشاهده می کنید که تمامی این log ها در آنجا ثبت شده اند (به ترتیب):
خطایی که در console مشاهده می کنید مربوط به این کد است:
static getDerivedStateFromProps(props, state) { console.log('[Persons.js] getDerivedStateFromProps'); return state; }
ما در اینجا state را برگردانده ایم اما اصلا state ای درون این کامپوننت نداریم و مقدار برگشتی تنها یک شیء خالی است. React در این خطا به شما می گوید اگر state شما خالی است بهتر است از این lifecycle hook (یعنی همین متد getDerivedStateFromProps) استفاده نکنید. کد ما به طور صحیح کار می کند و مشکلی نیست اما برای اصولی تر بودن کار این متد را کامنت می کنیم.
سوال: دستور log مربوط به componentDidUpdate کجا است؟ چرا ما آن را در console مشاهده نمی کنیم!؟
پاسخ: آن را مشاهده نمی کنید چرا که هنوز وارد چرخه Update یا بروزرسانی کامپوننت هایمان نشده ایم. متد getDerivedStateFromProps اجرا شد چرا که بخشی از چرخه ایجاد (creation) نیز می باشد اما هنوز وارد چرخه update نشده ایم. برای وارد شدن به این چرخه باید کامپوننتی تغییر کند (به طور مثال در یکی از input ها چیزی تایپ کنم تا نام فرد نیز بروزرسانی شود).
به فایل Person.js نگاه کنید:
render() { console.log('[Person.js rendering...'); return ( <div className={classes.Person}> <p onClick={this.props.click}>I'm {this.props.name} and I am {this.props.age} years old!</p> <p>{this.props.children}</p> <input type="text" onChange={this.props.changed} value={this.props.name} /> </div> ) }
زمانی که چیزی تایپ شود، رویداد onChange نیز فعال می شود. onChange باعث می شود متد changed (در فایل Persons.js) به شکل زیر اجرا شود:
changed={(event) => this.props.changed(event, person.id)} />
سپس این دستور به فایل App.js می رود:
persons = <Persons persons={this.state.persons} clicked={this.deletePersonHandler} changed={this.nameChangedHandler} />
که nameChangedHandler را صدا می زند. nameChangedHandler نیز persons را تغییر می دهد که دوباره به عنوان prop به Persons.js فرستاده می شود. حالا چرخه بروزرسانی (update) در این کامپوننت شروع می شود:
بهتر است توضیحات بیشتری برای snapshot بدهم. Snapshot یک پکیج داده است که به componentDidUpdate پاس داده می شود. برای تست می توانیم بگوییم:
getSnapshotBeforeUpdate() { console.log('[Persons.js] getSnapshotBeforeUpdate'); return { message: 'snapshot!' }; } componentDidUpdate(prevProps, prevState, snapshot) { console.log('[Persons.js componentDidUpdate'); console.log(snapshot); }
در این کد یک شیء ساده را برای تست برگردانده ام که حاوی پیام snapshot است. متد componentDidUpdate نیز سه پارامتر می گیرد: props قبل از بروزرسانی، state قبل از بروزرسانی (در صورتی که بخواهید کاری با این دو انجام دهید) و در آخر هم مقدار برگشتی از متد getSnapshotBeforeUpdate را می گیرد. ما هم آن را در console نمایش داده ایم:
نکته: ممکن است در بعضی از سایت ها یا پروژه های قدیمی متدهای componentWillReceiveProps و componentWillUpdate را ببینید. این ها جزء همان lifecycle hook هایی هستند که قرار است در آینده حذف شوند و شما نباید از آن استفاده کنید.
امیدوارم به طور کامل با چرخه بروزرسانی کامپوننت ها آشنا شده باشید.
در این قسمت، به پرسشهای تخصصی شما دربارهی محتوای مقاله پاسخ داده نمیشود. سوالات خود را اینجا بپرسید.