Context و Re-render: چه زمانی مصرف‌کنندگان دوباره رندر می‌شوند

در دو درس قبلی دیدیم چگونه با Provider و useContext، یک مقدار را بدون Prop Drilling در اختیار کامپوننت‌های مختلف قرار دهیم. اما یک سؤال مهم درباره عملکرد باقی می‌ماند: وقتی مقدار یک Context تغییر می‌کند، دقیقاً چه کامپوننت‌هایی دوباره رندر می‌شوند؟ این درس به همین سؤال می‌پردازد.

قانون اصلی: تغییر value باعث رندر همه مصرف‌کنندگان می‌شود

وقتی مقدار value روی یک Provider تغییر می‌کند، React تمام کامپوننت‌هایی را که با useContext از همان Context استفاده می‌کنند، دوباره رندر می‌کند؛ صرف‌نظر از این‌که آن کامپوننت‌ها چقدر عمیق در درخت قرار داشته باشند.

const CountContext = createContext(0);

function App() {
  const [count, setCount] = useState(0);

  return (
    <CountContext.Provider value={count}>
      <Toolbar />
      <button onClick={() => setCount(count + 1)}>افزایش</button>
    </CountContext.Provider>
  );
}

با هر کلیک روی دکمه، count تغییر می‌کند و در نتیجه، هر کامپوننتی که در هر عمقی داخل Toolbar، از useContext(CountContext) استفاده کرده باشد، دوباره رندر خواهد شد؛ حتی اگر آن کامپوننت خودش هیچ Prop یا Stateی نداشته باشد.

مشکل: یک Object بزرگ به‌عنوان value

یک اشتباه رایج، قرار دادن چند مقدار مختلف در یک Object واحد و دادن آن به value است:

function App() {
  const [user, setUser] = useState({ name: "سارا" });
  const [theme, setTheme] = useState("light");

  return (
    <AppContext.Provider value={{ user, theme, setUser, setTheme }}>
      <Page />
    </AppContext.Provider>
  );
}

همان‌طور که در فصل ششم، درباره Objectهای تازه‌ساخته‌شده در هر رندر دیدیم، این Object داخل value در هر رندر App، یک آبجکت کاملاً جدید است. نتیجه این است که با تغییر هر یک از این مقادیر (مثلاً فقط theme)، همه مصرف‌کنندگانی که از این Context استفاده می‌کنند دوباره رندر می‌شوند؛ حتی کامپوننت‌هایی که فقط به user نیاز دارند و اصلاً کاری با theme ندارند.

راه‌حل: تفکیک Contextهای مستقل

راه‌حل رایج، جدا کردن مقادیری که مستقل از هم تغییر می‌کنند، به Contextهای جداگانه است:

function App() {
  const [user, setUser] = useState({ name: "سارا" });
  const [theme, setTheme] = useState("light");

  return (
    <UserContext.Provider value={user}>
      <ThemeContext.Provider value={theme}>
        <Page />
      </ThemeContext.Provider>
    </UserContext.Provider>
  );
}

حالا با تغییر theme، فقط کامپوننت‌هایی که از ThemeContext استفاده می‌کنند دوباره رندر می‌شوند؛ کامپوننت‌هایی که فقط به UserContext وابسته‌اند، بدون تغییر باقی می‌مانند.

راه‌حل مکمل: پایدار نگه‌داشتن Object با useMemo

اگر واقعاً نیاز به گروه‌بندی چند مقدار در یک value وجود دارد، می‌توان با useMemo از ساخته‌شدن یک Object جدید در هر رندر جلوگیری کرد؛ روشی که با جزئیات کامل آن در فصل سیزدهم آشنا خواهیم شد.

یک نکته مهم: Context برای به‌روزرسانی‌های بسیار مکرر مناسب نیست

چون تغییر value باعث رندر همه مصرف‌کنندگان می‌شود، Context برای داده‌ای که با فرکانس بسیار بالا تغییر می‌کند (مثلاً موقعیت لحظه‌ای موس)، انتخاب مناسبی نیست؛ چون می‌تواند باعث رندرهای مکرر و پرهزینه در بخش‌های زیادی از برنامه شود. Context بیشتر برای داده‌ای مناسب است که نسبتاً کم‌تر تغییر می‌کند، مانند کاربر وارد‌شده یا تم برنامه.

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

تفکیک Contextهای مستقل برای جلوگیری از رندر اضافه
برای دیدن پیش‌نمایش کامپوننت، روی Run بزنید.

جمع‌بندی

تغییر مقدار value یک Provider، همه کامپوننت‌هایی که از همان Context با useContext استفاده می‌کنند را دوباره رندر می‌کند. قرار دادن چند مقدار مستقل در یک Object واحد، می‌تواند باعث رندرهای غیرضروری شود؛ راه‌حل رایج، تفکیک به Contextهای جداگانه یا پایدار نگه‌داشتن Object با useMemo است. در درس بعدی به مقایسه Context با Prop Drilling می‌پردازیم.