هر توسعهدهنده React در نهایت با یک مانع مشابه روبرو میشود. شما یک شیء کاربر (user object) را در کامپوننت سطح بالای App خود دریافت میکنید. سپس آن را به پایین پاس میدهید. و باز هم پایینتر. از یک پوشش مسیر (route wrapper)، از یک پوسته چیدمان (layout shell)، از یک کانتینر سایدبار، فقط برای اینکه یک کامپوننت کوچک آواتار که سه لایه پایینتر قرار دارد بتواند عکس پروفایل را نمایش دهد. کامپوننتهای میانی هیچ اهمیتی به آن شیء کاربر نمیدهند. آنها فقط در حال انتقال یک بسته هستند. این همان Prop Drilling است، و این کار یک درخت کامپوننت تمیز را به یک بازی کلافهکننده از «تلفنخانه» تبدیل میکند.
درد اصلی زمانی شروع میشود که ساختار آن داده تغییر میکند. مثلاً ممکن است بکاِند به جای user.avatar شروع به استفاده از ساختار تو در تو user.profile.avatar کند. ناگهان شما مجبور میشوید اینترفیسهای TypeScript یا PropTypes را در پنج فایل مختلف که خودشان هرگز از آن داده استفاده نمیکنند، ویرایش کنید. اینجاست که React Context API وارد عمل میشود.
چگونه Context جریان داده را بازسازی میکند
Context را مانند یک روتر وایفای تصور کنید که در مرکز خانه شما قرار دارد. بدون آن، برای رساندن سیگنال به لپتاپ خود، نیاز به کابلهای اترنت داشتید که از هر اتاقی عبور میکردند. با وجود آن، روتر سیگنال را در هوا پخش میکند و هر دستگاهی که رمز عبور صحیح را داشته باشد، میتواند مستقیماً متصل شود. دیوارها اهمیتی ندارند.
در اصطلاحات React، ریشه اپلیکیشن شما میتواند دادهها را از طریق درخت کامپوننتها پخش کند، بدون اینکه از هر لایه بخواهد نقش پیک را ایفا کند. هر کامپوننت تودرتو میتواند مشترک آن پخش باشد و دقیقاً همان چیزی را که نیاز دارد دریافت کند.
سه بخش اصلی
Context API به سه بخش متحرک خلاصه میشود.
React.createContext() کانال پخش را راهاندازی میکند. این تابع شیئی شامل یک Provider و (در کدهای قدیمیتر) یک Consumer را برمیگرداند. شما فقط نیاز دارید این را یک بار برای یک قابلیت خاص فراخوانی کنید.
The Provider کامپوننتی است که بخشی از درخت شما را در بر میگیرد. این کامپوننت یک پراپ به نام value میپذیرد. هر چیزی که در آن پراپ قرار دهید، برای تمام فرزندان (هر چقدر هم که در عمق درخت باشند) در دسترس خواهد بود.
useContext هوکی (Hook) است که به یک کامپوننت تابعی اجازه میدهد به آن پخش متصل شود. داخل کامپوننت خود، شیء context که ساختهاید را به useContext پاس میدهید و این هوک، مقدار فعلی را برمیگرداند. همین و بس. بدون نیاز به پوششهای اضافی یا پراپهای بیشتر.
قبل از آمدن هوکها، مجبور بودید از الگوی Consumer با render props استفاده کنید. این روش کار میکرد، اما باعث ایجاد تورفتگیهای زیاد و شلوغی در کد میشد. useContext تمام آنها را به یک خط واحد در بدنه تابع شما تبدیل کرد.
چه زمانی استفاده از Context واقعاً منطقی است
از روی عادت به سراغ Context نروید. این ابزار برای دادههایی ساخته شده است که بسیاری از کامپوننتهای بیارتباط، در شاخههای مختلف درخت شما از آنها استفاده میکنند. موارد مناسب عبارتند از:
- تنظیمات تم (Theme settings): نه فقط حالت روشن یا تاریک، بلکه توکنهای فاصلهگذاری، پالتهای رنگی و مقیاسهای فونت. انتقال دستی این موارد از طریق هر دکمه و مودالِ استایلگذاری شده، خیلی زود خستهکننده میشود.
- احراز هویت کاربر: وضعیت ورود، آرایهای از مجوزها یا شیء کاربر فعلی. نوار هدر، یک ویجت داشبورد و یک محافظ مسیر خصوصی (private route guard) ممکن است همگی در گوشههای مختلف درخت قرار داشته باشند.
- تنظیمات زبان: رشتههای Locale، فرمت تاریخ و نمادهای ارز. کامپوننتهای لایه پایین مثل برچسبهای فرم (form labels) به این موارد نیاز دارند، بدون اینکه هر والد در مسیر آنها باید از آن باخبر باشد.
- دادههای سبد خرید: تعداد آیتمها، ارزش کل و توابع افزودن به سبد خرید. نشانگر هدر و صفحه تسویه حساب به یک وضعیت (state) یکسان نیاز دارند، اما معمولاً زیر شاخههای کاملاً متفاوتی از چیدمان قرار میگیرند.
یک سوئیچر تم کاربردی
یکی از واضحترین راهها برای مشاهده عملکرد Context، استفاده از یک سوئیچ تم (theme toggle) است. در اینجا نحوه پیادهسازی آن را بدون نادیده گرفتن جزئیات مهم مشاهده میکنید.
ابتدا، یک فایل ThemeContext.js بسازید. React.createContext() را فراخوانی کرده و نتیجه را ذخیره کنید. سپس یک کامپوننت ThemeProvider بسازید که تم فعلی را با useState یا useReducer مدیریت میکند. فرزندان را در Provider مربوط به context خود قرار دهید و شیئی شامل تم فعلی و تابعی برای تغییر آن را پاس دهید. هم ThemeProvider و هم خودِ شیء context را اکسپورت کنید.
دوم، به نقطه ورود اپلیکیشن خود بروید. ThemeProvider را وارد (import) کرده و کل اپلیکیشن خود را با آن بپوشانید. اگر این مرحله را انجام ندهید، هر چیزی که بخواهد بعداً context را بخواند، فقط مقدار پیشفرض را خواهد دید.
سوم، داخل یک کامپوننت Header یا Content ، شیء context و useContext را وارد کنید. هوک را فراخوانی کنید، تم و تابع تغییر دهنده را از آن استخراج (destructure) کنید و کلاسهای CSS خود را به صورت شرطی اعمال کنید. دکمهای اضافه کنید که تابع تغییر دهنده را فراخوانی کند. این کامپوننت هرگز پراپ theme را از والد خود دریافت نمیکند؛ بلکه سیگنال را مستقیماً از هوا دریافت میکند.
Prop Drilling، Context یا Redux؟
انتخاب بین این ابزارها کمتر به وفاداری مربوط میشود و بیشتر به ساختار وضعیت (state) شما بستگی دارد.
Prop drilling برای دو یا سه سطح عمق کاملاً مناسب است. این روش صریح است، ردیابی آن در IDE آسان است و وابستگیها را شفاف نگه میدارد. مشکلات زمانی ظاهر میشوند که شروع به عبور دادن یک prop یکسان از شش یا هفت لایه کنید.
Context API همراه با خود React ارائه میشود. این یعنی بدون حجم اضافی در باندل (bundle size) و بدون نیاز به تنظیمات خارجی. این ابزار مدیریت وضعیتهای سراسری کوچک تا متوسط را به زیبایی انجام میدهد، بهویژه دادههایی که به ندرت تغییر میکنند، مانند تمها یا پروفایلهای کاربری.
Redux مستلزم نصب کتابخانههای اضافی و نوشتن کدهای تکراری (boilerplate) است. زمانی که منطق وضعیت شما پیچیده باشد، زمانی که بخشهای مختلف وضعیت (state slices) به شکلی عمیق با هم تعامل داشته باشند، یا زمانی که به قابلیت time-travel debugging و middleware نیاز داشته باشید، استفاده از آن ارزشش را دارد. برای دادههای سراسری ساده، Redux بیش از حد نیاز (overkill) است.
واقعیت عملکردی که هیچکس درباره آن صحبت نمیکند
نکتهای که پیادهسازیهای سطح جونیور (junior) را از سطح سنیور (senior) متمایز میکند اینجاست. وقتی مقدار یک Context Provider تغییر میکند، هر کامپوننتی که از آن context استفاده میکند، دوباره رندر (re-render) میشود. فرقی نمیکند که آن بخش خاص که کامپوننت به آن اهمیت دارد، تغییری نکرده باشد. React مرجع (reference) جدید را میبیند و یک بهروزرسانی را برنامهریزی میکند.
اگر تمام وضعیت اپلیکیشن خود را در یک StoreContext غولآسا بریزید، در واقع کل رابط کاربری (UI) خود را به هم چسباندهاید. تغییر یک تنظیمات تم، سبد خرید، نمودارهای داشبورد و لیست اعلانهای شما را دوباره رندر میکند. این یک کار غیرضروری است.
contextهای خود را بر اساس دامنه (domain) تقسیم کنید. یک ThemeContext برای تنظیمات ظاهری، یک UserContext برای دادههای پروفایل و یک CartContext برای وضعیت خرید نگه دارید. اگر کاربری نام نمایشی خود را ویرایش کند، هدر شما بدون دست زدن به شبکه محصولات بهروز میشود. همچنین، مراقب باشید چه چیزی را به propِ value در Provider پاس میدهید. اگر یک شیء مستقیم (object literal) مانند { theme, toggleTheme } را در حین رندر پاس دهید، در هر بار رندر یک مرجع جدید ایجاد میکنید و باعث بهروزرسانیهای بیمورد میشوید. اگر مقدار شامل توابع یا دادههای غیرپریمیتیو (non-primitive) است، آن ساختار را با useMemo تثبیت کنید.
اشتباهاتی که ساعتها وقت تلف میکنند
دو خطا بارها و بارها تیمها را گرفتار میکنند.
فراموش کردن اکسپورت کردن شیء context. بسیار ساده است که کامپوننت ThemeProvider را اکسپورت کنید و سپس سعی کنید useContext(ThemeProvider) را فراخوانی کنید. اما کار اینگونه نیست. این Hook به شیء context که توسط createContext بازگردانده شده نیاز دارد، نه به کامپوننتِ پوششدهنده (wrapper component). اگر فقط Provider را اکسپورت کنید، مصرفکنندگان (consumers) چیزی برای import کردن نخواهند داشت.
فراخوانی useContext خارج از Provider آن. این Hook همان مقدار پیشفرضی را برمیگرداند که به createContext پاس دادهاید. اگر مقدار پیشفرضی پاس نداده باشید، مقدار undefined دریافت میکنید. اگر درخت کامپوننت شما، مصرفکننده را در جای بالاتری از DOM نسبت به Provider رندر کند، یا اگر Provider کلاً وجود نداشته باشد، دادههای شما به سادگی نخواهند رسید. حتماً بررسی کنید که فایل index یا root شما واقعاً اپلیکیشن را محصور (wrap) کرده باشد.
نتیجهگیری نهایی
React Context یک انقلاب در مدیریت وضعیت نیست. بلکه ابزاری هدفمند برای یک مشکل مکانی خاص است: رساندن دادهها به کامپوننتهای دوردست بدون اینکه هر لایه را به یک اداره پست تبدیل کنید. از آن برای دادههای واقعاً سراسری استفاده کنید، contextهای خود را بر اساس دامنه تقسیم کنید تا از عملکرد رندر محافظت شود، و همیشه قبل از تلاش برای خواندن سیگنال، درخت خود را با Provider صحیح محصور کنید. با رعایت این عادتها، درختهای کامپوننت شما تمیز، سریع و قابل درک باقی میمانند.
