چرخه حیات یک Request: از شروع تا پایان

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

مرحله اول: تصمیم به شروع درخواست

یک درخواست معمولاً به یکی از دو شکل آغاز می‌شود: به‌خاطر یک Effect که با Mount شدن کامپوننت یا تغییر یک Dependency اجرا می‌شود (مثل مثال‌های دو درس قبل)، یا به‌خاطر یک Event Handler مستقیم، مثل کلیک روی دکمه «ارسال» یک فرم. همان‌طور که در فصل ششم دیدیم، تشخیص درست این‌که کدام مورد است، مهم است: اگر درخواست باید با وجود یک مقدار همگام بماند، جایش Effect است؛ اگر باید دقیقاً به‌خاطر یک عمل کاربر رخ دهد، جایش Event Handler است.

مرحله دوم: اعلام شروع (State بارگذاری)

بلافاصله پیش از ارسال درخواست واقعی، State بارگذاری باید فعال شود تا رابط کاربری بازخورد فوری نشان دهد:

useEffect(() => {
  setLoading(true);
  setError(null);

  fetchData(id).then(/* ... */);
}, [id]);

پاک کردن error در همین لحظه هم مهم است؛ وگرنه اگر درخواست قبلی با خطا مواجه شده بود، آن پیام خطا تا رسیدن نتیجه جدید روی صفحه باقی می‌ماند و گمراه‌کننده می‌شود.

مرحله سوم: انتظار برای پاسخ

در این بازه، درخواست در پس‌زمینه مرورگر در حال اجراست و React کاری جز نمایش State فعلی (مثلاً پیام بارگذاری) انجام نمی‌دهد. طول این بازه، به سرعت شبکه و سرور بستگی دارد و کاملاً خارج از کنترل کامپوننت است؛ دقیقاً همان دلیلی که در فصل ششم، این نوع عملیات را «همگام‌سازی با یک سیستم بیرونی» نامیدیم.

مرحله چهارم: رسیدن پاسخ (موفق یا ناموفق)

وقتی پاسخ می‌رسد، یکی از این دو مسیر طی می‌شود:

fetchData(id)
  .then((data) => {
    setResult(data);
  })
  .catch((err) => {
    setError(err.message);
  })
  .finally(() => {
    setLoading(false);
  });

استفاده از finally برای خاموش کردن loading، مهم است؛ چون صرف‌نظر از موفق یا ناموفق بودن درخواست، این مرحله باید همیشه اتفاق بیفتد.

مرحله پنجم: نقطه‌ای که کامپوننت ممکن است دیگر وجود نداشته باشد

بین زمان شروع درخواست و زمان رسیدن پاسخ، ممکن است کامپوننت از صفحه حذف شده باشد (کاربر به صفحه دیگری رفته) یا id دوباره عوض شده باشد. در هر دو حالت، اعمال نتیجه یک درخواست قدیمی روی State فعلی، اشتباه است؛ همان Race Condition که در فصل ششم با جزئیات کامل بررسی کردیم و راه‌حل آن (پرچم ignore در Cleanup) را در درس بعدی همین فصل دوباره و در بافت دریافت داده مرور می‌کنیم.

چرخه کامل، به‌شکل خلاصه

تصمیم به شروع (Effect یا Event)
   ↓
فعال‌سازی Loading، پاک کردن Error قبلی
   ↓
ارسال درخواست و انتظار (خارج از کنترل React)
   ↓
پاسخ می‌رسد: موفق → setResult   |   ناموفق → setError
   ↓
خاموش کردن Loading (در هر دو حالت)
   ↓
بررسی اعتبار: آیا این هنوز همان درخواست مرتبط فعلی است؟

چرا دیدن این چرخه به‌شکل کامل مفید است

بسیاری از باگ‌های رایج در دریافت داده (پیام بارگذاری که هیچ‌وقت خاموش نمی‌شود، خطای قدیمی که پاک نمی‌شود، یا داده‌ای که به کامپوننت اشتباه می‌رسد) از نادیده گرفتن یکی از همین مراحل می‌آیند. داشتن این چرخه کامل در ذهن، هنگام نوشتن یا بررسی کد دریافت داده، کمک می‌کند هیچ مرحله‌ای فراموش نشود.

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

در این مثال، هر مرحله چرخه در کنسول چاپ می‌شود تا ترتیب دقیق آن‌ها قابل مشاهده باشد.

مشاهده چرخه کامل یک Request در کنسول
برای دیدن پیش‌نمایش کامپوننت، روی Run بزنید.

جمع‌بندی

یک درخواست کامل، این مراحل را طی می‌کند: شروع، فعال‌سازی Loading و پاک کردن خطای قبلی، انتظار، رسیدن پاسخ (موفق یا ناموفق)، خاموش کردن Loading، و در نهایت بررسی این‌که آیا نتیجه هنوز مربوط به وضعیت فعلی کامپوننت است. فراموش کردن هرکدام از این مراحل، منبع رایج باگ در دریافت داده است. در درس بعدی، مستقیماً به Race Condition در بافت دریافت داده برمی‌گردیم.