برای ایجاد اکثر برنامه های کاربردی پیچیده توسط جاوا اسکریپت حتما نیاز به state پیدا خواهید کرد بسیاری از توسعه دهندان مبتدی و حتی کسانی که سالها از جاوا اسکریپت استفاده میکنند با متد stat آشنا نیستند. اما ببینیم stste چیست؟
state چیست؟
گر با React یا Vue آشنا باشید، میدانید که state مفهومی ضروری برای کار با این کتابخانه های جاوا اسکریپتی است. بسیاری از توسعه دهندگان جاوا اسکریپت تازه پس از آشنایی با این کتابخانه ها با مفهوم state آشنا میشوند و سعی میکنند کارکرد و کاربردهای اون رو درک کنند. این روش معکوسه!
State تقزیبا در هر برنامه تحت وب کاربرد داره و با خود جاوا اسکریپ هم میشه اون رو پیداه سازی کرد. State یک مفهوم یا کلاس خاصی نیست که فقط کتابخانه های جاوا اسکریپت از آن استفاده میکنند بلکه یک متد برنامه نویسی است که باید آن را درک کنیم.
یک برنامه با تمام پیچیدگیهایش تنها با داده هاست که تغییر میکند. بنابراین می توان گفت حالت یک برنامه را داده هایش تعیین میکند. State نیز برای نگه داری داده هایی که در طول اجرای برنامه تغییر میکنند و مدیریت آنها ایجاد می شود. برای درک بهتر بیایید یک برنامه ایجاد کنیم و ببینیم چگونه در تعاول با کاربر تغییر میکند.
قصد داریم برنامه ای مانند تصویر فوق ایجاد کنیم که با توجه به وضعیت کاربر ( کاربری که به برنامه ورود کرده یا کاربر مهمان) پیامی برای کاربر نمایش می دهد. اگر از شما بپرشم “وضعیت این برنامه چیست؟” ( ? What is this application’s state) قطع به یقیین پاسخ میدهید ” بستگی به وضعیت کاربر دارد” بنابراین نیاز داریم وضعیت کاربر را رصد کنیم. پس وضعیت کاربر (داده ای که ورود کردن یا نکردن کاربر به برنامه را نگه داری میکند) قسمتی از state این برنامه است. اگر به تصویر دقیقتر نگاه کنید با توجه به نوع کاربر متن و رنگ پیام متفاوت است. پس رنگ و متن پیام نیز بخشی دیگر از state برنامه ما است.
چالش اصلی، درک مفهوم State است
وضعیت کاربر، متن پیام و رنگ آن داده های کوچکی است که وضعیت برنامه ما را مشخص می کند. البته که این همه داده برنامه ما نیست. این یک برنامه خیلی کوچک برای درک مفهوم state و آموختن آن است. از ساده ترین برنامه مانند برنامه toDo گرفته تا نرم افزارهای پیچیده ای مانند twitter، وقتی کاربر با یک برنامه تعامل میکند داده های به دست آمده از این تعاملات وضعیت (حالت) برنامه را تعیین میکند. بنابراین state در هر برنامه ای وجود دارد اما درک آن کمی برای مبتدی ها سخت است. اما وقتی با آن آشنا بشیم و نمونه هایی از آن ببینیم متوجه می شویم که در تمام برنامه ها وجود دارد.
State در اصل تغییرات داده در گذر زمان است
شاید این سوال برای شما پیش آمده که اگر state (وضعیت) چیزی جز داده نیست چرا برای آن واژه درست کردیم و به آن state میگوییم چرا از همان واژه “داده” استفاده نمیکنیم؟ (بحث خیلی فلسفی شد!) داده ( یا همان دیتا) خرده اطلاعاتی است که بر اساس آن نرم افزار تغییراتی را ایجاد میکند و براساس آن یک خروجی مشخص تولید و در اختیار کاربر می گذارد. اما میتوانیم به تغییرات داده در طول زمان اجرای برنامه هم از لفظ “داده” استفاده کنیم؟ به عنوان مثال فرض کنید کاربری با ایمیل و رمز ورود وارد برنامه می شود داده هایی که کاربر به برنامه داده نام کاربری و رمز ورد است اما پس ورود کاربر داده ای که وضعیت ورود کاربر را چک میکرده تغییر کرده و از حالت نا درست به درست تغییر کرده به این معنا که داده ای فرضی با نام “isAuth ” از حالت “false” به حالت “true” تغییر کرده است و چون مقدار isAuth بر وضعیت برنامه اثر میگذارد به آن state میگوییم. برای درک بهتر state را به عنوتان یک فریم از یک فیلم (نرم افزار) در نظر بگیرید.
فایده مدیریت state (حالت برنامه) چیست؟
حالا که متوجه شدیم state چیست(؟) سوال این است که چگونه آن را مدیریت کنیم یا بهتر است بپرسیم اصلا چرا باید آن را مدیریت کنیم؟
یکی از مزیت های مدیریت state این است که می توانیم داده های نامریی برنامه (مانند isAuth در مثال بالا) را به صورت مشخص در یک ساختارداده ای نگه داری و در هز زمان آن را فراخوانی(get State) یا به روز رسانی(set State) کنیم. اجازه دهید با یک مثال درک کنیم که چه تفاوتی بین حالتی که فقط state وجود دارد (مانند آنچه تا اینجا گفتیم) و حالتی که بتوان آن (state) را مدیریت کرد، وجود دارد. بیایید به کد برنامه ای که عملکرد آن را در تصویر ابتدای مقاله دیدیم، نگاهی بیاندازیم:
<!DOCTYPE html>
<html>
<head>
<title>Before State Management</title>
</head>
<body>
<div id="root"></div>
<script>
class App {
constructor() {
this.render();
// we use '$' to indicate that 'this.$userMessage' references an HTML element, not a normal JS value
this.$userMessage = document.getElementById("user-message");
this.$authStatus = document.getElementById('auth-status');
this.$authStatus.addEventListener('change', event => {
this.checkAuth(event.target.value);
});
}
checkAuth(status) {
if (status === 'auth') {
this.$userMessage.textContent = "Welcome back!";
} else if (status === "unauth") {
this.$userMessage.textContent = "You must sign in!";
this.$userMessage.style.color = "red";
}
}
render() {
document.getElementById("root").innerHTML = `
<select id="auth-status">
<option value="" selected>Choose your status</option>
<option value="auth">Signed In!</option>
<option value="unauth">Guest</option>
</select>
<div>
<span id="user-message"></span>
</div>
`;
}
}
new App();
</script>
</body>
</html>
اگر قستمهایی از کد را متوجه نمیشوید مهم نیست. فقط بدانید هدف از این برنامه این است که بسته به وضعیت کاربر، پیامی را با رنگ مشخص به آن نشان بدهیم. در این برنامه صفحه ما توسط جاوا اسکریپت مدیریت می شود؛ شبیه برنامه های تک صفحه ای که با کتابخانه هایی مانند React یا Vue تولید میشوند، یک روش رندر DOM و تولید HTML داریم. در این مثال ما کدی برای مدیریت State نداریم. آیا اگر توسعه دهنده دیگری به کد ما نگاه کند متوجه چگونگی کارکرد آن می شود. برنامه ما قرار است پیام خطایی را با توجه به خروجی متد checkAuth نمایش بدهد. اما آیا ما در متد خود صریحا به ایجاد خطا اشاره کرده ایم؟ مدیریت حالت تا حد زیادی داشتن وضوح در کد نویسی است به گونه ای که آنچه انجام می دهد کاملا مشخص باشد. وقتی قرار است به تنهایی روی پروژه ای کارکنیم روش کد زنی ما اهمیت ندارد. اما در یک کار تیمی چطور؟ به هر حال ما نیاز داریم تا با سایر توسعه دهندگان در ارتباط باشیم و به گونه ای کد بزنیم که دیگران بدون توضیحات اضافی متوجه بخشهای مختلف آن بشوند. از طرفی در کد بالا ما جایی برای بررسی state برنامه ایجاد نکردیم. یک توسعه دهنده دیگر باید تمام کد ما را بررسی کند تا state های برنامه را متوجه شود بخشی از وضعیت برنامه ما در توابع جاوا اسکریپت و برخی در میان کدهای HTML است. این باعث گمراهی توسعه دهندگان دیگر می شود. پس بیایید این کد را بازنویسی کنیم تا وضعیت، و مقادیری که باید آنها را رصد کنیم در مکانی واضح برای هر توسعه دهندهی دیگر در دسترس قرار گیرند. بهتر است آن را در یک مکان متمرکز تر قرار دهیم، نه به صورت پر و پخش در همه جای کد مان.
State به عنوان تنها منبع امن برای نگه داری داده ها
قبل از اینکه چیزی را بازنویسی کنید اجازه بدهید ایده ای که به ما کمک میکند state برنامه خود را در هر لحظه پایش کنیم را مطرح کنم. state را به صورت منبعی قابل اطمینان برای درک وضعیت برنامه در نظر بگیرید. بنابراین هر موقع بخواهیم وضعیت برنامه را بررسی کنیم می دانیم باید به کدام قسمت کد نگاه کنیم. یکی از راه های محبوب برای مدیریت وضعیت در بسیاری از کتابخانه های جاوا اسکریپت (مانند React،Redux، Vue و Vuex) استفاده از اشیاء است. بنابراین ما هم برای ایجاد آن از یک Object استفاده میکنیم و آن در بالاترین قسمت constructor کلاس مان تعریف میکنیم:
constructor() {
this.state = {};
}
هر توسعه دهنده ای که بخواهد بداند مقادیر کلاس ما چیست و کلاس ما با چه مقادیری سر و کا دارد به اینجا مراجعه میکند. در داخل state دو متغیر داریم؛ isAuth که وضعیت کاربر را مشخص می کند و error که متن خطا را نگه می دارد.
constructor() {
this.state = {
isAuth: false,
error: ""
};
}
بروز رسانی تغییرناپذیر
همانطور که قبلا گفتیم state علاوه برا تعیین وضعیت برنامه، به عنوان منبعی امن برای نگه داری اطلاعات تلقی می شود. بنا براین ویرایش و تغییر در داده ها نباید مستقیما انجام شود. یعنی قبل از هر گونه تغییر در هر یک از داده ها، یک کپی از وضعیت قبلی داشته باشیم پس به جای اینکه کدمان را به این صورت بنویسیم:
if (status === "auth") {
this.state.isAuth = true;
}
داده های قبلی را کلون میکنیم تا اولا مطمئن شویم وضعیت جدید به داده ها قبلی به داده های قبلی ارجاع داده نمی شود و دوما بتوانیم در صورت نیاز داده های جدید را با داده های قبلی مقایسه کنیم. برای این منظور با کمک عملگر spread (…) یک نمونه از شی (Object) state کپی گرفته و فقط مقادیری که تغییر میکنند را به روز رسانی میکنیم.
if (status === "auth") {
this.state = { ...this.state, error: "", isAuth: true };
}
برای متن پیام خطا هم به همین صورت عمل میکنیم؛
else if (status === 'unauth') {
this.state = { ...this.state, isAuth: false error: "You must sign in!" };
}
ممکنه به این نتیجه برسید که استفاده از متد spread برای نگه داری state لازم نیست اما توجه داشته باشید این یک برنامه فرضی کوچک است. در برنامه های واقعی تعداد داده های داخل state به مراتب بیشتر است و لازم نیست در هر زمان تمام آنها را به روز کرد.
اکنون کد ما به راحتی متن پیام خطا را مدیریت میکند و لازم نیست با تغییر وضعیت تابع checkAuth برای تغییر متن پیام خطا، DOM را زیرورو کنیم!
صفحه چطوری رندر میشه؟ در این روش state تعیین کننده UI نرم افزار ماست. به این معنی که در پاسخ به بروزرساین state نرم افزار باید DOM را رندر کند بنابراین باید this.render() را پس از بروزرسانی State فراخوانی کنیم:
constructor() {
this.state = {
isAuth: false,
error: ''
};
this.$authStatus = document.getElementById('auth-status');
this
.$authStatus
.addEventListener('change', event => {
// update state with checkAuth...
this.checkAuth(event.target.value);
// ...then render app to display new state
this.render();
});
}
به این ترتیب اگر this.state.isAuth درست باشد (اگر کاربر وجود داشته باشد ) پیام خوش آمد گویی را به نشانه موفقیت نشان می دهیم، اما اگر کاربر ورود نکرده باشد(به صورت مهمان وارد شده باشد) خطایی با پیام مربوطه به آن نشان می دهیم :
render() {
...
document.getElementById("root").innerHTML = `
<div>
${this.state.isAuth ? "Welcome back!" : this.state.error}
</div>
`;
}
حتی می توانیم با نسبت دادن یک مثدار ثابت به this.state کد را خوانا تر کنیم:
render() {
const { isAuth, error } = this.state;
...
document.getElementById("root").innerHTML = `
<div>
${isAuth ? "Welcome back!" : error}
</div>
`;
}
و در نهایت با استفاده از عملگر (&&) و استایل دهی داخلی به تگ نگهداری متن خطا، میتوانید رنگ متن را در صورت وجو خطا تعیین کنید:
render() {
const { isAuth, error } = this.state;
...
document.getElementById("root").innerHTML = `
<div style="color: ${error && "red"}">
${isAuth ? "Welcome back!" : error}
</div>
`;
}
و این نتیجه نهایی کد ماست با متد مدیریت state :
<!DOCTYPE html>
<html>
<head>
<title>After State Management</title>
</head>
<body>
<select id="auth-status">
<option value="" selected>Choose your status</option>
<option value="auth">Signed In!</option>
<option value="unauth">Guest</option>
</select>
<div id="root"></div>
<script>
class App {
constructor() {
this.state = {
isAuth: false,
error: ''
};
this.$authStatus = document.getElementById('auth-status');
this
.$authStatus
.addEventListener('change', event => {
this.checkAuth(event.target.value);
this.render();
});
}
checkAuth(status) {
if (status === 'auth') {
this.state = {
...this.state,
error: '',
isAuth: true
};
} else if (status === 'unauth') {
this.state = {
...this.state,
isAuth: false,
error: "You must sign in!"
};
}
}
render() {
const {isAuth, error} = this.state;
document
.getElementById("root")
.innerHTML = `
<div style="color: ${error && "red"}">
${isAuth ? "Welcome back!" : error}
</div>
`;
}
}
new App();
</script>
</body>
</html>
اکنون می توانید خودتان مقایسه کنید کدام کد خوانا تر و قابل فهم تر است. شاید کمی کد ما طولانی تر شده باشد. ام خوانایی و نگه داری آن بیشتر می شود.