Render Phase و Commit Phase: نگاهی دقیق‌تر به دو مرحله اصلی

در درس قبل، Render و Commit را به‌عنوان دو مرحله جدا معرفی کردیم. در این درس، هرکدام از این دو مرحله را با جزئیات بیشتری بررسی می‌کنیم و می‌بینیم دقیقاً چه کارهایی در هرکدام مجاز است و چه کارهایی نباید در آن‌ها انجام شود.

Render Phase: فقط محاسبه، بدون عوارض جانبی

در طول Render Phase، React تابع کامپوننت شما و تمام کامپوننت‌های فرزندش را اجرا می‌کند تا درخت جدیدی از عناصر JSX بسازد. این مرحله باید Pure باشد؛ یعنی تابع کامپوننت نباید در طول اجرای خودش، چیزی بیرون از خودش را تغییر دهد.

// نادرست: تغییر یک متغیر بیرونی در حین Render
let renderCount = 0;

function Counter() {
  renderCount = renderCount + 1; // عارضه جانبی در حین Render
  return <p>{renderCount}</p>;
}

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

چه چیزهایی در Render Phase مجاز نیستند

به همین دلیل، کارهایی مانند این‌ها نباید مستقیماً در بدنه تابع کامپوننت (یعنی در حین Render) انجام شوند:

  • تغییر مستقیم یک متغیر یا آبجکت بیرون از کامپوننت
  • فراخوانی setState بدون شرط، به شکلی که حلقه بسازد (همان مشکلی که در فصل ششم دیدیم)
  • خواندن یا نوشتن روی ref.current یک عنصر DOM (چون هنوز Commit نشده)
  • ارسال درخواست به سرور یا تنظیم تایمر (این‌ها باید داخل Effect باشند)

کارهایی مانند خواندن Props و State فعلی، محاسبه مقادیر Derived (که در فصل سوم دیدیم)، و ساختن JSX، دقیقاً همان کارهایی هستند که Render Phase برای آن‌ها طراحی شده است.

Commit Phase: اعمال تغییر و اجرای Effectها

وقتی React خروجی جدید را محاسبه کرد و آن را با خروجی قبلی مقایسه کرد، وارد Commit Phase می‌شود. در این مرحله، React واقعاً عناصر DOM را می‌سازد، به‌روزرسانی می‌کند یا حذف می‌کند. بلافاصله پس از این تغییرات، مقدار ref.current برای عناصر جدید تنظیم می‌شود و سپس Effectهایی که در فصل ششم دیدیم، اجرا می‌شوند.

function SearchBox() {
  const inputRef = useRef(null);

  useEffect(() => {
    // این کد پس از Commit اجرا می‌شود؛ عنصر واقعاً در DOM وجود دارد
    inputRef.current.focus();
  }, []);

  return <input ref={inputRef} />;
}

چرا این ترتیب طراحی شده

جدا کردن این دو مرحله، به React اجازه می‌دهد محاسبه خروجی جدید (Render) را از تغییر واقعی صفحه (Commit) مستقل نگه دارد. این استقلال پایه‌ای است برای قابلیت‌هایی مانند Concurrent Rendering که در فصل بیست‌ودوم با جزئیات بیشتری بررسی می‌شود؛ جایی که React ممکن است یک محاسبه Render را شروع کند، متوقف کند یا حتی کاملاً دور بریزد، بدون اینکه هیچ اثری روی صفحه واقعی باقی بماند، چون هنوز به Commit نرسیده است.

یک قاعده عملی برای تشخیص

اگر کدی که می‌نویسید، فقط بر اساس Props و State فعلی، یک مقدار محاسبه می‌کند و هیچ اثری بیرون از خودش ندارد، جایش در بدنه اصلی کامپوننت (Render Phase) است. اگر کدی نیاز دارد چیزی بیرون از React را تغییر دهد یا به DOM واقعی دسترسی داشته باشد، باید در Event Handler یا Effect (که بخشی از Commit Phase و پس از آن هستند) نوشته شود.

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

در این مثال، تفاوت زمان‌بندی بین چیزی که در حین Render محاسبه می‌شود و چیزی که پس از Commit در Effect اجرا می‌شود، در کنسول قابل مشاهده است.

ترتیب اجرای Render در برابر Effect پس از Commit
برای دیدن پیش‌نمایش کامپوننت، روی Run بزنید.

جمع‌بندی

Render Phase باید Pure باشد و فقط خروجی JSX را از روی State و Props محاسبه کند، بدون تغییر چیزی بیرون از خودش و بدون دسترسی به DOM واقعی. Commit Phase جایی است که React واقعاً DOM را تغییر می‌دهد و بلافاصله پس از آن، Effectها اجرا می‌شوند. در درس بعدی به مفهوم Component Tree و نقش آن در این دو مرحله می‌پردازیم.