
در توسعه فرانتاند، خیلی زود به این نقطه میرسیم که دادههایی که از سرور میگیریم، ساختاری تودرتو (nested) دارند. یک پست وبلاگ چند کامنت دارد، هر کامنت یک نویسنده دارد، و همان نویسنده ممکن است در پستهای دیگر هم حضور داشته باشد. وقتی این دادهها را همانطور که از API آمدهاند مستقیم در state ذخیره میکنیم، خیلی زود با مشکلاتی مثل دادههای تکراری، ریرندرهای غیرضروری و آپدیتهای پیچیده مواجه میشویم.
State Normalization دقیقاً همین مشکل را حل میکند: تبدیل دادههای تودرتو به یک ساختار مسطح (flat) که شبیه جداول یک دیتابیس رفتار میکند.
نرمالسازی مفهومی است که از طراحی دیتابیسهای رابطهای وام گرفته شده. در دیتابیس، بهجای اینکه اطلاعات کاربر را داخل هر رکورد سفارش تکرار کنیم، یک جدول جداگانه برای کاربران میسازیم و در جدول سفارشها فقط شناسه (ID) کاربر را نگه میداریم.
همین ایده در state فرانتاند هم قابل استفاده است. مستندات رسمی Redux این رویکرد را بهعنوان روش پیشنهادی برای مدیریت دادههای رابطهای یا تودرتو معرفی کرده است؛ ایده اصلی این است که بخشی از store را طوری در نظر بگیریم که انگار یک دیتابیس است.
در عمل، نرمالسازی یعنی:
فرض کنید یک اپلیکیشن وبلاگ داریم که پاسخ API آن اینطور است:
{
"posts": [
{
"id": "p1",
"title": "شروع کار با React",
"author": { "id": "u1", "name": "سارا احمدی" },
"comments": [
{
"id": "c1",
"text": "مقاله خوبی بود",
"author": { "id": "u2", "name": "رضا کریمی" }
}
]
}
]
}مشکل این ساختار این است که اگر کاربر «سارا احمدی» نامش را عوض کند، باید در تمام پستها و کامنتهایی که او در آنها حضور دارد، این تغییر را اعمال کنیم. همینطور اگر بخواهیم لیست پستها را جدا از کامنتها رندر کنیم، باید کل درخت را پیمایش کنیم.
نسخه نرمالشده همین داده به این شکل است:
{
"posts": {
"byId": {
"p1": { "id": "p1", "title": "شروع کار با React", "author": "u1", "comments": ["c1"] }
},
"allIds": ["p1"]
},
"users": {
"byId": {
"u1": { "id": "u1", "name": "سارا احمدی" },
"u2": { "id": "u2", "name": "رضا کریمی" }
},
"allIds": ["u1", "u2"]
},
"comments": {
"byId": {
"c1": { "id": "c1", "text": "مقاله خوبی بود", "author": "u2" }
},
"allIds": ["c1"]
}
}حالا هر نوع داده در جدول خودش قرار دارد و هیچ اطلاعاتی تکرار نشده. این الگو معمولاً بهنام ساختار byId و allIds شناخته میشود؛ byId برای دسترسی سریع به هر آیتم با شناسهاش، و allIds برای حفظ ترتیب آیتمها.
وقتی یک آبجکت (مثلاً یک کاربر) در چند جای مختلف state کپی شده باشد، بهمحض تغییر یکی از آنها، بقیهی کپیها قدیمی میمانند. نرمالسازی تضمین میکند هر موجودیت (entity) فقط یک "منبع حقیقت" (single source of truth) دارد.
در کتابخانههایی مثل React که آپدیتهای تغییرناپذیر (immutable) دارند، تغییر یک آیتم تودرتو معمولاً باعث میشود کل مسیر تا ریشهی state کپی شود. این یعنی حتی کامپوننتهایی که دادهشان تغییر نکرده هم ممکن است دوباره رندر شوند. با ساختار مسطح، فقط همان بخش کوچکی از state که واقعاً تغییر کرده بهروزرسانی میشود.
آپدیت، حذف یا افزودن یک آیتم در یک آبجکت با کلید شناسه، بسیار سادهتر از پیدا کردن و ویرایش آن در دل چند سطح آرایهی تودرتو است. کافی است با شناسهی آیتم به آن دسترسی پیدا کنید.
هرچه اپلیکیشن بزرگتر و روابط بین دادهها پیچیدهتر شود، نگهداری ساختار تودرتو سختتر میشود. state نرمالشده رفتار قابلپیشبینیتری دارد و تیمهای بزرگتر راحتتر میتوانند روی آن کار کنند.
نرمالسازی همیشه لازم نیست. برای فرمهای ساده، صفحات تکی، یا دادههایی که هیچ رابطهای با هم ندارند، اضافه کردن این لایه فقط پیچیدگی بیدلیل ایجاد میکند. اما وقتی با یکی از این موارد روبهرو هستید، نرمالسازی معمولاً ارزشش را نشان میدهد:
برای پیادهسازی دستی، میتوانید ساختار byId/allIds را خودتان بسازید. اما برای دادههای واقعاً تودرتو، کتابخانهی normalizr استاندارد صنعتی است. با این کتابخانه، یک schema برای موجودیتها و روابطشان تعریف میکنید و تابع normalize() دادهی خام را به همان ساختار مسطح تبدیل میکند.
اگر از Redux Toolkit استفاده میکنید، ابزار createEntityAdapter دقیقاً برای همین منظور طراحی شده و بخش زیادی از این کار تکراری را خودکار میکند. در کتابخانههای دیگر مدیریت state مثل Zustand یا حتی در کش کوئریهای TanStack Query هم میتوانید همین اصول را بهصورت دستی پیاده کنید، چون این یک الگوی معماری است، نه یک ویژگی مخصوص Redux.
State Normalization یعنی رفتار کردن با بخشی از state اپلیکیشن مثل یک دیتابیس کوچک: هر نوع داده در جدول خودش، دسترسی از طریق شناسه، و بدون تکرار اطلاعات. این الگو مشکلات رایج دادههای تودرتو -- ناهماهنگی، ریرندرهای غیرضروری و آپدیتهای پیچیده -- را حل میکند و برای اپلیکیشنهایی با دادههای رابطهای، یک سرمایهگذاری کمهزینه با بازدهی بالا در بلندمدت است.