توسعه فرانت‌اند

React Suspense و Concurrent Features

تفاوت Suspense و Fetch معمول، مرز Fallback، Transition و خطا؛ نمونه کامل lazy با React و Vite و توضیح محدودیت رفتار.

1404/12/27 4 دقیقه مطالعه 1,138 بازدید

Suspense مرزی برای هماهنگ کردن نمایش محتوایی است که هنوز آماده نشده و از سازوکار پشتیبانی‌شده React استفاده می‌کند. useTransition نیز بعضی Updateها را غیر فوری معرفی می‌کند تا کار فوری کاربر بتواند اولویت مناسب داشته باشد. این دو کار به معنی اجرای محاسبه در Thread دیگر یا تضمین تجربه شبیه برنامه Native نیست. طراحی مرز انتظار، داده قدیمی و پیام وضعیت همچنان به نیاز رابط بستگی دارد و از اضافه کردن یک Wrapper به دست نمی‌آید.

برای مثال فرضی، صفحه توضیح کوتاه و پنل جزئیات دارد. کد پنل می‌تواند هنگام نیاز با lazy دریافت شود و مرز Suspense وضعیت انتظار آن را نشان دهد. اگر کاربر در حال انتخاب مسیر دیگری است، لازم نیست همه صفحه با Spinner جایگزین شود. تصمیم درباره اینکه کدام محتوا باقی بماند و کدام بخش منتظر شود، باید پیش از کد روشن باشد. عنوان قدیمی Concurrent Mode هم نباید به عنوان یک کلید عمومی برای موازی کردن برنامه معرفی شود.

هر دریافت async، Suspense را فعال نمی‌کند

مرجع رسمی Suspense توضیح می‌دهد که دریافت داده داخل Effect یا Event Handler به تنهایی این مرز را فعال نمی‌کند. lazy برای بارگذاری کد و خواندن Promise با سازوکار مناسب از مسیرهای مرتبط هستند. بنابراین قرار دادن fetch در useEffect و پوشاندن Component با Suspense، خودکار Fallback دریافت داده نمی‌سازد. برای داده، باید روش Framework یا قرارداد Cache و Promise مورد استفاده با مدل React هماهنگ باشد.

نمونه مستقل بهتر است یک مسیر روشن مثل lazy را نشان دهد تا Promiseهای بدون قرارداد هنگام Render ساخته نشوند. ساخت Promise تازه در هر Render می‌تواند انتظار تکراری ایجاد کند. در برنامه محصولی، منبع داده و Cache باید مسئولیت مشخص داشته باشند. مقاله Server Components درباره دریافت و انتقال داده در Framework توضیح دارد. آن مسیر را با برنامه Client ساده‌ای که صرفاً یک Module را lazy می‌کند یکسان ندانید.

Boundary را با واحد فهم کاربر انتخاب کنید

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

Fallback باید اندازه و معنی قابل پیش‌بینی داشته باشد. Skeleton با ابعاد نزدیک می‌تواند جابه‌جایی را کم کند، اما نباید محتوای واقعی یا پیشرفت ساختگی القا کند. Spinner همراه توضیح روشن گاهی کافی است. بارگذاری اولیه با تغییر داده در بخشی که قبلاً دیده شده فرق دارد؛ حذف ناگهانی محتوای قبلی می‌تواند کار جاری را قطع کند. مرزهای تودرتو هم باید با ترتیب معنی‌دار نمایش هماهنگ باشند، نه فقط با ساختار Importها.

Transition زمان‌بندی Update است

مستند useTransition isPending و startTransition را معرفی می‌کند. به‌روزرسانی غیر فوری می‌تواند در تعامل با کار فوری مدیریت شود. این ابزار سرعت Query یا محاسبه CPU را خودکار بیشتر نمی‌کند. یک حلقه طولانی هم‌زمان داخل Callback همچنان هزینه خودش را دارد. برای کار مستقل سنگین، Web Workers مسیر متفاوتی است. این دو ابزار را با نام کلی «اجرای پس‌زمینه» به جای یکدیگر معرفی نکنید.

مقدار کنترل‌شده input باید به تایپ پاسخ فوری بدهد و نباید همان State کنترل را به شکل نامناسب در Transition بگذارید. می‌توان نمایش نتیجه وابسته را با روش مناسب از ورودی جدا کرد. isPending وضعیت Transition است، نه گزارش همه درخواست‌های شبکه برنامه. اگر عملیات مهم Server دارید، وضعیت آن باید از قرارداد خود عملیات بیاید. مقاله TypeScript با React درباره حالت‌های ارسال و نتیجه توضیح دارد؛ یک Boolean عمومی pending تمام آن معناها را پوشش نمی‌دهد.

نمونه کامل lazy و تغییر فوری یا Transition

چهار فایل زیر پروژه مستقلی می‌سازند. Node.js سازگار با Vite 7 و npm پیش‌نیازند. package.json و index.html در ریشه و main.jsx و Details.jsx در پوشه src قرار می‌گیرند. npm install و npm run dev را در همان ریشه اجرا و نشانی اعلام‌شده ابزار را باز کنید. نمونه API بیرونی ندارد؛ Module جزئیات از همان پروژه دریافت می‌شود. زمان دیده شدن Fallback به محیط و Cache وابسته است و عدد یا تأخیر ثابت برای آن اعلام نشده است.

{"name":"suspense-demo","version":"1.0.0","private":true,"type":"module","scripts":{"dev":"vite","build":"vite build"},"dependencies":{"react":"^19.0.0","react-dom":"^19.0.0"},"devDependencies":{"vite":"^7.0.0"}}
<!doctype html><html lang="fa" dir="rtl"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>نمونه Suspense</title><style>body{font:1rem/1.8 Tahoma,sans-serif;max-width:44rem;margin:2rem auto;padding:1rem}button{font:inherit;padding:.5rem;margin:.3rem}button:focus-visible{outline:3px solid #111;outline-offset:3px}.panel{border:2px solid #111;padding:1rem;min-height:10rem}</style></head><body><div id="root"></div><script type="module" src="/src/main.jsx"></script></body></html>

کد اصلی، Details را با lazy در سطح ماژول تعریف می‌کند. یک دکمه Update فوری و دکمه دیگر Update با Transition دارد. برای مقایسه دریافت اولیه، هر مسیر را از اجرای تازه صفحه مشاهده کنید؛ پس از دریافت Module، بار دوم همان هزینه اولیه را ندارد. این تفاوت Cache نباید به عنوان اثبات سرعت همگانی Transition تعبیر شود. Error Boundary برای شکست بارگذاری یا Render بخش، پیام جدا دارد؛ Suspense جای مدیریت خطا را نمی‌گیرد.

import React,{Component,Suspense,lazy,useState,useTransition} from 'react';
import {createRoot} from 'react-dom/client';
const Details=lazy(()=>import('./Details.jsx'));
class ErrorBoundary extends Component{state={error:false};static getDerivedStateFromError(){return {error:true}}render(){return this.state.error?<div role="alert"><p>بارگذاری یا نمایش بخش ممکن نشد.</p><button type="button" onClick={()=>location.reload()}>شروع دوباره صفحه</button></div>:this.props.children}}
function App(){const [view,setView]=useState('overview');const [pending,startTransition]=useTransition();return <main><h1>انتظار برای کد جزئیات</h1><p>این مثال داده Backend دریافت نمی‌کند.</p><button type="button" onClick={()=>setView('overview')}>نمای کلی</button><button type="button" onClick={()=>setView('details')}>جزئیات با Update فوری</button><button type="button" onClick={()=>startTransition(()=>setView('details'))}>جزئیات با Transition</button><p role="status">{pending?'در حال آماده‌سازی تغییر':'Update منتظر نیست'}</p><div className="panel"><ErrorBoundary><Suspense fallback={<p role="status">کد بخش جزئیات در حال بارگذاری است.</p>}>{view==='overview'?<section><h2>نمای کلی</h2><p>محتوای پایه پیش از دریافت کد جزئیات دیده می‌شود.</p></section>:<Details/>}</Suspense></ErrorBoundary></div></main>}
createRoot(document.getElementById('root')).render(<App/>);

Details.jsx محتوای آموزشی ساده دارد و default export آن با import پویا هماهنگ است. اگر نام فایل یا Export را عوض کنید، قرارداد lazy را هم اصلاح کنید. فایل داده پنهان یا تصویری لازم نیست. برنامه هیچ دریافت واقعی گزارش از Backend انجام نمی‌دهد و عنوان جزئیات به محتوای محلی اشاره دارد. افزودن API به این نمونه نیازمند مدل انتظار، خطا و Cache همان منبع است؛ نمی‌توان فقط تابع async را اضافه کرد و همه رفتار داده را آماده فرض کرد.

import React from 'react';
export default function Details(){return <section><h2>جزئیات محلی</h2><p>این Component از Module جدا دریافت شده است. زمان نمایش به دریافت کد و Cache بستگی دارد.</p><ul><li>Suspense انتظار را مدیریت می‌کند.</li><li>Error Boundary مسیر خطای این بخش را نشان می‌دهد.</li></ul></section>}

Boundary محتوای فعلی را در بر می‌گیرد و تغییر Transition می‌تواند از پنهان شدن محتوای قبلاً نمایان جلوگیری کند تا بخش تازه آماده شود. وضعیت isPending در بیرون مرز دیده می‌شود. رفتار فوری ممکن است Fallback را برای انتظار اولیه نشان دهد. این توضیح درباره ساختار همین نمونه است و تضمین نمی‌کند همه Boundaryهای تازه در هر ناوبری رفتار یکسان داشته باشند. جای مرز و آماده بودن Module در نتیجه نقش دارند.

داده قبلی باید به عنوان داده قبلی شناخته شود

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

پاسخ قدیمی ممکن است دیرتر برسد؛ Transition به تنهایی قرارداد انتخاب پاسخ معتبر را تعریف نمی‌کند. Query یا شناسه درخواست باید با نتیجه مرتبط باشد. Event Loop درباره زمان آماده شدن و لغو توضیح دارد. وقتی داده جدید دریافت نشد، حفظ ظاهر قبلی نباید موفقیت ساختگی بسازد. خطای به‌روزرسانی و ادامه نمایش داده Cacheشده می‌توانند هم‌زمان رخ دهند و رابط باید همین محدودیت را بیان کند.

خطا، State و Cleanup را جدا مدیریت کنید

Suspense برای انتظار است و Error Boundary برای دسته‌ای از خطاهای Render و بارگذاری کد نقش دارد. خطای Event Handler یا عملیات async نیز مسیر مدیریت مناسب خودش را می‌خواهد. دکمه Retry باید واقعاً راه تلاش دوباره داشته باشد؛ Reset کردن پیام بدون تغییر منبع خطا کافی نیست. در نمونه آموزشی، Reload راه شروع دوباره است. در محصول می‌توان مرز مناسب‌تر و Cache قابل بازیابی داشت، به شرط اینکه رفتار آن مشخص باشد.

درختی که پیش از Mount اولیه Suspend شده ممکن است از نو Render شود؛ State و اثر جانبی باید با مدل React سازگار باشند. شروع Listener یا درخواست نامناسب داخل Render، راه حل انتظار نیست. Cleanup منابع در چرخه مناسب انجام شود. مقاله Component Patterns درباره مالکیت State و منطق مشترک مفید است. پنهان شدن موقت یک بخش نیز نباید به باقی ماندن کارهای بی‌صاحب یا ثبت نتیجه در محدوده نامعتبر منجر شود.

کار کمتر و اولویت بهتر را تفکیک کنید

Code Splitting می‌تواند کد اولیه را کم کند و Transition اولویت Update را تغییر دهد؛ این دو اثر یکسان نیستند. Virtualization تعداد عناصر را محدود می‌کند و Memoization کار تکراری را کاهش می‌دهد. مقاله عملکرد React این هزینه‌ها را بررسی می‌کند. اگر علت کندی تصویر بزرگ یا شبکه است، افزودن Transition به یک دکمه لزوماً انتظار را کم نمی‌کند. اصلاح باید با شواهد مسیر واقعی کاربر توضیح داده شود.

در تحویل، منبع فعال‌کننده Suspense، محدوده Boundary، معنی pending و راه خطا را ثبت کنید. نسخه و امکانات Stable را از گزینه‌های Experimental جدا نگه دارید. شروع مناسب یک بخش مشخص با بارگذاری واقعی است: تصمیم بگیرید در انتظار چه چیزی باقی می‌ماند، چه پیام کوتاهی لازم است و کاربر چگونه از شکست خارج می‌شود. این قرارداد، Suspense و Transition را به رفتار قابل فهم تبدیل می‌کند؛ نام API به تنهایی تجربه بارگذاری کامل نمی‌سازد.

واکنش به این مقاله

اشتراک‌گذاری این مقاله:
دیدگاه‌ها 0
۰ / ۲۰۰۰

هنوز دیدگاهی ثبت نشده

اولین نفری باشید که نظر می‌دهد!