Immutability و روش درست به‌روزرسانی Objects و Arrays در State

تا اینجا مثال‌های State را با مقادیر ساده مانند عدد یا رشته دیدیم. اما در برنامه‌های واقعی، State اغلب یک آبجکت یا آرایه است. به‌روزرسانی این نوع مقادیر، قانون مهمی دارد که اگر رعایت نشود، می‌تواند باعث رفتارهای اشتباه و سخت‌ردیابی شود: قانون Immutability.

Immutability یعنی چه

Immutability به این معناست که یک مقدار، پس از ساخته شدن، دیگر نباید مستقیماً تغییر کند؛ به‌جای تغییر مستقیم، باید یک نسخه جدید از آن ساخته شود که شامل تغییر موردنظر است. در React، این قانون به‌طور خاص درباره State اهمیت زیادی دارد.

مشکل: تغییر مستقیم یک Object در State

function ProfileForm() {
  const [user, setUser] = useState({ name: "سارا", age: 25 });

  function handleBirthday() {
    user.age = user.age + 1; // تغییر مستقیم آبجکت
    setUser(user);
  }

  return <button onClick={handleBirthday}>{user.age}</button>;
}

ممکن است تصور کنیم چون setUser(user) را فراخوانی کرده‌ایم، React باید رندر مجدد را انجام دهد. اما مشکل این است که user همان آبجکت قبلی است (فقط یکی از فیلدهای آن به‌صورت مستقیم تغییر کرده)؛ React برای تشخیص اینکه آیا State واقعاً تغییر کرده یا نه، آبجکت جدید را با آبجکت قبلی از نظر ارجاع (Reference) مقایسه می‌کند، نه محتوای داخلی آن. چون ارجاع هر دو یکسان است، React ممکن است متوجه تغییر نشود و رندر مجدد انجام ندهد.

راه‌حل درست: ساخت یک آبجکت جدید

به‌جای تغییر مستقیم فیلدهای آبجکت، باید یک آبجکت جدید بسازیم که کپی‌ای از مقادیر قبلی به‌همراه تغییر موردنظر است. رایج‌ترین ابزار برای این کار، عملگر Spread (...) است:

function handleBirthday() {
  setUser({ ...user, age: user.age + 1 });
}

در این کد، یک آبجکت کاملاً جدید ساخته می‌شود که تمام فیلدهای user را کپی می‌کند و فقط مقدار age را بازنویسی می‌کند. چون این آبجکت جدید از نظر ارجاع با آبجکت قبلی متفاوت است، React به‌درستی متوجه تغییر می‌شود.

به‌روزرسانی Arrays در State

همین قانون درباره آرایه‌ها هم صدق می‌کند. متدهایی مانند push، splice یا sort آرایه اصلی را مستقیماً تغییر می‌دهند و نباید روی State استفاده شوند. به‌جای آن، باید از متدهایی استفاده کرد که یک آرایه جدید بازمی‌گردانند:

const [items, setItems] = useState(["قلم", "دفتر"]);

// اشتباه: تغییر مستقیم آرایه
items.push("پاک‌کن");
setItems(items);

// درست: ساخت آرایه جدید
setItems([...items, "پاک‌کن"]);

برای حذف یک آیتم از آرایه، به‌جای splice، معمولاً از filter استفاده می‌شود:

setItems(items.filter((item) => item !== "دفتر"));

چرا React به این قانون نیاز دارد

مقایسه بر اساس ارجاع، به‌جای مقایسه عمیق محتوا، یک تصمیم عمدی در طراحی React است؛ چون بررسی عمیق تمام محتوای یک آبجکت یا آرایه در هر رندر، از نظر عملکرد پرهزینه است. با تکیه بر مقایسه ارجاع، React می‌تواند به‌سرعت تشخیص دهد که آیا مقدار تغییر کرده یا نه، اما در ازای این سرعت، از ما می‌خواهد همیشه یک مقدار جدید بسازیم، نه اینکه مقدار قبلی را مستقیماً تغییر دهیم.

مثال قابل اجرا

به‌روزرسانی درست یک آبجکت در State
برای دیدن پیش‌نمایش کامپوننت، روی Run بزنید.

جمع‌بندی

State نوع Object یا Array هرگز نباید مستقیماً تغییر کند؛ باید همیشه یک نسخه جدید ساخته و به تابع set داده شود، معمولاً با کمک عملگر Spread یا متدهایی مانند filter که آرایه جدید بازمی‌گردانند. دلیل این قانون، تکیه React بر مقایسه ارجاع برای تشخیص تغییر State است. در درس بعدی به مفهوم Derived State می‌پردازیم.