هر توسعه‌دهنده React در نهایت با این سوال روبرو می‌شود: آیا باید از Context استفاده کنم یا این یک مشکل مربوط به Redux است؟ اگر تنها چند ماه است که برنامه‌نویسی می‌کنید، هیاهوی موجود در فضای آنلاین باعث می‌شود این موضوع یک تصمیم «یا این یا آن» به نظر برسد. برخی آموزش‌ها با Redux مانند یک بار اضافی و قدیمی برخورد می‌کنند. برخی دیگر هشدار می‌دهند که Context نمی‌تواند فراتر از یک لیست انجام کار (to-do list) مقیاس‌پذیر باشد. هیچ‌کدام از این دو دیدگاه افراطی مفید نیستند. حقیقت این است که این ابزارها انواع مختلفی از مشکلات را حل می‌کنند و انتخاب هوشمندانه بستگی به این دارد که اپلیکیشن شما واقعاً چه کاری انجام می‌دهد.

مشکل Prop Drilling

قبل از اینکه یک استراتژی مدیریت وضعیت (state management) انتخاب کنید، بهتر است درکی از مشکلی داشته باشید که هر دو ابزار سعی در درمان آن دارند. تصور کنید در حال ساخت یک سایت تجارت الکترونیک هستید. شما پروفایل کاربر را در سطح بالای کامپوننت App دریافت می‌کنید. در پایین‌ترین قسمت یعنی footer، یک کامپوننت کوچک به نام AccountLink به آن عکس پروفایل نیاز دارد. بدون یک استور (store) سراسری، شیء کاربر باید از طریق Home سپس Header سپس NavContainer و در نهایت UserDropdown عبور کرده و به AccountLink برسد. هر لایه در این میان، به داده‌ای دسترسی پیدا می‌کند که از آن استفاده نمی‌کند. این همان Prop Drilling است.

Prop drilling باعث شکنندگی کامپوننت‌ها می‌شود. بازسازی کد (Refactoring) ریسک‌بردار می‌شود، زیرا حذف کردن یک واسطه در میان، کل زنجیره را از کار می‌اندازد. قابلیت استفاده مجدد (Reusability) آسیب می‌بیند، زیرا کامپوننت‌ها از propهایی درخواست می‌کنند که آن‌ها را صرفاً به لایه‌های پایین‌تر پاس می‌دهند. هر دو ابزار Context و Redux با اجازه دادن به کامپوننت‌های دوردست برای اشتراک مستقیم در داده‌های مشترک، این مشکل را از بین می‌برند. اما روشی که آن‌ها این داده‌ها را تحویل می‌دهند و هزینه‌ی انجام این کار، به سرعت از هم فاصله می‌گیرند.

چه زمانی React Context API انتخاب مناسبی است

React Context به خودِ کتابخانه داخلی شده است. نیاز به نصب npm اضافی، پیکربندی build یا فایل‌های boilerplate نیست. شما یک شیء context ایجاد می‌کنید، بخشی از درخت خود را در یک Provider قرار می‌دهید و مقدار را با استفاده از useContext در هر کامپوننت تودرتو مصرف می‌کنید. به دلیل همین سادگی، Context در پروژه‌های کوچک تا متوسط که تغییرات وضعیت (state) کم‌تکرار است و ساختار آن وضعیت نسبتاً تخت (flat) است، می‌درخشد.

به تم‌های رابط کاربری (UI themes) فکر کنید. یک کاربر شاید در هر جلسه کار، تنها یک بار بین حالت روشن و تاریک جابجا شود. این مقدار به هر کامپوننت استایل‌گذاری شده منتقل می‌شود، اما آنقدر به ندرت تغییر می‌کند که نگرانی‌های مربوط به عملکرد (performance) به سختی خود را نشان می‌دهند. وضعیت احراز هویت (Authentication) یکی دیگر از موارد کلاسیک است. پس از اینکه کاربر وارد شد، پرچم isAuthenticated و شیء user در طول ده‌ها پیمایش در صفحات ثابت می‌مانند. تنظیمات زبان یا بومی‌سازی نیز به همین صورت عمل می‌کنند. این‌ها سیگنال‌های گسترده و کندی هستند که بسیاری از کامپوننت‌ها به آن‌ها نیاز دارند، اما تعداد کمی از کامپوننت‌ها آن‌ها را تغییر می‌دهند (mutate).

نکته منفی، نحوه مدیریت به‌روزرسانی‌ها توسط Context است. وقتی مقدار یک Context Provider تغییر می‌کند، React تمام کامپوننت‌هایی را که از آن context استفاده می‌کنند، دوباره رندر (re-render) می‌کند. در یک اپلیکیشن کوچک، متوجه این موضوع نخواهید شد. اما در یک اپلیکیشن بزرگتر، اگر داده‌هایی با تغییرات سریع را درون یک Context پرکاربرد قرار دهید، باعث ایجاد زنجیره‌ای از رندرهای هدر رفته خواهید شد. می‌توانید contextها را برای جداسازی بخش‌های پرنوسان تقسیم کنید، اما در آن نقطه، شما در حال مهندسی دستی راهکارهای جایگزین برای بهینه‌سازی هستید که یک ابزار دیگر از قبل آن‌ها را حل کرده است.

چه زمانی Redux Toolkit جایگاه خود را به دست می‌آورد

Redux Toolkit برای اپلیکیشن‌هایی طراحی شده است که وضعیت (state) در آن‌ها پیچیده است، به‌روزرسانی‌ها مکرر هستند و چندین ویژگی دور از هم نیاز دارند بدون برخورد با یکدیگر، داده‌های یکسانی را بخوانند و بنویسند. یک سبد خرید را در نظر بگیرید. کاربر آیتمی را از یک کارت محصول اضافه می‌کند. آیکون سبد خرید در هدر باید تعداد آیتم‌های خود را به‌روز کند. یک سایدبار باز می‌شود تا موارد لیست را نشان دهد. یک ورودی کد تخفیف، اعتبار‌سنجی را انجام می‌دهد. صفحه تسویه حساب نیز بعداً محتویات سبد خرید را می‌خواند. آن وضعیت توسط کامپوننت‌های بی‌ارتباط در سراسر درخت تحت تأثیر قرار می‌گیرد و اغلب تغییر می‌کند.

Redux Toolkit این مشکل را از طریق یک استور متمرکز و اسلایس‌های (slices) صریح از وضعیت حل می‌کند. کامپوننت‌ها با استفاده از useSelector تنها به بخش‌های کوچکی از داده‌ها که نیاز دارند، مشترک می‌شوند. اگر قیمت سهام در یک داشبورد بلادرنگ (real-time) به‌روز شود، کامپوننتی که تنظیمات پروفایل کاربر را نمایش می‌دهد، بیدار نمی‌شود. Redux در پشت صحنه از بررسی‌های برابری مرجع (reference equality checks) استفاده می‌کند تا اشتراک‌ها دقیق و جزئی (granular) باشند. این موضوع زمانی که تعداد کامپوننت‌های شما به صدها عدد می‌رسد، حیاتی می‌شود.

Redux همچنین یک جریان داده قابل پیش‌بینی به شما می‌دهد. تغییرات وضعیت از طریق اکشن‌های (actions) ارسال شده که توسط ردیوسرها (reducers) مدیریت می‌شوند، رخ می‌دهد. شاید این اصطلاحات فنی به نظر برسند، اما در عمل به این معناست که می‌توانید در کد خود به دنبال addToCart بگردید و تمام مسیرهای کدی که سبد خرید را تغییر می‌دهند، پیدا کنید. در یک تیم بزرگ، این قرارداد از بروز باگ‌ها جلوگیری می‌کند. در مقابل، Context فقط یک مقدار و یک setter است. هر مصرف‌کننده‌ای می‌تواند setState را فراخوانی کند و ردیابی منشأ یک مقدار اشتباه به معنای قرار دادن breakpoint در چندین کامپوننت مختلف است.

جایی که آن‌ها واقعاً از هم جدا می‌شوند

ویژگی‌های عملکردی بیش از هر چیز دیگری این ابزارها را از هم متمایز می‌کند. Context یک مقدار جدید را بدون قید و شرط به تمام مصرف‌کنندگان (consumers) مخابره می‌کند. Redux فقط مشترکانی را مطلع می‌کند که بخش (slice) انتخابی‌شان تغییر کرده باشد. اگر در حال ساخت یک داشبورد بورس بلادرنگ هستید که در آن قیمت‌ها هر ثانیه به‌روز می‌شوند، Context باعث ایجاد طوفانی از رندر مجدد (re-render) سراسری می‌شود. Redux اجازه می‌دهد فقط سلول تیکر و نمودار sparkline دوباره محاسبه شوند.

عیب‌یابی (Debugging) حوزه دیگری است که Redux در اپلیکیشن‌های پیچیده پیشتاز است. Redux DevTools قابلیت عیب‌یابی در زمان (time-travel debugging) را به شما می‌دهد. می‌توانید در هر اکشنِ ارسال‌شده (dispatched action) به عقب برگردید و بازگشت وضعیت (state rewind) را تماشا کنید. در یک جریان پرداخت چند مرحله‌ای شامل محاسبات حمل‌ونقل، تأیید پرداخت و بازیابی خطا، توانایی بازپخش دقیق توالی‌ای که منجر به یک باگ شده است، بسیار ارزشمند است. Context به React DevTools استاندارد متکی است. می‌توانید مقادیر فعلی context را بررسی کنید، اما هیچ لاگ اکشن داخلی یا نمایشگر تفاوت وضعیت (state diff viewer) وجود ندارد. در نهایت مجبور می‌شوید به همان روش قدیمی استفاده از console.log برگردید.

میان‌افزارها (Middleware) و اثرات جانبی (side effects) بخشی از DNA Redux هستند. Redux Toolkit شامل createAsyncThunk است و به‌خوبی با کتابخانه‌های دریافت داده (data-fetching) ادغام می‌شود. می‌توانید یک فراخوانی API را مدیریت کنید، یک نشانگر بارگذاری (loading spinner) نمایش دهید، خطای شبکه را هندل کنید و نتیجه را کش کنید، همگی در جریان داده‌های Redux. Context هیچ الگوی داخلی برای منطق ناهمگام (asynchronous) ارائه نمی‌دهد. یا باید داده‌ها را داخل کامپوننت‌ها دریافت کرده و سپس نتیجه را به Context بفرستید، یا Providerها را در ابزارهای ناهمگام دست‌ساز خودتان قرار دهید. این روش کار می‌کند، اما موردی (ad hoc) است.

هزینه راه‌اندازی (Setup cost) جایی است که Context به‌راحتی پیروز می‌شود. ساخت یک theme provider حدود پنج دقیقه زمان می‌برد. Redux Toolkit مستلزم ایجاد یک فایل store، تعریف sliceها و قرار دادن اپلیکیشن در یک Provider است. این دیگر آن مراسم هفته‌طولانی که در Redux قدیمی با کوهی از کدهای تکراری (boilerplate) بود نیست، اما همچنان نسبت به Context نیاز به تنظیمات بیشتری دارد. برای یک پروژه جانبی آخر هفته یا داشبوردی با سه مسیر (route)، این سربار ممکن است ارزشش را نداشته باشد.

استفاده از هر دو در یک اپلیکیشن

لازم نیست به هیچ‌کدام از این دو جبهه وفادار بمانید. بسیاری از اپلیکیشن‌های عملیاتی از Context برای مسائل مربوط به پوسته UI سراسری و از Redux برای داده‌های تجاری سنگینِ دامنه (domain-heavy business data) استفاده می‌کنند. یک الگوی رایج این است که theme، locale و شاید یک پرچم احراز هویت (auth flag) سبک را در Context نگه دارید، زیرا هر مسیر به آن‌ها نیاز دارد و به‌ندرت تغییر می‌کنند. در همین حال، سیستم مدیریت سفارش، مرکز اعلان‌ها و جداول داده در Redux قرار می‌گیرند، جایی که به‌روزرسانی‌های مکرر و منطق بین‌کامپوننتی نیازمند کنترل دقیق هستند.

این رویکرد ترکیبی، مسائل ساده را ساده نگه می‌دارد بدون اینکه یک Redux store کامل را به یک شیء استاتیکِ theme تحمیل کند. همچنین از پر شدن sliceهای Redux شما با جزئیات ظاهری UI (UI chrome) که از ابتدا هرگز به مدیریت وضعیت در سطح صنعتی نیاز نداشتند، جلوگیری می‌کند.

نتیجه‌گیری نهایی

انتخاب ابزار سنگین‌تر، افتخاری محسوب نمی‌شود. با بررسی این موارد شروع کنید: وضعیت شما هر چند وقت یک‌بار تغییر می‌کند، چه تعداد کامپوننت با آن درگیر هستند و آیا نیاز دارید تغییرات (mutations) را در مرزهای تیم دنبال کنید یا خیر. اگر در یک اپلیکیشن با اندازه متوسط، در حال مدیریت مقادیر با تغییرات کم و مشترک هستید، Context احتمالاً کافی است. اگر وضعیت شما به‌طور مکرر تغییر می‌کند، ویژگی‌های بی‌ارتباط را در بر می‌گیرد و به یک ردپای بازرسی (audit trail) شفاف نیاز دارد، Redux Toolkit شما را از دردسر نجات می‌دهد.

بر اساس ساختار پروژه خود انتخاب کنید، نه بر اساس سخنرانی‌های کنفرانس یا ستاره‌های GitHub. یک سبد خرید که به پنجاه قلم کالا می‌رسد، به‌طور خودکار نیازمند Redux نیست و یک سوئیچ تغییر تم (theme toggle) نیز به یک store سراسری نیاز ندارد. ابزار را با مسئله مطابقت دهید، تا کد شما مدت‌ها پس از فروکش کردن موج هیجانات، قابل نگهداری باقی بماند.