بیشتر باگهای فرانتاند به خاطر UI بد نیستند.
مشکل اصلی معمولاً از مالکیت نامشخص State شروع میشود.
ممکن است باگ خودش را در ظاهر رابط کاربری نشان دهد، اما ریشه مشکل معمولاً جای دیگری است:
مالک این State کیست؟ چه کسی اجازه دارد آن را تغییر دهد؟ چه بخشهایی به آن وابسته هستند؟ چه زمانی باید reset شود؟ آیا باید بعد از تغییر صفحه باقی بماند؟ آیا باید با سرور sync شود؟
وقتی جواب این سؤالها مشخص نباشد، نگهداری یک اپلیکیشن فرانتاند به مرور سختتر میشود.
در ابتدا همهچیز ساده به نظر میرسد.
یک useState اینجا اضافه میکنید.
یک prop آنجا پاس میدهید.
بعد Context اضافه میکنید چون prop drilling اذیتکننده شده.
بعدتر شاید Zustand، Redux، React Query یا یک ابزار دیگر برای مدیریت state اضافه شود.
و برای مدتی همهچیز کار میکند.
اما وقتی اپلیکیشن بزرگتر میشود، باگهای عجیب شروع میشوند:
فیلترها بیدلیل reset میشوند
مودالها از جای اشتباه باز میشوند
فرمها مقدارهای قدیمی را نگه میدارند
دادههای API داخل local state کپی میشوند
چند کامپوننت یک مقدار مشترک را تغییر میدهند
یک تغییر کوچک سه صفحه مختلف را خراب میکند
در این نقطه، مشکل معمولاً React نیست.
مشکل، مالکیت نامشخص state است.
وقتی توسعهدهندهها درباره مدیریت state صحبت میکنند، بحث خیلی سریع به سمت ابزارها میرود.
آیا باید از Redux استفاده کنیم؟
آیا باید Zustand استفاده کنیم؟
آیا Context API کافی است؟
آیا باید React Query داشته باشیم؟
Jotai، Recoil، MobX یا ابزارهای دیگر چطور؟
همه این ابزارها مفید هستند.
اما هیچ ابزاری بهتنهایی طراحی بد state را درست نمیکند.
اگر اپلیکیشن قوانین واضحی برای محل زندگی state نداشته باشد، حتی بهترین کتابخانه مدیریت state هم میتواند تبدیل به یک آشفتگی شود.
Global store میتواند تبدیل به سطل زباله همهچیز شود.
Context میتواند بیش از حد استفاده شود.
Local state میتواند تکراری شود.
Server data میتواند داخل client state کپی شود و stale بماند.
سؤال اصلی این نیست:
از کدام کتابخانه مدیریت state استفاده کنم؟
سؤال بهتر این است:
مالک این مقدار چه کسی باید باشد؟
این سؤال معمولاً ما را به معماری بهتری میرساند.
در بیشتر اپلیکیشنهای فرانتاند، state معمولاً در سه دسته کلی قرار میگیرد:
server state local UI state shared client state
هرکدام مالک متفاوت و lifecycle متفاوتی دارند.
اشتباه گرفتن اینها با هم یکی از سادهترین راهها برای ساختن باگهای فرانتاند است.
Server state دادهای است که متعلق به بکاند است.
مثالها:
کاربران سفارشها محصولات دسترسیها اعلانها پیامها تاریخچه پرداخت اطلاعات پروفایل
این داده معمولاً از API میآید.
ممکن است cache شود.
ممکن است نیاز به refetch داشته باشد.
ممکن است بین چند کاربر مشترک باشد.
ممکن است خارج از session فعلی مرورگر تغییر کند.
ممکن است stale شود.
به همین دلیل، server state معمولاً باید متفاوت از state معمولی UI مدیریت شود.
مثلاً اگر لیست کاربران را از بکاند میگیرید، کپی کردن آن داخل useState و sync نگه داشتن دستی آن میتواند دردسرساز شود.
رویکرد بهتر معمولاً استفاده از ابزارهای server-state است، مثل:
TanStack Query / React Query SWR Apollo Client Relay
این ابزارها برای مسائلی مثل موارد زیر ساخته شدهاند:
caching refetching loading states error states pagination invalidation background updates stale data
Server state باید به این سؤال جواب دهد:
بکاند در حال حاضر چه دادهای را میشناسد؟
نباید با state باز یا بسته بودن یک dropdown یکسان دیده شود.
Local UI state متعلق به یک کامپوننت یا یک بخش کوچک از UI است.
مثالها:
آیا این dropdown باز است؟ آیا این modal نمایش داده شود؟ کدام tab انتخاب شده؟ مقدار فعلی input چیست؟ آیا tooltip فعال است؟ آیا accordion باز شده؟
این نوع state معمولاً نیازی به global شدن ندارد.
نیازی ندارد در کل اپلیکیشن باقی بماند.
نیازی ندارد بهصورت پیشفرض داخل Redux یا Zustand ذخیره شود.
در بسیاری از موارد، state ساده React کافی است:
const [isOpen, setIsOpen] = useState(false);
اشتباه زمانی اتفاق میافتد که توسعهدهندهها local UI state را خیلی زود global میکنند.
مثلاً state باز یا بسته بودن یک dropdown معمولاً به global store تعلق ندارد.
یک draft موقت فرم هم لزوماً نباید در global store باشد، مگر اینکه لازم باشد بعد از navigation باقی بماند یا بین چند بخش مستقل از اپلیکیشن shared شود.
Local UI state باید به این سؤال جواب دهد:
این بخش از رابط کاربری همین الان به چه چیزی نیاز دارد؟
اگر جواب فقط داخل یک کامپوننت اهمیت دارد، آن را local نگه دارید.
Shared client state دادهای است که متعلق به اپلیکیشن فرانتاند است و چند بخش مستقل از UI به آن نیاز دارند.
مثالها:
session کاربر لاگینشده theme زبان وضعیت باز یا بسته بودن sidebar فیلترهای global سبد خرید workspace انتخابشده تعداد اعلانها state یک wizard چندمرحلهای
این نوع state ممکن است به یک shared store نیاز داشته باشد.
بسته به اپلیکیشن، این store میتواند یکی از اینها باشد:
Context API Zustand Redux Toolkit Jotai MobX custom store
اما shared state باید آگاهانه استفاده شود.
هر prop chain به Context نیاز ندارد.
هر مقدار تکرارشوندهای global store نمیخواهد.
هر مشکل ارتباط بین کامپوننتها نباید تبدیل به global state شود.
Shared client state باید به این سؤال جواب دهد:
آیا چند بخش غیرمرتبط از اپلیکیشن واقعاً به این مقدار نیاز دارند؟
اگر جواب نه است، احتمالاً این state نباید global باشد.
یکی از رایجترین code smellها در فرانتاند، کپی کردن داده API داخل local state بدون دلیل مشخص است.
مثلاً:
const { data: user } = useQuery({ queryKey: ["user", userId], queryFn: () => fetchUser(userId) }); const [localUser, setLocalUser] = useState(user);
این کار معمولاً دو منبع حقیقت ایجاد میکند.
حالا اپلیکیشن باید به این سؤالها جواب دهد:
کدام درست است؟ داده API؟ کپی local؟ وقتی داده سرور تغییر کند چه میشود؟ بعد از mutation چه اتفاقی میافتد؟ بعد از refetch چه میشود؟
گاهی اوقات داشتن کپی local منطقی است، مخصوصاً برای draftهای قابل ویرایش.
مثلاً یک فرم ممکن است با داده سرور پر شود، اما کاربر قبل از ذخیره آن را ویرایش کند.
اما در این حالت، state باید بهعنوان draft دیده شود، نه نسخه دوم server state.
مدل ذهنی بهتر:
Server state = منبع حقیقت از سمت بکاند Form state = draft موقت ساختهشده از آن منبع
این تفاوت مهم است.
Global state در ابتدا راحت به نظر میرسد.
از هر جایی میتوانید به آن دسترسی داشته باشید.
از هر جایی میتوانید آن را تغییر دهید.
میتوانید از prop drilling فرار کنید.
اما این راحتی هزینه دارد.
اگر همهچیز بتواند یک state را بخواند و تغییر دهد، فهمیدن سیستم سختتر میشود.
کمکم این سؤالها پیش میآید:
چه کسی این مقدار را تغییر داد؟ چرا این کامپوننت دوباره render شد؟ چرا این صفحه reset شد؟ چرا این modal باز است؟ چرا این فیلتر روی صفحه دیگری اثر گذاشت؟
Global state نباید حالت پیشفرض باشد.
باید یک تصمیم آگاهانه باشد.
قبل از global کردن state، بپرسید:
آیا این مقدار باید توسط بخشهای غیرمرتبط اپلیکیشن استفاده شود؟ آیا باید بعد از تغییر route باقی بماند؟ آیا نشاندهنده رفتار سطح اپلیکیشن است؟ آیا پاس دادن آن واقعاً کد را بدتر میکند یا فقط کمی ناخوشایند است؟
گاهی global state کاملاً درست است.
اما global کردن همهچیز coupling پنهان ایجاد میکند.
یک state variable فقط درباره محل زندگیاش نیست.
درباره مدت زنده بودنش هم هست.
بعضی stateها باید هنگام unmount شدن کامپوننت reset شوند.
بعضی stateها باید هنگام تغییر route reset شوند.
بعضی stateها باید بعد از navigation باقی بمانند.
بعضی stateها باید بعد از refresh هم باقی بمانند.
بعضی stateها باید داخل URL ذخیره شوند.
بعضی stateها باید داخل local storage persist شوند.
مثلاً:
فیلترهای جستجو ممکن است به URL تعلق داشته باشند. state باز بودن modal ممکن است local component state باشد. authentication state ممکن است در shared store باشد. داده API ممکن است در server-state cache باشد. فرم چندمرحلهای ممکن است نیاز به persistence داشته باشد.
اگر lifecycle واضح نباشد، احتمال باگ بالا میرود.
برای همین من دوست دارم این سؤال را بپرسم:
این state چه زمانی باید بمیرد؟
این سؤال به اندازه سؤال زیر مهم است:
این state کجا باید زندگی کند؟
بعضی stateهای فرانتاند باید در URL زندگی کنند.
مثالها:
عبارت جستجو صفحه pagination ترتیب مرتبسازی فیلترهای انتخابشده tab فعال در بعضی صفحات
چرا؟
چون URL state قابل اشتراکگذاری است.
بعد از refresh باقی میماند.
از browser navigation پشتیبانی میکند.
صفحه را bookmarkable میکند.
debug کردن را سادهتر میکند.
مثلاً این معمولاً بهتر است:
/products?category=laptops&page=2&sort=price
تا اینکه تمام state فیلترها را داخل یک کامپوننت یا global store پنهان کنیم.
همه UI stateها به URL تعلق ندارند، اما برای فیلترهای سطح صفحه و stateهای مرتبط با navigation، URL اغلب مالک مناسبی است.
قبل از اضافه کردن یک state variable جدید، من معمولاً این سؤالها را میپرسم:
۱. آیا این داده از بکاند میآید؟ ۲. آیا فقط در یک کامپوننت استفاده میشود؟ ۳. آیا چند بخش غیرمرتبط از اپلیکیشن به آن نیاز دارند؟ ۴. آیا باید بعد از navigation باقی بماند؟ ۵. آیا باید بعد از refresh باقی بماند؟ ۶. آیا باید از طریق URL قابل اشتراکگذاری باشد؟ ۷. آیا یک draft موقت است؟ ۸. چه کسی اجازه دارد آن را تغییر دهد؟ ۹. چه زمانی باید reset شود؟ ۱۰. منبع حقیقت چیست؟
این سؤالها ساده هستند، اما جلوی بسیاری از باگها را میگیرند.
از server state برای دادهای استفاده کنید که متعلق به بکاند است.
از local state برای رفتار UI داخل یک کامپوننت یا یک بخش کوچک استفاده کنید.
از shared client state فقط زمانی استفاده کنید که چند بخش غیرمرتبط از اپلیکیشن واقعاً به یک مقدار مشترک نیاز دارند.
از URL state زمانی استفاده کنید که state مربوط به navigation، فیلترها، جستجو یا تنظیمات قابل اشتراکگذاری صفحه است.
Server state را داخل local state کپی نکنید، مگر اینکه عمداً در حال ساختن یک draft باشید.
State را فقط به این دلیل که پاس دادن prop کمی آزاردهنده است global نکنید.
و مهمتر از همه:
قبل از اضافه کردن یک state variable جدید، بپرسید چه کسی باید مسئول این مقدار باشد.
همین یک سؤال میتواند جلوی بسیاری از باگهای آینده را بگیرد.
توسعه فرانتاند در سطح senior فقط دانستن hookها، کتابخانههای state management یا APIهای فریمورک نیست.
موضوع اصلی این است که بدانیم state کجا باید زندگی کند.
و کجا نباید زندگی کند.
یک معماری فرانتاند تمیز معمولاً مالکیت مشخصی دارد:
بکاند مالک server data است کامپوننت مالک local UI behavior است app store مالک shared client state واقعی است URL مالک navigation state قابل اشتراکگذاری است
وقتی مالکیت واضح باشد، فهمیدن اپلیکیشن سادهتر میشود.
وقتی مالکیت نامشخص باشد، حتی featureهای ساده هم شکننده میشوند.
بیشتر باگهای فرانتاند واقعاً باگ UI نیستند.
آنها باگهای مالکیت هستند.
