state و lifecycle
این صفحه مفهوم state و lifecycle را در یک کامپوننت ریاکتی معرفی میکند. برای مطالعه مرجع API کامپوننت با جزئیات به اینجا مراجعه کنید.
مثال تیک تاک ساعت از یکی از بخشهای قبلی را در نظر بگیرید. در رندرکردن المنتها، ما فقط یک راه برای به روز رسانی رابط کاربری یاد گرفته ایم. برای تغییر خروجی رندر شده root.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
یک تایمر را تنظیم و هر ثانیه UI را بهروز رسانی کند، که باید بخشی از جزئیات پیادهسازی خود Clock
باشد.
ایدهآل این است که ما یک بار Clock
را بنویسیم و خودش بهروز رسانی را انجام دهد:
root.render(<Clock />);
برای پیادهسازی آن، باید به کامپوننت Clock
state اضافه کنیم.
state مشابه props است، اما خصوصی است و کاملا توسط کامپوننت کنترل میشود.
تبدیل یک تابع به یک کلاس
شما میتوانید یک کامپوننت برپایه تابع را در پنج مرحله به یک کلاس تبدیل کنید:
- یک کلاس ES6 بسازید، با همان نام که از
React.Component
ارث میبرد. - یک متد خالی با نام
render()
به آن اضافه کنید. - بدنه تابع را به متد
render()
منتقل کنید. - در بدنه
render()
بهجایprops
،this.props
بنویسید. - آنچه از تعریف تابع خالی باقی ماندهاست را پاک کنید.
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 node رندر میکنیم، تنها یک نمونه از کلاس Clock
استفاده خواهدشد. این باعث میشود ما قادر به استفاده از ویژگیهای دیگری مانند state و توابع lifecycle باشیم.
اضافهکردن state داخلی به یک کلاس
ما در سه مرحله، date
را از props به state انتقال میدهیم:
- در تابع
render()
،this.props.date
را باthis.state.date
جایگزین کنید:
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
فراخوانی کنند.
date
را از props المنت<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
را طوری تغییر میدهیم که تایمر خودش را داشتهباشد و خودش هر ثانیه بهروز رسانی شود.
افزودن توابع lifecycle به یک کلاس
در برنامههایی با تعداد کامپوننت زیاد، بسیار اهمیت دارد که با از بین رفتن کامپوننت، منابعی که توسط آن اشغال شدهبود نیز آزاد شود.
ما می خواهیم یک تایمر را برای اولین دفعهای که Clock
در DOM رندر میشود، تنظیم کنیم. در ریاکت به آن “mounting” گفته میشود.
همچنین میخواهیم هر زمان که DOM تولید شده توسط Clock
حذف میشود، تایمر را پاک کنیم. در ریاکت به آن “unmounting” گفته میشود.
برای اجرای کدهایی زمان 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>
);
}
}
به آنها “توابع lifecycle” گفته میشود.
تابع componentDidMount()
پس از رندر شدن خروجی کامپوننت توی DOM، اجرا میشود. اینجا محل خوبی برای تنظیم یک تایمر است:
componentDidMount() {
this.timerID = setInterval( () => this.tick(), 1000 ); }
دقت داشتهباشید که چگونه ما شناسه تایمر را دقیقا روی this
(this.timerID
) ذخیره میکنیم.
در حالی که this.props
توسط خود ریاکت تنظیم میشود و this.state
کاربرد خاص خودش را دارد، شما آزاد هستید که برای ذخیره چیزی که نقشی در جریان دادهها ندارد (مانند شناسه تایمر)، به صورت دستی فیلدهای دیگری به کلاس اضافه کنید.
ما تایمر را در تابع lifecycle 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 />
بهroot.render()
منتقل می شود، React سازنده کامپوننتClock
را فراخوانی می کند. از آنجایی کهClock
باید زمان جاری را نمایش دهد،this.state
را با یک آبجکت شامل زمان فعلی مقداردهی اولیه می کند. ما بعداً این حالت (state) را به روز خواهیم کرد. - سپس ریاکت تابع
render()
کامپوننتClock
را فرا میخواند. این روشی است که ریاکت میفهمد چه چیزی باید روی صفجه نمایشداده شود. سپس ریاکت DOM را بهروز رسانی و با خروجی رندرClock
تطبیق میدهد. - زمانی که خروجی
Clock
به DOM اضافه میشود، ریاکت تابع lifecyclecomponentDidMount()
را فراخوانی میکند. درون آن، کامپوننتClock
از مرورگر میخواهد که یک تایمر تنظیم کند که هر ثانیه تابعtick()
را فراخوانی کند. - مرورگر هر ثانیه تابع
tick()
را فرا میخواند. درون آن، کامپوننتClock
بهروز رسانی UI را با فراخوانی تابعsetState()
همراه یک شیء شامل زمان جاری، زمانبندی میکند. ریاکت با کمکsetState()
متوجه میشود که state تغییر کردهاست. و آنگاه برای اطلاع از آنچه باید روی صفحه نمایش دادهشود، تابعrender()
را فرا میخواند. این دفعه، مقدارthis.state.date
در تابعrender()
متفاوت خواهد بود و بنابراین خروجی رندر دارای زمان بهروز رسانی شده خواهدبود. ریاکت DOM را بر همین اساس بهروز رسانی میکند. - اگر کامپوننت
Clock
از DOM حذف شود، ریاکت هم تابع lifecyclecomponentWillUnmount()
را فرا میخواند و در نتیجه تایمر متوقف میشود.
استفاده صحیح از state
سه چیز هست که باید درباره setState()
بدانید:
مستقیم state را تغییر ندهید
برای مثال، این کار باعث رندر مجدد یک کامپوننت نمیشود:
// اشتباه
this.state.comment = 'Hello';
به جای آن از setState()
استفاده کنید:
// درست
this.setState({comment: 'Hello'});
تنها جایی که شما میتوانید this.state
را [مستقیم] مقداردهی کنید، سازنده [کلاس] است.
بهروز رسانی state ممکن است غیرهمزمان باشد
ممکن است ریاکت برای بهبود عملکرد، فراخوانی چند باره setState()
را در یک بهروز رسانی انجام دهد.
از آنجا که ممکن است this.props
و this.state
به صورت غیرهمزمان بهروز رسانی شوند، برای محاسبه وضعیت بعدی نباید روی مقادیر آنها حساب کنید.
برای مثال، این کد ممکن است در بهروز رسانی شمارنده دچار اشکال شود:
// اشتباه
this.setState({
counter: this.state.counter + this.props.increment,
});
برای حل آن، شکل دوم تابع setState()
را استفاده کنید که به جای یک شیء، یک تابع به عنوان ورودی میپذیرد. آن تابع هم state قبلی را به عنوان ورودی اول، و props مربوط به زمانی که تعییرات اعمال شدهاست را به عنوان ورودی دوم دریافت میکند.
// درست
this.setState((state, props) => ({
counter: state.counter + props.increment
}));
در بالا ما از تابع arrow استفاده کردهایم، اما با تابع معمولی هم کار میکند:
// درست
this.setState(function(state, props) {
return {
counter: state.counter + props.increment
};
});
بهروز رسانیهای state ادغامشده هستند
وقتی شما setState()
را فرا میخوانید، ریاکت آن شیء را در 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 خود را به عنوان props به کامپوننتهای فرزند انتقال دهد.
<FormattedDate date={this.state.date} />
کامپوننت FormattedDate
از props خود date
را دریافت میکند و نخواهد فهمید که از state یا props Clock
به دریافت کرده، یا با دست تایپ شدهاست.
function FormattedDate(props) {
return <h2>It is {props.date.toLocaleTimeString()}.</h2>;
}
به این جریان داده عموما “یک طرفه” یا “بالا به پایین” گفته میشود. مالکیت هر state در دست یک کامپوننت مشخص است و هر داده یا UI که از آن state مشتق شدهباشد، فقط کامپوننتهای زیرین خود را در ساختار درختی تحت تاثیر قرار میدهد.
اگر یک درخت کامپوننت را به شکل آبشاری از props ها تصور کنید، state هر کامپوننت مانند منبعی اضافه از آب است که در نقطهای دلخواه به آن متصل و به پایین جریان پیدا میکند.
برای نشاندادن اینکه تمام کامپوننتها واقعا ایزوله هستند، ما میتوانیم یک کامپوننت App
بسازیم که سه کامپوننت <Clock>
را رندر میکند.
function App() {
return (
<div>
<Clock /> <Clock /> <Clock /> </div>
);
}
هر Clock
تایمر خودش را تنظیم و به طور مستقل بهروز رسانی میکند.
در برنامههای ریاکتی، اینکه یک کامپوننت دارای state هست یا نه، به جزئیات طراحی آن کامپوننت مربوط میشود که ممکن است در طول زمان تغییر کند. شما میتوانید کامپوننتهای بدون state را درون کامپوننت های دارای state استفاده کنید و همچنین برعکس.