تقریباً هر کدبیس React را که باز کنید، متوجه یک واکنش مشابه خواهید شد. یک توسعه‌دهنده نیاز دارد مقداری را ردیابی کند، پس سراغ useState می‌رود. نیاز به یک شمارنده دارید؟ useState. یک مقدار ورودی موقت؟ useState. یک boolean برای باز و بسته کردن یک مودال؟ useState. طولی نمی‌کشد که یک کامپوننت واحد، ده‌ها هوک مجزا را در خود جای می‌دهد که هر کدام بخش کوچکی از داده‌ها را مدیریت می‌کنند؛ داده‌هایی که ممکن است نیاز به ماندگاری در طول رندرها داشته باشند یا نداشته باشند. نتیجه این کار، کدی شلوغ، رندرهای مجدد (re-renders) اضافی و state‌ای است که مانند پول خرد در گوشه و کنار یک کامپوننت پخش شده است.

این عادت قابل درک است. useState اولین هوکی است که اکثر ما یاد می‌گیریم و کار می‌کند. اما «کار کردن» با «مناسب بودن» یکی نیست. برخورد با هر تکه از داده به عنوان یک state واکنش‌گرا (reactive state)، مشکلاتی ایجاد می‌کند که تنها پس از بزرگ شدن کامپوننت خود را نشان می‌دهند.

فقط به این دلیل که چیزی تغییر می‌کند، لزوماً به State نیاز ندارد

هر متغیری که در طول زمان تغییر می‌کند، لزوماً نباید در useState باشد. برخی از مقادیر صرفاً نتیجه‌ی چیز دیگری هستند که از قبل در اختیار دارید. اگر نام کامل کاربر را صرفاً به این دلیل که firstName و lastName را به هم می‌چسبانید در state ذخیره کنید، اکنون دو منبع حقیقت (source of truth) دارید. وقتی firstName به دلیل رندر مجدد والد به‌روز می‌شود، state مربوط به fullName شما تا زمانی که یک effect دیگر برای همگام‌سازی آن اجرا نشود، قدیمی (stale) باقی می‌ماند. شما به یک effect برای همگام‌سازی نیاز ندارید؛ شما به یک مقدار مشتق‌شده (derived value) نیاز دارید.

const fullName = `${firstName} ${lastName}`;

آن را در حین رندر محاسبه کنید. اگر محاسبه‌ی آن هزینه‌بر است، آن را memoize کنید. اما تا زمانی که کاربر نتواند آن نام کامل را مستقل از اجزای آن ویرایش کند، برای آن یک هوک useState مجزا ایجاد نکنید.

همین قاعده برای لیست‌های فیلتر شده نیز صدق می‌کند. اگر هم allItems و هم filteredItems را در state نگه دارید، سطح نگهداری کد خود را دو برابر کرده‌اید. فیلتر کردن را در حین رندر انجام دهید. آرایه اصلی و متن فیلتر را در state نگه دارید، سپس لیست قابل مشاهده را مشتق کنید. این کار تضمین می‌کند که لیست فیلتر شده هرگز با منبع اصلی از همگام خارج نمی‌شود.

برخی مقادیر هرگز نباید باعث رندر مجدد شوند

useState دقیقاً برای این وجود دارد که به React بگوید چیزی تغییر کرده و ممکن است DOM نیاز به به‌روزرسانی داشته باشد. اگر مقداری تغییر می‌کند اما هیچ بخشی از UI به آن تغییر اهمیت نمی‌دهد، useRef ابزار بهتری است.

تایمرها و اینتروال‌ها (intervals) نمونه‌ای کلاسیک هستند. ذخیره کردن IDهای setInterval در state باعث می‌شود هر بار که یک تایمر را شروع یا متوقف می‌کنید، یک رندر مجدد رخ دهد، در حالی که کاربر اصلاً ID اینتروال را نمی‌بیند. یک ref آن مقدار را بدون اطلاع‌رسانی به React نگه می‌دارد. همین منطق برای ردیابی props قبلی، اندازه‌گیری گره‌های DOM قبل از ترسیم (paint)، یا ذخیره آخرین callback برای یک هوک سفارشی نیز صدق می‌کند. از خود بپرسید: آیا این مقدار نیاز دارد روی صفحه نمایش داده شود؟ اگر پاسخ منفی است، احتمالاً نیازی به useState ندارد.

خودِ گره‌های DOM نیز متعلق به refها هستند. اگرچه می‌توانید یک عنصر DOM را در state ذخیره کنید، اما انجام این کار باعث می‌شود پس از اجرای ref callback، یک رندر مجدد رخ دهد. در بیشتر موارد، شما فقط به آن گره برای یک متد دستوری (imperative) یا یک اندازه‌گیری نیاز دارید، نه برای رندر کردن متفاوت آن.

تله‌ی Boolean

مسائل مرتبط با UI تمایل دارند زمانی که هر پرچم (flag) هوک مخصوص به خود را داشته باشد، پراکنده شوند. شما کامپوننت‌هایی را می‌بینید که در آن‌ها isLoading ،isError و isSuccess به عنوان سه boolean مجزا تعریف شده‌اند. مشکل اینجاست که این سه وضعیت مستقل از هم نیستند. اگر هر دو isLoading و isSuccess درست (true) باشند، UI شما در یک وضعیت غیرممکن قرار می‌گیرد، با این حال TypeScript و React اجازه می‌دهند که آن را رندر کنید.

گروه‌بندی stateهای مرتبط از این ترکیب‌های نامعتبر جلوگیری می‌کند. به جای سه boolean، یک رشته وضعیت (status string) واحد را ردیابی کنید: 'idle'، 'loading'، 'success' یا 'error'. در هر لحظه فقط یکی می‌تواند فعال باشد، که این کار حالت‌های غیرممکن را در سطح تایپ حذف می‌کند. اگر داده‌ها پیچیده‌تر هستند، استفاده از یک object با یک discriminated union همه چیز را حتی تمیزتر می‌کند. هرگاه متوجه شدید که چندین فراخوانی useState را در داخل یک هندلر رویداد (event handler) واحد به‌روزرسانی می‌کنید، این نشانه‌ای است که آن مقادیر باید با هم باشند.

به جای یک useState دیگر، سراغ useReducer بروید

نقطه‌ای وجود دارد که به‌روزرسانی‌های state شبیه به بازی whack-a-mole (زدن موش‌های سر به بیرون) می‌شوند. شما setA را فراخوانی می‌کنید، سپس setB را، و سپس به صورت شرطی setC را، همگی در داخل یک تابع. توسعه‌دهنده‌ای که بعداً آن کد را می‌خواند، باید تمام آن توالی را دنبال کند تا بفهمد کامپوننت واقعاً چه کاری انجام می‌دهد.

useReducer در اینجا می‌درخشد. این هوک به این دلیل که پیشرفته‌تر است جایگزین useState نمی‌شود، بلکه به این دلیل که منطق برنامه آن را می‌طلبد، جایگزین آن می‌شود. یک reducer نحوه تغییر state را متمرکز می‌کند. به جای پراکنده کردن دستورات در هندلرهای رویداد، شما یک قصد (intention) را dispatch می‌کنید: dispatch({ type: 'submitted' }). ردیسر تصمیم می‌گیرد که وضعیت بعدی چگونه باشد. این کار تست کردن را بسیار ساده می‌کند، زیرا منطق state شما یک تابع خالص (pure function) است. همچنین عیب‌یابی (debugging) را آسان‌تر می‌کند، زیرا هر تغییر یک action قابل ردیابی به جا می‌گذارد.

برای استفاده از یک reducer نیازی به Redux ندارید. اگر سه یا چند متغیر state دارید که با هم به‌روزرسانی می‌شوند، یا اگر state بعدی شما به شدت به state قبلی وابسته است، یک reducer به‌طور چشمگیری کار با component را ساده می‌کند.

محل واقعی قرارگیری state

گاهی اوقات مسئله این نیست که چگونه state را ذخیره می‌کنید، بلکه مسئله این است که کجا. یک اشتباه رایج، hoisting کردن state به یک parent است، صرفاً به این دلیل که ممکن است در جای دیگری به آن نیاز باشد. اگر تنها یک leaf component از بخشی از state استفاده می‌کند، آن را همان‌جا نگه دارید. این همان colocation است و شعاع اثر (blast radius) تغییرات را کاهش می‌دهد. باعث re-render شدن parent نشوید، صرفاً چون یک child باز شده است