طراحی Context در برنامه‌های بزرگ

در درس‌های قبلی این فصل، اصول پایه Context و موقعیت‌های مناسب استفاده از آن را دیدیم. در این درس آخر فصل هشتم، به سراغ چند اصل طراحی می‌رویم که وقتی تعداد Contextها و اندازه برنامه بزرگ‌تر می‌شود، اهمیت بیشتری پیدا می‌کنند.

الگوی رایج: بسته‌بندی Context در یک Provider اختصاصی

به‌جای این‌که در فایل اصلی برنامه، مستقیماً با createContext، useState و Provider سروکار داشته باشیم، رایج است که هر Context، همراه با منطق مربوط به خودش، در یک کامپوننت Provider اختصاصی بسته‌بندی شود:

const AuthContext = createContext(null);

function AuthProvider({ children }) {
  const [user, setUser] = useState(null);

  function login(name) {
    setUser({ name });
  }

  function logout() {
    setUser(null);
  }

  return (
    <AuthContext.Provider value={{ user, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

function useAuth() {
  return useContext(AuthContext);
}

با این ساختار، تمام State و توابع مرتبط با احراز هویت، در یک‌جای واحد (AuthProvider) نگه‌داری می‌شوند و بقیه برنامه فقط با <AuthProvider> و Custom Hook همراه آن (useAuth) سروکار دارد، بدون نیاز به دانستن جزئیات داخلی.

ترکیب چند Provider در ریشه برنامه

در برنامه‌های بزرگ‌تر که چند Context مستقل دارند (مثلاً Auth، Theme، Language)، معمولاً همه این Providerها در بالاترین سطح ممکن، دور کل برنامه قرار می‌گیرند:

function App() {
  return (
    <AuthProvider>
      <ThemeProvider>
        <LanguageProvider>
          <MainApp />
        </LanguageProvider>
      </ThemeProvider>
    </AuthProvider>
  );
}

این ساختار تودرتو، هرچند در نگاه اول کمی شلوغ به نظر می‌رسد، اما هر Provider مسئولیت مستقل خودش را دارد و ترتیب آن‌ها، معمولاً بر اساس وابستگی منطقی بین آن‌ها تعیین می‌شود (مثلاً اگر ThemeProvider به اطلاعات کاربر برای تعیین تم پیش‌فرض نیاز داشته باشد، باید داخل AuthProvider قرار گیرد).

جدا نگه‌داشتن مقدار از تابع تغییردهنده، در صورت نیاز

در Contextهایی که هم مقدار و هم توابع تغییردهنده را در خود دارند، گاهی برای کاهش رندرهای غیرضروری، مقدار و توابع تغییردهنده در دو Context جداگانه قرار می‌گیرند؛ چون توابع تغییردهنده (مانند setUser) معمولاً هرگز تغییر نمی‌کنند، ولی مقدار (user) ممکن است مرتب عوض شود. این تفکیک، ادامه همان اصلی است که در درس Context و Re-render دیدیم: جدا کردن داده‌هایی که با فرکانس متفاوت تغییر می‌کنند.

مستندسازی و محدود کردن دسترسی مستقیم

یک عادت خوب دیگر، صادر نکردن مستقیم خودِ Context از یک فایل، و صادر کردن فقط Provider و Custom Hook مربوط به آن است. این کار باعث می‌شود همه کد بیرونی، مجبور شود از طریق useAuth() (نه useContext(AuthContext) مستقیم) به مقدار دسترسی پیدا کند؛ و اگر روزی لازم شد یک بررسی اضافه انجام شود (مثلاً پرتاب خطا در صورت استفاده بیرون از Provider)، فقط کافی است همان یک Custom Hook تغییر کند.

function useAuth() {
  const context = useContext(AuthContext);

  if (context === null) {
    throw new Error("useAuth باید داخل AuthProvider استفاده شود");
  }

  return context;
}

مثال قابل اجرا

یک AuthProvider کامل با Custom Hook اختصاصی
برای دیدن پیش‌نمایش کامپوننت، روی Run بزنید.

جمع‌بندی

در برنامه‌های بزرگ، Context معمولاً در قالب یک Provider اختصاصی، همراه با State و منطق مربوط به خودش و یک Custom Hook برای مصرف آن، سازمان‌دهی می‌شود. چند Provider مستقل می‌توانند در ریشه برنامه ترکیب شوند، و جدا کردن مقدار از تابع تغییردهنده، در صورت نیاز، به کاهش رندرهای غیرضروری کمک می‌کند. با این درس، فصل هشتم به پایان می‌رسد و در ادامه به سراغ آزمون فصل ۸ می‌رویم.