در ریاکت، وضعیت یا همان استیت (State) کاملاً بین کامپوننتها ایزوله است. ریآکت بر اساس موقعیت هر کامپوننت در درخت واجهه کاربری (UI Tree) متوجه میشود که کدام استیت متعلق به کدام کامپوننت است. شما میتوانید به طور کامل کنترل کنید که چه زمانی این استیتها حفظ (Preserve) و چه زمانی بین رندرها کاملاً پاکسازی و ریست (Reset) شوند.
ریآکت چه زمانی تصمیم به حفظ یا حذف استیت میگیرد؟
چطور ریآکت را مجبور کنیم استیت یک کامپوننت را کاملاً ریست کند؟
نقش کلیدها (keys) و نوع کامپوننتها (types) در ماندگاری استیت چیست؟
شاید تصور کنید استیت در داخل خود کامپوننت زندگی میکند، اما این یک تصور اشتباه است! استیت در واقع درون هسته ریآکت نگهداری میشود. ریآکت بر اساس آدرس و موقعیتی (Position) که کامپوننت در درخت رندر دارد، استیت را به آن اختصاص میدهد.
اگر دو کامپوننت <Counter /> را در کنار هم رندر کنید، ریآکت دو نسخه کاملاً مجزا و ایزوله از استیتهای score و hover را برای هر کدام ایجاد میکند؛ چون هر کدام آدرس مختص به خود را در درخت دارند:
📌 قانون طلایی ریآکت: ریآکت استیت یک کامپوننت را تا زمانی که همان کامپوننت در همان موقعیت از درخت UI رندر شود، زنده نگه میدارد. به محض اینکه کامپوننت از آن موقعیت حذف شود، استیت آن نیز برای همیشه نابود خواهد شد.
به این تکه کد نگاه کنید:
export default function App() {
const [isFancy, setIsFancy] = useState(false);
return (
<div>
{isFancy ? <Counter isFancy={true} /> : <Counter isFancy={false} />}
<input type="checkbox" checked={isFancy} onChange={e => setIsFancy(e.target.checked)} />
</div>
);
}
وقتی چکباکس را فعال یا غیرفعال میکنید، استیت کامپوننت <Counter /> (یعنی امتیاز ثبت شده) ریست نمیشود! چرا؟ چون از دیدگاه ریآکت، چه پروپ isFancy مقدار true داشته باشد و چه false، در هر دو حالت اولین فرزندِ تگ <div> یک کامپوننت از نوع Counter است. آدرس در ساختار درخت تغییر نکرده، پس استیت حفظ میشود.
اگر در یک موقعیت ثابت از درخت، نوع کامپوننت را تغییر دهید، ریآکت تمام استیت آن درختواره را نابود میکند. به عنوان مثال، تغییر <Counter> به تگ <p>:
<div>
{isPaused ? <p>بعداً میبینمت!</p> : <Counter />}
</div>
با تغییر وضعیت، <Counter> حذف شده، استیت آن کاملاً پاک میشود و تگ <p> جای آن را میگیرد. اگر دوباره به حالت قبل برگردید، کامپوننت Counter از صفر و با استیت اولیه (امتیاز ۰) مقداردهی میشود.
حتی اگر نوع کامپوننت پدر تغییر کند، کل فرزندان زیرمجموعه آن ریست میشوند:
// تغییر از div به section باعث ریست شدن استیت Counter میشود!
<div>
{isFancy ? (
<div><Counter /></div>
) : (
<section><Counter /></section>
)}
</div>
⚠️ یک تله بزرگ (Nested Definitions): هرگز توابع کامپوننتها را داخل یکدیگر تعریف نکنید!
function MyComponent() { // ❌ اشتباه بزرگ: تعریف کامپوننت درون کامپوننت دیگر function MyTextField() { ... } }با این کار، در هر بار رندر شدن
MyComponent، یک تابع کامپوننت کاملاً جدید پدید میآید. ریآکت در هر رندر نوع متفاوتی را در آن موقعیت تشخیص داده و تمام استیتهای ورودی شما (مثل متون داخل اینپوت) با هر لود به طور آزاردهندهای پاک خواهند شد. همیشه کامپوننتها را در لایه Top-level تعریف کنید.
گاهی اوقات کامپوننت در یک موقعیت ثابت باقی میماند، اما شما عمداً میخواهید استیت آن ریست شود. برای مثال، ساخت یک سیستم امتیازدهی که با زدن دکمه "بازیکن بعدی"، امتیاز باید صفر شود اما کامپوننت همان <Counter /> قبلی است:
میتوانید با تکیه بر عبارات شرطی، کاری کنید پنلها آدرسهای متفاوتی بگیرند:
<div>
{isPlayerA && <Counter person="سارا" />}
{!isPlayerA && <Counter person="نیما" />}
</div>
key برای تغییر هویت (بهترین راهکار)کلیدها (keys) فقط مخصوص رندر کردن لیستها (.map) نیستند! شما میتوانید به کمک پروپ key به ریآکت بفهمانید که این کامپوننتها فراتر از یک آدرس عددی، هویت منحصربهفردی دارند.
وقتی به یک کامپوننت کلید اختصاص میدهید، ریآکت مقدار کلید را به عنوان بخشی از موقعیت (Address) آن در نظر میگیرد:
{isPlayerA ? (
<Counter key="sara" person="سارا" />
) : (
<Counter key="nima" person="نیما" />
)}
حالا با سوییچ بین سارا و نیما، با اینکه کامپوننت در یک خط از کد JSX قرار دارد، ریآکت متوجه میشود که این دو، دو هویت کاملاً مجزا هستند. در نتیجه کامپوننت قبلی را کاملاً منهدم کرده و کامپوننت جدید را با استیت ریستشده بازسازی میکند.
keyدر برنامههای پیامرسان، وقتی در حال تایپ پیام برای "آلیس" هستید و ناگهان روی مخاطب "باب" کلیک میکنید، اصلاً دوست ندارید متنی که برای آلیس تایپ کرده بودید در چتباکس باب باقی بماند! برای حل این مشکل، کافیست شناسه مخاطب را به عنوان key به کامپوننت چت بدهید:
export default function Messenger() {
const [to, setTo] = useState(contacts[0]);
return (
<div className="messenger">
<ContactList contacts={contacts} onSelect={contact => setTo(contact)} />
{/* استفاده از شناسه کاربر به عنوان کلید هویت */}
<Chat key={to.id} contact={to} />
</div>
);
}
با این ترفند ساده، به محض تغییر مخاطب (to.id)، کل استیتهای داخلی جعبه چت و اینپوتهای آن پاک شده و آماده نوشتن پیام جدید برای مخاطب بعدی میشود.
ریآکت استیت را تا زمانی زنده نگه میدارد که همان کامپوننت در همان آدرس از درخت رندر باقی بماند.
تگهای JSX حاوی استیت نیستند؛ استیتها در هسته ریآکت و متصل به موقعیتِ درختِ خروجیِ JSX نگهداری میشوند.
برای مجبور کردن ریآکت به پاکسازی استیت یک زیردرخت، از پروپ key استفاده کنید.
تعاریف کامپوننتها را هرگز به صورت تودرتو ننویسید تا دچار باگِ ریستِ ناخواسته استیت نشوید.
این محتوا کاملا رایگان توسط تیم کدلپر ترجمه شده و در اختیار شما کاربران عزیز قرار گرفته است، هر گونه کپی برداری برای مقاصد غیر رایگان و بدون ذکر منبع، مورد پیگیری قانونی قرار میگیرد.
ترجمه شده از منبع: https://react.dev/learn