متغیرهای استیت (State) ممکن است شبیه به متغیرهای معمولی جاوااسکریپت به نظر برسند که میتوانید در آنها بنویسید یا مقدارشان را بخوانید. اما استیت در React بیشتر شبیه به یک اسنپشات (Snapshot) یا تصویر ثبتشده در لحظه رفتار میکند. تغییر دادن استیت، مقدار متغیری که در حال حاضر دارید را عوض نمیکند، بلکه رندر مجددی را پیریزی میکند.
چگونه تغییر استیت، رندر مجدد را کلید میزند؟
استیتها دقیقاً چه زمانی و چگونه بهروزرسانی میشوند؟
چرا استیت بلافاصله پس از مقداردهی جدید، بهروز نمیشود؟
نحوه دسترسی Event Handlerها به اسنپشات استیت.
شاید مدل ذهنی شما این باشد که رابط کاربری (UI) مستقیماً در پاسخ به رویدادهایی مثل کلیک کاربر تغییر میکند. اما در React همهچیز کمی متفاوت است. برای اینکه رابط کاربری به یک رویداد واکنش نشان دهد، شما باید استیت را آپدیت کنید تا تغییر رندر درخواست شود.
به این نمونه فرم نگاه کنید:
import { useState } from 'react';
export default function Form() {
const [isSent, setIsSent] = useState(false);
const [message, setMessage] = useState('سلام!');
if (isSent) {
return <h1>پیام شما در راه است!</h1>;
}
return (
<form onSubmit={(e) => {
e.preventDefault();
setIsSent(true); // درخواست رندر مجدد از React
}}>
<textarea value={message} onChange={e => setMessage(e.target.value)} />
<button type="submit">ارسال</button>
</form>
);
}
وقتی کاربر روی دکمه ارسال کلیک میکند:
۱. رویداد onSubmit اجرا میشود.
۲. تابع setIsSent(true) مقدار وضعیت را به true تغییر داده و یک رندر جدید را در صف قرار میدهد.
۳. فریمورک React کامپوننت را بر اساس مقدار جدید isSent دوباره رندر میکند و خروجی تغییر مییابد.
«رندر کردن» یعنی React تابع کامپوننت شما را صدا میزند. خروجی JSX که از این تابع بازمیگردد، دقیقاً مثل یک اسنپشات از UI در آن لحظه از زمان است. تمام پراپها، مدیریتکنندههای رویداد (Event Handlers) و متغیرهای محلی، همگی با استفاده از مقدار استیت در همان لحظه رندر محاسبه شدهاند.
وقتی React یک کامپوننت را مجدداً رندر میکند:
۱. تابع شما را دوباره فراخوانی میکند.
۲. تابع شما یک اسنپشات JSX جدید برمیگرداند.
۳. تغییرات ساختار داخلی بررسی شده و صفحه واقعی مرورگر متناسب با آن بهروز میشود.
استیت برعکس متغیرهای معمولی که پس از پایان اجرای تابع نابود میشوند، در خودِ React (خارج از تابع شما و انگار روی یک قفسه اختصاصی!) زنده میماند. وقتی کامپوننت صدا زده میشود، اسنپشاتی از استیتِ آن رندر به آن تحویل داده میشود.
فرض کنید کدی بنویسیم که با یکبار کلیک، تابع تغییر استیت را ۳ بار پشت سر هم صدا بزند تا به عدد شمارنده ۳ واحد اضافه کند:
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(number + 1);
setNumber(number + 1);
setNumber(number + 1);
}}>۳+</button>
</>
);
}
اگر روی این دکمه کلیک کنید، با کمال تعجب خواهید دید که شمارنده در هر کلیک فقط ۱ واحد بالا میرود!
چرا این اتفاق میافتد؟ تغییر دادن استیت، آن را فقط برای رندر بعدی عوض میکند. در طول رندر اول، مقدار number برابر 0 است. بنابراین در بلاکِ رویداد کلیک، مقدار number تا پایان اجرای خطوط، همچنان 0 باقی میماند:
خط اول: setNumber(number + 1) $\rightarrow$ مقدار number صفر است $\rightarrow$ setNumber(0 + 1). ریآکت آماده میشود که در رندر بعدی مقدار را 1 کند.
خط دوم: setNumber(number + 1) $\rightarrow$ مقدار number هنوز در این رندر صفر است $\rightarrow$ setNumber(0 + 1). ریآکت آماده میشود که در رندر بعدی مقدار را 1 کند.
خط سوم: setNumber(number + 1) $\rightarrow$ مقدار number همچنان صفر است $\rightarrow$ setNumber(0 + 1). ریآکت آماده میشود که در رندر بعدی مقدار را 1 کند.
با اینکه شما این تابع را ۳ بار صدا زدید، اما در این رندر خاص، مقدار شمارنده همیشه 0 بوده است، پس شما ۳ بار دستور setNumber(1) را صادر کردهاید!
برای درک راحتتر، هر زمان که میخواهید رفتار رویدادها را تحلیل کنید، مقدار فعلی استیت را به جای متغیر قرار دهید. در رندر اول، کد کلیک دکمه از نظر React اینگونه دیده میشود:
<button onClick={() => {
setNumber(0 + 1);
setNumber(0 + 1);
setNumber(0 + 1);
}}>۳+</button>
در رندر بعدی مقدار number برابر 1 خواهد شد، پس در رندر دوم کد کلیک به این شکل عمل میکند: setNumber(1 + 1) و عدد به ۲ تغییر خواهد کرد.
بیایید یک چالش دیگر را بررسی کنیم. حدس بزنید با کلیک روی دکمه زیر، دستور alert چه عددی را نمایش میدهد؟
const [number, setNumber] = useState(0);
// داخل دکمه:
onClick={() => {
setNumber(number + 5);
alert(number);
}}
با استفاده از روش جایگذاری، مشخص است که پیام عدد 0 را نشان میدهد؛ زیرا در این رندر مقدار number صفر است و کد به صورت setNumber(0 + 5) و alert(0) اجرا میشود.
حالا اگر یک تایمر (setTimeout) روی آلرت بگذاریم تا ۳ ثانیه دیگر (یعنی قطعاً بعد از اینکه رندر بعدی انجام شد و عدد روی صفحه ۵ شد) اجرا شود چطور؟ آیا عدد ۰ را نشان میدهد یا ۵؟
onClick={() => {
setNumber(number + 5);
setTimeout(() => {
alert(number);
}, 3000);
}}
پاسخ باز هم عدد 0 است! تعجب کردید؟
مقدار ذخیره شده در حافظه اصلی React پس از رندر تغییر کرده و ۵ شده است، اما این پندار که آلرت مقدار جدید را میبیند اشتباه است؛ زیرا این تابع بر اساس اسنپشاتی از استیت که در زمان تعامل کاربر وجود داشته، برنامهریزی شده است.
📌 قانون طلایی: مقدار یک متغیر استیت در طول یک رندر هرگز تغییر نمیکند، حتی اگر کدهای داخل Event Handler به صورت ناهمگام (Asynchronous) یا زماندار اجرا شوند. مقدار آن در لحظه گرفتن اسنپشات توسط React کاملاً ثابت و قفل شده است.
این رفتار ثابت و قفلشدگی استیت در طول یک رندر، یک مزیت بزرگ برای ایمنسازی کدهای شماست. فرم زیر را در نظر بگیرید که یک پیام را با تأخیر ۵ ثانیهای ارسال میکند:
import { useState } from 'react';
export default function Form() {
const [to, setTo] = useState('آلیس');
const [message, setMessage] = useState('سلام');
function handleSubmit(e) {
e.preventDefault();
setTimeout(() => {
alert(`پیام [${message}] به [${to}] ارسال شد.`);
}, 5000);
}
return (
<form onSubmit={handleSubmit}>
<select value={to} onChange={e => setTo(e.target.value)}>
<option value="Alice">آلیس</option>
<option value="Bob">باب</option>
</select>
<button type="submit">ارسال پیام</button>
</form>
);
}
یک سناریوی واقعی:
۱. شما دکمه ارسال را میزنید تا پیام "سلام" به «آلیس» فرستاده شود.
۲. پیش از اینکه تایمر ۵ ثانیهای تمام شود، نظر خود را عوض کرده و از منوی کشویی نام «باب» را انتخاب میکنید.
هنگامی که آلرت ظاهر میشود، فکر میکنید پیام به چه کسی ارسال شده است؟ بله، خروجی خواهد بود: پیام [سلام] به [آلیس] ارسال شد.
از آنجا که رویداد بر اساس اسنپشات زمان کلیک ساخته شده بود، تغییرات بعدی کاربر در المانهای صفحه، دیتای قفلشدهی آن رندر را خراب نمیکند. این ویژگی مانع از بروز باگهای پیچیده تعاملی در سیستمهای چت یا ثبت فرم میشود.
تنظیم و تغییر استیت، درخواست یک رندر جدید را در صف قرار میدهد.
React مقادیر استیت را خارج از کامپوننت شما (روی یک قفسه فرضی) نگهداری میکند.
با هر بار فراخوانی useState، تصویر یا اسنپشاتی از دیتای مخصوص همان رندر به شما داده میشود.
متغیرها و مدیریتکنندههای رویداد در بین رندرها زنده نمیمانند؛ هر رندر Event Handlerهای مستقل خود را دارد.
شما میتوانید استیتها را در ذهن خود با مقدار عددی/رشتهای واقعیشان در آن رندر جایگزین کنید تا رفتار کد را به درستی پیشبینی کنید.
رویدادهایی که در گذشته برنامهریزی شدهاند (مثل setTimeout) همیشه مقادیر استیت مربوط به همان رندری که در آن ساخته شدهاند را میبینند.
این محتوا کاملا رایگان توسط تیم کدلپر ترجمه شده و در اختیار شما کاربران عزیز قرار گرفته است، هر گونه کپی برداری برای مقاصد غیر رایگان و بدون ذکر منبع، مورد پیگیری قانونی قرار میگیرد.
ترجمه شده از منبع: https://react.dev/learn