طراحی اصولی ساختار استیت (State)، مرز بین یک کامپوننت تمیز و خوانا با کامپوننتی است که به منبع دائمی باگ در پروژه تبدیل میشود. تغییر و دیباگ استیتهای خوشساختار بسیار لذتبخش است.
در این درس یاد میگیریم که چه زمانی استیتها را یکپارچه یا تفکیک کنیم، از چه اشتباهاتی دوری کنیم و چطور مشکلات رایج معماری استیت را برطرف سازیم.
درست مثل یک مهندس دیتابیس که جدولها را برای جلوگیری از بروز باگ «نرمالسازی» (Normalize) میکند، شما هم باید استیتهای خود را تا حد امکان ساده و بهینه نگه دارید:
۱. دستهبندی استیتهای مرتبط (Group Related State): اگر دو یا چند متغیر استیت همیشه با هم و در یک لحظه آپدیت میشوند، آنها را در قالب یک شیء یا یک استیت واحد ادغام کنید.
۲. جلوگیری از ایجاد تناقض (Avoid Contradictions): ساختار استیت را طوری نچینید که بخشهای مختلف آن با هم متناقض باشند و «وضعیتهای غیرممکن» (Impossible States) ایجاد کنند.
۳. حذف استیتهای تکراری (Avoid Duplicate State): وقتی یک داده را در چند استیت یا در آبجکتهای تودرتو کپی میکنید، همگامسازی آنها در آینده به کابوس تبدیل میشود.
۴. حذف استیتهای محاسباتی (Avoid Redundant State): اگر میتوانید اطلاعاتی را در طول رندر، بر اساس پروپها (props) یا استیتهای موجود محاسبه کنید، هرگز برای آن استیت جدید نسازید.
۵. تختسازی استیتهای تودرتو (Avoid Deeply Nested State): آپدیت کردن اشیاء یا آرایههایی که ساختار درختی و عمیق دارند فوقالعاده دشوار است. تا حد امکان استیتها را به صورت تخت (Flat) طراحی کنید.
گاهی اوقات شک میکنید که برای دو متغیر از دو استیت جداگانه استفاده کنید یا یک شیء واحد:
// روش اول: تفکیک شده
const [x, setX] = useState(0);
const [y, setY] = useState(0);
// روش دوم: یکپارچه و منسجم
const [position, setPosition] = useState({ x: 0, y: 0 });
هر دو روش از نظر فنی کار میکنند، اما اگر دیتای x و y مانند مختصات حرکت ماوس روی صفحه همیشه با هم تغییر میکنند، روش دوم بهترین انتخاب است. این کار باعث میشود هیچگاه آپدیت کردن یکی از مختصاتها را فراموش نکنید.
⚠️ تله آپدیت اشیاء: فراموش نکنید که در ریآکت، متد آپدیت استیت کپی خودکار انجام نمیدهد! اگر میخواهید فقط فیلد
xرا تغییر دهید، نمیتوانید بنویسیدsetPosition({ x: 100 })؛ چون ویژگیyکاملاً حذف خواهد شد. باید ابتدا فیلدهای قبلی را کپی کنید:
setPosition({ ...position, x: 100 })
به این نمونه فرم ثبت نظر نگاه کنید که از دو متغیر بولین استفاده کرده است:
const [text, setText] = useState('');
const [isSending, setIsSending] = useState(false);
const [isSent, setIsSent] = useState(false);
در نگاه اول کد بدون مشکل کار میکند، اما یک تله بزرگ دارد؛ احتمال دارد در منطق پیچیده برنامه فراموش کنید setIsSent و setIsSending را همزمان مدیریت کنید و در یک لحظه هر دو مقدار true شوند! یعنی فرم هم در حال ارسال باشد و هم ارسال شده باشد که یک وضعیت غیرممکن است.
بهترین راهحل، جایگزینی آنها با یک استیت رشتهای واحد به نام status است که در هر لحظه فقط یکی از سه حالت معتبر را میپذیرد: 'typing'، 'sending' یا 'sent':
import { useState } from 'react';
export default function FeedbackForm() {
const [text, setText] = useState('');
const [status, setStatus] = useState('typing');
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setStatus('sending');
await sendMessage(text);
setStatus('sent');
}
// تعریف متغیرهای کمکی محاسباتی برای حفظ خوانایی کدهای JSX
const isSending = status === 'sending';
const isSent = status === 'sent';
if (isSent) return <h1>ممنون از بازخورد شما!</h1>;
return (
<form onSubmit={handleSubmit}>
<textarea disabled={isSending} value={text} onChange={e => setText(e.target.value)} />
<br />
<button disabled={isSending} type="submit">ارسال</button>
{isSending && <p>در حال ارسال...</p>}
</form>
);
}
یکی از رایجترین اشتباهات، ذخیره کردن مقادیری در استیت است که خودشان از روی دیگر استیتها قابل محاسبه هستند. به این مثال اشتباه توجه کنید:
// ❌ ساختار اشتباه و دارای افزونگی
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fullName, setFullName] = useState(''); // اشتباه!
چرا fullName اشتباه است؟ چون تغییرات آن به شدت وابسته به دو استیت دیگر است و باید در تمام onChangeها آن را به صورت دستی آپدیت کنید. به جای این کار، آن را از استیت حذف کرده و به عنوان یک متغیر معمولی در بدنه کامپوننت محاسبه کنید:
// ساختار بهینه و بدون افزونگی
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
// به محض تغییر هر کدام از استیتها، کامپوننت رندر شده و این مقدار خودکار محاسبه میشود
const fullName = firstName + ' ' + lastName;
تصور کنید لیستی از تنقلات سفر دارید و میخواهید کاربر بتواند یکی از آنها را انتخاب کند:
// ❌ ساختار اشتباه (ذخیره کامل آبجکت انتخابی)
const [items, setItems] = useState([
{ title: 'چیپس', id: 0 },
{ title: 'پفک', id: 1 }
]);
const [selectedItem, setSelectedItem] = useState(items[0]); // کپی کل آبجکت!
چرا این ساختار خطرناک است؟ اگر کاربر آیتم اول را انتخاب کند و سپس شما نام آن آیتم را در آرایه اصلی ویرایش کنید (مثلاً «چیپس» بشود «چیپس فلفلی»)، متنی که در پایین صفحه به عنوان آیتم انتخابی نشان داده میشود آپدیت نخواهد شد! چون selectedItem یک کپی قدیمی از آبجکت را نگه داشته و از تغییرات آرایه items بیخبر است.
راهحل بهینه: به جای ذخیره کل آبجکت، فقط شناسه (id) آیتم انتخابی را ذخیره کنید:
import { useState } from 'react';
export default function Menu() {
const [items, setItems] = useState([
{ title: 'چیپس', id: 0 },
{ title: 'پفک', id: 1 }
]);
// فقط ذخیره شناسه به جای کپی کردن کل دیتای آبجکت
const [selectedId, setSelectedId] = useState(0);
// پیدا کردن آیتم زنده در زمان رندر
const selectedItem = items.find(item => item.id === selectedId);
function handleItemChange(id: number, newTitle: string) {
setItems(items.map(item => item.id === id ? { ...item, title: newTitle } : item));
}
return (
<>
<h2>خوراکی سفر شما چیست؟</h2>
<ul>
{items.map(item => (
<li key={item.id}>
<input value={item.title} onChange={e => handleItemChange(item.id, e.target.value)} />
<button onClick={() => setSelectedId(item.id)}>انتخاب</button>
</li>
))}
</ul>
<p>انتخاب شما: {selectedItem?.title}</p>
</>
);
}
اگر در حال طراحی یک ساختار درختی عمیق هستید (مثلاً برنامه مدیریت سفر شامل: سیاره $\rightarrow$ قاره $\rightarrow$ کشور $\rightarrow$ شهر)، مدلسازی آن به صورت آبجکتهای تودرتو (nested) کد شما را برای اعمال یک ویرایش یا حذف ساده، بسیار طولانی و پیچیده میکند؛ زیرا مجبورید تمام لایههای بالایی را کپی (...) کنید.
به جای ساختار درختی، دیتای خود را اصطلاحاً تخت یا نرمالسازی (Normalized) کنید. یعنی هر آیتم را با شناسه خودش به عنوان یک کلید در نظر بگیرید و فرزندان آن را به صورت آرایهای از شناسهها (childIds) نگهداری کنید:
export const initialTravelPlan = {
0: { id: 0, title: '(Root)', childIds: [1, 2] },
1: { id: 1, title: 'زمین', childIds: [3, 4] },
2: { id: 2, title: 'مریخ', childIds: [] },
3: { id: 3, title: 'آسیا', childIds: [] },
4: { id: 4, title: 'اروپا', childIds: [] },
};
حالا اگر بخواهید «آسیا» (شناسه ۳) را از لایه فرزندان «زمین» (شناسه ۱) حذف کنید، نیازی به کپی کردن کل درخت لایوت ندارید؛ بلکه به سادگی و در دو مرحله استیت را بهروزرسانی میکنید:
function handleComplete(parentId: number, childId: number) {
const parent = plan[parentId];
// ۱. حذف آیدی فرزند از آرایه childIds پدر
const nextParent = {
...parent,
childIds: parent.childIds.filter(id => id !== childId)
};
// ۲. آپدیت کردن آبجکت ریشه
setPlan({
...plan,
[parentId]: nextParent
});
}
اگر چند استیت همیشه با هم تغییر میکنند، آنها را در یک آبجکت واحد قرار دهید.
از استیتهای رشتهای وضعیت (مانند status: 'sending') برای جلوگیری از وضعیتهای غیرممکن استفاده کنید.
هرگز متغیر پروپ را مستقیماً درون useState کپی (Mirror) نکنید، مگر اینکه عمداً بخواهید جلوی بهروزرسانیهای کامپوننت پدر را بگیرید.
برای سیستمهای انتخاب (Selection)، به جای ذخیره کامل آبجکت، فقط شناسه یا ایندکس آن را در استیت نگه دارید.
با تختسازی و نرمالسازی دیتای کامپوننتهای تودرتو، فرآیند جهش و دستکاری مپها را به شدت سادهتر کنید.
این محتوا کاملا رایگان توسط تیم کدلپر ترجمه شده و در اختیار شما کاربران عزیز قرار گرفته است، هر گونه کپی برداری برای مقاصد غیر رایگان و بدون ذکر منبع، مورد پیگیری قانونی قرار میگیرد.
ترجمه شده از منبع: https://react.dev/learn