بذار از یه سؤال شروع کنیم: وقتی توی یه اینپوت تایپ می‌کنی و setState صدا زده می‌شه، دقیقاً چه اتفاقی داخل React می‌افته؟ کامپوننت‌ها دوباره اجرا می‌شن، یه چیزی با قبلی مقایسه می‌شه، DOM آپدیت می‌شه... این‌ها رو همه می‌دونیم. ولی «چطوری» و «به چه ترتیبی» و مهم‌تر از همه «چرا بعضی کارها رو می‌تونه وسطش نگه داره و بعداً ادامه بده؟» همون چیزیه که معماری درونی React رو جذاب می‌کنه.

توی این مقاله خودمونی و قدم‌به‌قدم می‌ریم سراغ قلب React: Fiber، دو فاز Render و Commit، الگوریتم Reconciliation، Scheduler و Laneها و در نهایت Concurrent Rendering و چیزایی که React 19 روش ساخته. برای هر بخش هم یه تصویر (که با SVG کشیدم) و کد گذاشتم که راحت‌تر توی ذهنت جا بیفته.

۱. اصلاً چرا Fiber اومد؟ ماجرای Stack Reconciler

قبل از React 16، موتور رندر React یه چیزی به اسم Stack Reconciler بود. اسمش از اینجا میاد که مثل یه call stack معمولی کار می‌کرد: یه بار شروع می‌کرد به پیمایش درخت کامپوننت‌ها (به‌صورت بازگشتی) و تا وقتی کل کار تموم نمی‌شد، متوقف نمی‌شد. یعنی «همه یا هیچ».

مشکلش کجا بود؟ مرورگر یه main thread بیشتر نداره و این thread هم‌زمان باید JavaScript اجرا کنه، رویدادهای کاربر (کلیک، تایپ) رو جواب بده و صفحه رو paint کنه. اگه رندر React یه لیست سنگین رو چند ده میلی‌ثانیه (یا بدتر، چند صد میلی‌ثانیه) بدون وقفه پردازش کنه، اون وسط هر کلیک یا کلیدی که کاربر بزنه توی صف می‌مونه و UI «قفل» به نظر می‌رسه. همون حس لگ موقع تایپ در اینپوتی که کنارش یه لیست سنگین رندر می‌شه.

Stack Reconciler (قدیمی) رندر یک‌تکه و بدون وقفه — main thread قفله کلیک کاربر؛ باید صبر کنه تا رندر تموم شه… Fiber (رندر تکه‌تکه) chunk 1 chunk 2 پاسخ به کلیک chunk 3 chunk 4 chunk 5 commit کلیک زمان
رندر قدیمی همه‌ی کار رو یک‌جا انجام می‌داد؛ Fiber کار رو به تکه‌های کوچیک می‌شکنه و بینشون به مرورگر نفس می‌ده.

ایده‌ی Fiber (که از React 16 اومد) ساده بود: «بیاید رندر رو از یه فراخوانی بازگشتیِ غیرقابل‌توقف، تبدیل کنیم به یه سری واحد کار کوچیک که هر لحظه بشه نگهشون داشت، ادامه‌شون داد، اولویت‌بندی‌شون کرد یا حتی دورشون انداخت.» همین یه تغییر، پایه‌ی همه‌ی چیزهاییه که امروز تحت اسم Concurrent React می‌شناسیم.

۲. Fiber دقیقاً چیه؟

Fiber در اصل یه آبجکت ساده‌ی JavaScript هست که یه «واحد کار» رو نشون می‌ده. «کار» یعنی هر چیزی که React باید انجام بده تا UI رو نشون بده: محاسبه‌ی خروجی JSX یه کامپوننت، پردازش یه آپدیت state، اجرای یه افکت و... وقتی یه Fiber مربوط به یه کامپوننت باشه، اطلاعاتی مثل props، state و ارتباطش با بقیه‌ی درخت رو هم نگه می‌داره.

این یه نسخه‌ی ساده‌شده از فیلدهای مهم یه Fiber هست (سورس واقعی خیلی فیلد بیشتری داره):

javascript
// نسخه‌ی ساده‌شده‌ی یه Fiber node
const fiber = {
  // --- هویت ---
  tag,            // نوع فایبر: FunctionComponent, HostComponent (div), HostRoot, ...
  key,            // همون key که توی لیست‌ها می‌دیم
  type,           // خود تابع کامپوننت یا اسم تگ ('div')
  stateNode,      // برای تگ‌های host: خود عنصر DOM
 
  // --- ساختار درخت (یه linked list درختی) ---
  return: null,   // پدر
  child: null,    // اولین بچه
  sibling: null,  // برادر/خواهر بعدی
  index: 0,
 
  // --- ورودی/خروجی ---
  pendingProps,   // props جدیدی که باید پردازش بشه
  memoizedProps,  // props‌ای که آخرین بار با اون رندر شد
  memoizedState,  // state (و لیست hookها)
  updateQueue,    // صف آپدیت‌های در انتظار
 
  // --- افکت‌ها و اولویت ---
  flags,          // چه کاری باید روی این فایبر انجام بشه؟ (Placement, Update, ...)
  subtreeFlags,   // خلاصه‌ی flagهای زیردرخت
  deletions,      // فایبرهایی که باید حذف بشن
  lanes,          // اولویت کارِ این فایبر
  childLanes,     // اولویت کار بچه‌ها
 
  // --- دوقلوی همین فایبر توی درخت دیگه ---
  alternate,
};

دقت کن به سه تا پوینتر child، sibling و return. React به‌جای اینکه درخت رو با آرایه‌ی بچه‌ها نگه داره، اون رو به شکل یه linked list ذخیره می‌کنه. دلیلش هم همونه که گفتیم: با این ساختار می‌تونه هر جای پیمایش وایسته و بعداً دقیقاً از همون نقطه ادامه بده؛ چیزی که با یه تابع بازگشتی ممکن نیست.

<App /> <div> <h1> <p> child child sibling return return child: اولین بچه sibling: هم‌سطحِ بعدی return: پدر
درخت Fiber یه linked list درختیه: هر نود فقط اولین بچه، برادر بعدی و پدرش رو می‌شناسه.

۳. دو تا درخت: Current و WorkInProgress

همه‌چیز از همون خطی شروع می‌شه که توی هر پروژه‌ی React داریم:

jsx
import { createRoot } from 'react-dom/client';
import App from './App';
 
const root = createRoot(document.getElementById('root'));
root.render(<App />);

createRoot یه آبجکت به اسم FiberRootNode می‌سازه که کارهای مدیریتی کل اپ رو انجام می‌ده و یه پوینتر به اسم current داره. این پوینتر به HostRootFiber اشاره می‌کنه؛ یعنی اولین نودِ درخت که نماینده‌ی همون <div id="root"> هست. بار اولی که رندر می‌کنی چون هنوز چیزی روی صفحه نیست، این نود بچه‌ای نداره.

حالا نکته‌ی اصلی: React همیشه دو تا درخت Fiber داره.

  • Current tree: چیزی که همین الان روی صفحه می‌بینی.
  • WorkInProgress (WIP) tree: «پیش‌نویس» درخت بعدی؛ جایی که React آپدیت‌ها رو توش حساب می‌کنه، بدون اینکه به UI فعلی دست بزنه.

هر فایبر توی یکی از این دو درخت، یه «دوقلو» توی درخت دیگه داره که با فیلد alternate بهش وصله. React موقع آپدیت، فایبرها رو از current کلون می‌کنه (یا اگه از رندر قبلی موجود بود، دوباره استفاده می‌کنه تا حافظه و زمان هدر نره). به این تکنیک می‌گن double buffering (همون ایده‌ای که توی گرافیک و بازی‌سازی برای جلوگیری از پرش تصویر استفاده می‌شه).

FiberRootNode Current tree (روی صفحه) WorkInProgress tree (پیش‌نویس) HostRoot App div HostRoot App div alternate current بعد از commit بعد از commit فقط یه پوینتر عوض می‌شه؛ هیچ کپی سنگینی انجام نمی‌شه.
React پیش‌نویس رو کنار درخت فعلی می‌سازه و وقتی آماده شد، پوینتر current رو بهش می‌چرخونه.

۴. فاز Render و Reconciliation

اینجا یه سوءتفاهم رایج داریم که باید صاف و ساده حلش کنیم: «render» توی React یعنی اجرای کامپوننت‌ها، نه کشیدن چیزی روی صفحه. توی فاز Render، React فقط توی حافظه حساب‌وکتاب می‌کنه که UI جدید باید چه شکلی باشه. هیچ تغییری روی DOM یا صفحه اتفاق نمی‌افته.

این فاز در دو حالت شروع می‌شه: یا بار اولِ لود صفحه (initial render) یا وقتی چیزی در runtime عوض بشه (مثلاً setState بعد از یه کلیک).

Work Loop: موتور فاز Render

کل کار توسط یه حلقه به اسم work loop انجام می‌شه. این حلقه درخت WIP رو به صورت depth-first پیمایش می‌کنه (اول می‌ره تا ته عمق، بعد برمی‌گرده) و برای هر فایبر دو تا مرحله داره:

  1. beginWork: موقع «پایین رفتن». برای کامپوننت‌های تابعی، همین‌جاست که تابع کامپوننت صدا زده می‌شه، JSX جدید به دست میاد و بچه‌ها با نتیجه‌ی قبلی مقایسه (reconcile) می‌شن.
  2. completeWork: موقع «برگشتن به بالا». وقتی کار یه فایبر و همه‌ی بچه‌هاش تموم شد، نتیجه جمع‌بندی می‌شه؛ مثلاً برای تگ‌های host، عنصر DOM (هنوز بیرون از صفحه) ساخته می‌شه و flagهای مربوط به این فایبر ثبت می‌شه.
javascript
// نسخه‌ی ساده‌شده‌ی منطق React
function performUnitOfWork(unitOfWork) {
  const current = unitOfWork.alternate;
 
  // ۱) بیا پایین: کامپوننت رو اجرا کن و بچه‌ها رو reconcile کن
  const next = beginWork(current, unitOfWork, renderLanes);
  unitOfWork.memoizedProps = unitOfWork.pendingProps;
 
  if (next === null) {
    // بچه‌ای نیست → کار این فایبر تموم شده، برگرد بالا
    completeUnitOfWork(unitOfWork);
  } else {
    workInProgress = next; // برو سراغ اولین بچه
  }
}
 
function completeUnitOfWork(unitOfWork) {
  let completedWork = unitOfWork;
 
  do {
    completeWork(completedWork.alternate, completedWork, renderLanes);
 
    // اگه برادر/خواهر داره، برو سراغش
    const sibling = completedWork.sibling;
    if (sibling !== null) {
      workInProgress = sibling;
      return;
    }
 
    // وگرنه برگرد پیش پدر
    completedWork = completedWork.return;
    workInProgress = completedWork;
  } while (completedWork !== null);
}
App Header Main Logo begin 1 · complete 8 begin 2 · complete 5 begin 6 · complete 7 begin 3 · complete 4 آبی = beginWork (رفتن به پایین) | سبز = completeWork (برگشتن به بالا)
ترتیب کار: اول تا ته عمق می‌ره، بعد موقع برگشت، برادرها رو بررسی می‌کنه.

خودِ Reconciliation یعنی چی؟

کل این جریان (پیمایش درخت، صدا زدن کامپوننت‌ها، ساختن فایبرهای جدید و مقایسه با درخت current) رو reconciliation می‌گن و الگوریتمش اسمش Fiber reconciler هست. برای اینکه مقایسه‌ی دو درخت از نظر زمانی خیلی گرون نشه، React روی چند تا قاعده‌ی ساده شرط می‌بنده:

  • اگه type یه المان عوض شد (مثلاً <div> شد <span> یا کامپوننت A شد B)، React کل اون زیردرخت رو دور می‌ریزه و از صفر می‌سازه.
  • اگه type همون بود، همون فایبر (و state اش) حفظ می‌شه و فقط props جدید بهش داده می‌شه.
  • برای لیست‌ها، key مشخص می‌کنه کدوم آیتم کدوم بوده، تا موقع جابه‌جایی یا حذف، state آیتم‌ها قاطی نشه.
jsx
// ❌ index به‌عنوان key: با حذف یا جابه‌جایی آیتم‌ها، state اشتباه به آیتم دیگه می‌چسبه
{todos.map((todo, i) => <TodoItem key={i} todo={todo} />)}
 
// ✅ یه شناسه‌ی پایدار و یکتا
{todos.map(todo => <TodoItem key={todo.id} todo={todo} />)}

نتیجه‌ی کار هم روی خود فایبرها flag می‌خوره: «این نود باید اضافه بشه» (Placement)، «این باید آپدیت بشه» (Update)، «این باید حذف بشه» (Deletion). (توی نسخه‌های قدیمی‌تر، این‌ها توی یه «effect list» جدا جمع می‌شد؛ توی نسخه‌های جدید، با flags و subtreeFlags روی خود درخت نگه داشته می‌شه تا React بتونه خیلی سریع بفهمه کدوم زیردرخت‌ها اصلاً کاری دارن.)

یه بهینه‌سازی مهم هم هست که بهش می‌گن bailout: اگه یه فایبر نه props جدیدی داره، نه state ای که تغییر کرده و نه کار در انتظاری توی بچه‌هاش (با کمک lanes و childLanes می‌فهمه)، React کل اون زیردرخت رو رد می‌کنه و همون فایبرهای قبلی رو دوباره استفاده می‌کنه.

چرا رندر باید «خالص» (pure) باشه؟

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

jsx
let counter = 0;
 
// ❌ توی رندر یه متغیر بیرونی رو تغییر می‌ده؛ اگه رندر دو بار یا نصفه اجرا بشه، نتیجه خراب می‌شه
function Bad() {
  counter++;
  return <p>{counter}</p>;
}
 
// ✅ فقط از props/state می‌خونه و یه خروجی برمی‌گردونه
function Good({ count }) {
  return <p>{count}</p>;
}

۵. فاز Commit: لحظه‌ای که تغییرات واقعاً اعمال می‌شن

وقتی کار فایبر HostRoot هم «complete» شد، یعنی درخت WIP کامل آماده‌ست و React وارد فاز Commit می‌شه. این فاز با فاز Render یه فرق بزرگ داره: همگام (synchronous) و غیرقابل وقفه هست. چرا؟ چون اگه وسط تغییر دادن DOM متوقف بشه، کاربر یه UI نصفه و خراب می‌بینه.

فاز Commit خودش چند زیرمرحله داره که به ترتیب اجرا می‌شن:

  1. Before Mutation: قبل از اینکه DOM تغییر کنه. مثلاً getSnapshotBeforeUpdate کامپوننت‌های کلاسی اینجا اجرا می‌شه تا بتونی اطلاعات layout (مثل موقعیت اسکرول) رو قبل از تغییر بخونی.
  2. Mutation: تغییرات واقعی روی DOM: حذف نودها (Deletion)، اضافه کردن (Placement با چیزی مثل appendChild یا insertBefore) و آپدیت عنصرهای موجود (Update).
  3. Layout: بلافاصله بعد از تغییر DOM و قبل از paint مرورگر. useLayoutEffect اینجا اجرا می‌شه.
  4. Passive Effects: بعد از اینکه مرورگر صفحه رو paint کرد (به‌صورت غیرهمگام). useEffect اینجا اجرا می‌شه.

و اون لحظه‌ی «جادویی» سوییچ درخت‌ها؟ React درست بعد از مرحله‌ی Mutation و قبل از مرحله‌ی Layout، پوینتر root.current رو به درخت WIP می‌چرخونه. یعنی همون موقع، پیش‌نویس رسماً می‌شه درخت current و درخت قبلی آماده‌ی استفاده به‌عنوان WIP رندر بعدی می‌شه. دلیل این ترتیب هم اینه که useLayoutEffect و componentDidMount باید درخت جدید رو به‌عنوان current ببینن.

تریگر setState Render begin/completeWork Before Mutation snapshot Mutation آپدیت DOM Layout useLayoutEffect Passive useEffect Browser Paint root.current = finishedWork قابل وقفه (interruptible) همگام و غیرقابل وقفه (synchronous) بعد از paint (async)
Render قابل وقفه‌ست، Commit نه؛ و useEffect بعد از paint اجرا می‌شه نه قبلش.

یه مثال کوچیک برای اینکه ترتیب رو با چشم خودت ببینی:

jsx
import { useState, useEffect, useLayoutEffect } from 'react';
 
function Demo() {
  const [n, setN] = useState(0);
 
  console.log('1) render (فاز Render؛ هنوز چیزی روی صفحه عوض نشده)');
 
  useLayoutEffect(() => {
    // ۲) DOM آپدیت شده، ولی مرورگر هنوز paint نکرده
    console.log('2) layout effect');
  });
 
  useEffect(() => {
    // ۳) بعد از paint
    console.log('3) passive effect');
  });
 
  return <button onClick={() => setN(n + 1)}>{n}</button>;
}

اگه از useLayoutEffect برای کارهای سنگین استفاده کنی، چون قبل از paint و به‌صورت همگام اجرا می‌شه، جلوی نمایش صفحه رو می‌گیره. پس قاعده‌ی کلی اینه: پیش‌فرضت useEffect باشه و فقط وقتی لازم داری قبل از paint اندازه‌ی چیزی رو بخونی یا یه تغییر ظاهری رو بدون چشمک زدن اعمال کنی، سراغ useLayoutEffect برو.

۶. Scheduler و Laneها: کی چه کاری انجام بشه؟

تا اینجا فهمیدیم کار تکه‌تکه می‌شه. ولی «چه کسی» تصمیم می‌گیره کدوم تکه اول بره و کی باید به مرورگر نفس داد؟ اینجاست که Scheduler میاد وسط. مثل یه مدیر پروژه‌ی باحاله که کارها رو می‌گیره، اولویت‌بندی می‌کنه و تو بازه‌های کوتاه اجراشون می‌کنه تا مرورگر بتونه ورودی‌های کاربر رو جواب بده و صفحه رو paint کنه.

دو تا مفهوم رو باید از هم جدا کنیم:

  • Lane (خط): سیستم اولویت‌بندی داخل خود React. هر آپدیت یه lane می‌گیره (و lane ها بیت‌ماسک هستن، پس می‌شه چند تاشون رو هم‌زمان نگه داشت و ترکیب کرد).
  • Scheduler: پکیج جدا که کار رو توی main thread زمان‌بندی می‌کنه و تعیین می‌کنه کی باید کنترل رو به مرورگر پس بده.
Lane مثال نحوه‌ی رندر SyncLane کلیک، keydown، submit (رویدادهای گسسته) بدون time slicing InputContinuousLane scroll، drag، mousemove بدون time slicing DefaultLane setState معمولی (مثلاً بعد از fetch) بدون time slicing TransitionLanes startTransition و useDeferredValue time-sliced RetryLanes تلاش دوباره بعد از resolve شدن Suspense time-sliced IdleLane کارهای پس‌زمینه با کمترین اولویت time-sliced
اولویت از بالا به پایین کم می‌شه. فقط آپدیت‌های غیرفوری (transition و بقیه‌ی پایین‌تری‌ها) تکه‌تکه رندر می‌شن.

این جدول یه نکته‌ی مهم داره که خیلی‌ها اشتباه برداشت می‌کنن: داشتن createRoot به تنهایی باعث نمی‌شه همه‌ی رندرهات تکه‌تکه بشن. آپدیت‌های فوری (کلیک، تایپ) و آپدیت‌های معمولی هنوز به‌صورت «blocking» رندر می‌شن؛ این عمداً هست، چون این آپدیت‌ها باید سریع و بدون ناپایداری روی صفحه بیان. اون چیزی که واقعاً قابل وقفه‌ست، آپدیت‌هاییه که خودت با startTransition یا useDeferredValue غیرفوری علامت‌گذاری می‌کنی.

Scheduler چطوری کنترل رو پس می‌ده؟

توی توضیحات قدیمی‌تر (و بعضی از منابع) می‌بینی که می‌گن React از requestIdleCallback استفاده می‌کنه. ایده‌ی کلی همون بود، ولی پیاده‌سازی فعلیِ Scheduler به‌جاش از یه MessageChannel استفاده می‌کنه تا یه تسک جدید توی event loop صف کنه و بین تکه‌ها به مرورگر فرصت بده. هر «تکه» هم حدود ۵ میلی‌ثانیه طول می‌کشه و بعدش، اگه هنوز کار مونده باشه، Scheduler دوباره خودش رو صف می‌کنه.

javascript
// نسخه‌ی ساده‌شده‌ی Scheduler
const frameInterval = 5; // میلی‌ثانیه
let startTime = -1;
 
function shouldYieldToHost() {
  const timeElapsed = performance.now() - startTime;
  return timeElapsed >= frameInterval; // وقتش رسیده؟ کنترل رو پس بده
}
 
const channel = new MessageChannel();
channel.port1.onmessage = performWorkUntilDeadline;
 
function schedulePerformWorkUntilDeadline() {
  channel.port2.postMessage(null); // یه تسک جدید توی event loop
}
 
function performWorkUntilDeadline() {
  startTime = performance.now();
  const hasMoreWork = flushWork(); // کارهای صف شده رو تا deadline اجرا کن
  if (hasMoreWork) {
    schedulePerformWorkUntilDeadline(); // مرورگر نفس بکشه، بعد ادامه
  }
}

و این هم تفاوت بین دو حلقه‌ی کاری React که همه‌ی ماجرای «قابل وقفه بودن» توی همین چند خط خلاصه می‌شه:

javascript
// رندر همگام (آپدیت‌های فوری): تا آخر بدون وقفه
function workLoopSync() {
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}
 
// رندر هم‌زمان (transitionها و ...): بعد از هر فایبر چک می‌کنه آیا وقت پس دادن کنترل رسیده؟
function workLoopConcurrent() {
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}

چون workInProgress یه متغیر سراسریه که همیشه «فایبر بعدی» رو نگه می‌داره، وقتی حلقه وسط کار می‌ایسته، هیچ چیزی از دست نمی‌ره؛ دفعه‌ی بعد که Scheduler دوباره بیاد، حلقه دقیقاً از همون فایبر ادامه می‌ده.

یه نکته‌ی جانبی: خود پکیج Scheduler هم برای کارهاش اولویت‌های جداگانه‌ای داره (Immediate، UserBlocking، Normal، Low و Idle) که هر کدوم یه «مهلت انقضا» دارن (مثلاً UserBlocking حدود ۲۵۰ میلی‌ثانیه و Normal حدود ۵ ثانیه). React lane ها رو به این اولویت‌ها تبدیل می‌کنه تا کار منتظر مونده‌ی قدیمی هم هیچ‌وقت برای همیشه گرسنه نمونه.

۷. Concurrent Rendering: وقفه، دور انداختن و شروع دوباره

حالا همه‌ی قطعات رو داریم. Concurrent Rendering یعنی React بتونه چند تا آپدیت با اولویت‌های مختلف رو هم‌زمان «مدیریت» کنه (نه اینکه واقعاً هم‌زمان اجرا کنه؛ هنوز single thread هستیم). قابلیت‌هاش از ترکیب این‌ها میاد:

  • Time Slicing: شکستن رندر به تکه‌های کوچیک تا UI جواب‌گو بمونه.
  • Prioritization: آپدیت‌های مهم‌تر (تایپ کردن) جلوی کم‌اهمیت‌ترها (فیلتر کردن لیست بزرگ) می‌افتن.
  • Interruption: رندر نیمه‌کاره می‌تونه رها بشه.
  • Reuse / Discard: کار انجام‌شده‌ی WIP یا دوباره استفاده می‌شه یا دور ریخته می‌شه.

سناریوی کلاسیک رو ببین: کاربر توی یه اینپوت جستجو تایپ می‌کنه و نتیجه‌ها یه لیست سنگینه. آپدیت اینپوت فوریه (باید همون لحظه حرف روی صفحه بیاد) ولی فیلتر کردن لیست غیرفوریه. اگه وسط رندر لیست، کاربر یه حرف دیگه بزنه چی می‌شه؟ React رندر نیمه‌کاره‌ی لیست رو که دیگه کهنه شده دور می‌ندازه، اول آپدیت فوری رو رندر و commit می‌کنه، و بعد رندر لیست رو با مقدار جدید از اول شروع می‌کنه.

وقفه‌ی یه Transition توسط آپدیت فوری کاربر یه حرف دیگه تایپ کرد render لیست فوری render لیست (از نو) commit این WIP کهنه شده و دور ریخته می‌شه اول جواب کاربر رو بده دوباره از اول (با state جدید) نتیجه: UI همیشه جواب‌گو می‌مونه و هیچ نتیجه‌ی نصفه‌ای روی صفحه نمیاد.
رندر فاز Render رو می‌شه دور ریخت، چون هنوز به DOM دست نزده؛ همین امنیت، وقفه رو ممکن می‌کنه.

توی کد، ابزار اصلی برای این کار useTransition هست:

jsx
import { useState, useTransition } from 'react';
 
function SearchPage() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [isPending, startTransition] = useTransition();
 
  function handleChange(e) {
    const value = e.target.value;
 
    setQuery(value); // فوری: حرف تایپ‌شده باید همون لحظه بیاد
 
    startTransition(() => {
      // غیرفوری: اگه کاربر دوباره تایپ کنه، این رندر قطع می‌شه
      setResults(filterHugeList(value));
    });
  }
 
  return (
    <>
      <input value={query} onChange={handleChange} />
      {isPending && <p>در حال جستجو...</p>}
      <ResultsList items={results} />
    </>
  );
}

اگه state رو خودت کنترل نمی‌کنی (مثلاً از props میاد)، useDeferredValue همین کار رو برات انجام می‌ده: یه «نسخه‌ی عقب‌افتاده» از مقدار می‌سازه که با اولویت پایین‌تر آپدیت می‌شه.

jsx
import { useDeferredValue, memo } from 'react';
 
const SlowList = memo(function SlowList({ query }) {
  // یه لیست خیلی سنگین...
  return <ul>{/* ... */}</ul>;
});
 
function Search({ query }) {
  const deferredQuery = useDeferredValue(query);
  const isStale = query !== deferredQuery;
 
  return (
    <div style={{ opacity: isStale ? 0.6 : 1 }}>
      <SlowList query={deferredQuery} />
    </div>
  );
}

۸. React 19 چی به این معماری اضافه کرد؟

اگه تا اینجا اومده باشی، حالا راحت می‌تونی بفهمی React 19 چرا اون‌طوری طراحی شده. بیشتر قابلیت‌های جدیدش (Actions، useOptimistic، use) خودشون جایگزین معماری Fiber نیستن؛ روی همین موتور Concurrent ساخته شدن و از transition و lane ها استفاده می‌کنن.

Actions: transition های async

توی React 19 می‌تونی یه تابع async رو به startTransition بدی. React خودش وضعیت pending و خطاها رو مدیریت می‌کنه و تا وقتی تموم نشده، UI فعلی رو نگه می‌داره.

jsx
function UpdateName() {
  const [name, setName] = useState('');
  const [error, setError] = useState(null);
  const [isPending, startTransition] = useTransition();
 
  const handleSubmit = () => {
    startTransition(async () => {
      const err = await updateName(name); // عملیات async داخل transition
      if (err) {
        setError(err);
        return;
      }
      redirect('/profile');
    });
  };
 
  return (
    <div>
      <input value={name} onChange={(e) => setName(e.target.value)} />
      <button onClick={handleSubmit} disabled={isPending}>ذخیره</button>
      {error && <p>{error}</p>}
    </div>
  );
}

useOptimistic: جواب فوری، تأیید بعداً

useOptimistic یه state «خوش‌بینانه» نشون می‌ده تا درخواست واقعی تموم بشه. اگه درخواست شکست بخوره، React خودش به مقدار واقعی برمی‌گرده. این هم دقیقاً مثل transition ها، توی یه اولویت جداگانه و بدون قفل کردن UI اتفاق می‌افته.

jsx
import { useOptimistic } from 'react';
 
function Likes({ likes, sendLike }) {
  const [optimisticLikes, addOptimistic] = useOptimistic(
    likes,
    (current, delta) => current + delta
  );
 
  async function action() {
    addOptimistic(1); // فوراً نشون بده
    await sendLike(); // بعد از تأیید سرور، مقدار واقعی جایگزین می‌شه
  }
 
  return (
    <form action={action}>
      <button>{optimisticLikes} ❤️</button>
    </form>
  );
}

use() و Suspense

هوک use اجازه می‌ده یه Promise (یا Context) رو مستقیم توی رندر بخونی. اگه Promise هنوز resolve نشده باشه، کامپوننت «suspend» می‌شه و نزدیک‌ترین Suspense fallback نشون داده می‌شه. با کمک معماری Fiber، React کارِ این زیردرخت رو متوقف می‌کنه، بقیه‌ی صفحه رو ادامه می‌ده و بعد از resolve شدن، با یه Retry lane دوباره امتحان می‌کنه. و اگه این اتفاق وسط یه transition بیفته، React ترجیح می‌ده UI قبلی رو نگه داره به‌جای اینکه یه fallback ناگهانی نشون بده.

jsx
import { use, Suspense } from 'react';
 
function Comments({ commentsPromise }) {
  const comments = use(commentsPromise); // تا resolve نشه، suspend می‌شه
  return comments.map((c) => <p key={c.id}>{c.text}</p>);
}
 
function Post({ commentsPromise }) {
  return (
    <Suspense fallback={<p>در حال بارگذاری نظرات...</p>}>
      <Comments commentsPromise={commentsPromise} />
    </Suspense>
  );
}

توی نسخه‌ی ۱۹.۲ هم چیزهایی مثل <Activity> (برای مخفی کردن بخشی از UI در حالی که state اش حفظ می‌شه و با اولویت پایین رندر می‌شه) و useEffectEvent اضافه شدن که باز هم از همین ایده‌ی «کار با اولویت‌های مختلف» استفاده می‌کنن.

۹. جمع‌بندی: کل ماجرا توی یه نگاه

  1. یه چیزی آپدیت رو تریگر می‌کنه (setState، dispatch، root.render) و برای اون آپدیت یه lane انتخاب می‌شه.
  2. Scheduler تصمیم می‌گیره کی اجرا بشه. آپدیت‌های فوری بلافاصله، transition ها تکه‌تکه و با فرصت دادن به مرورگر.
  3. فاز Render: work loop، درخت WIP رو با beginWork/completeWork می‌سازه و reconciliation فایبرها رو با درخت current مقایسه و flag گذاری می‌کنه. این فاز خالصه و قابل وقفه‌ست.
  4. فاز Commit (همگام و غیرقابل وقفه): Before Mutation ← Mutation (DOM) ← سوییچ root.current ← Layout (useLayoutEffect).
  5. مرورگر paint می‌کنه و بعد Passive Effects (useEffect) اجرا می‌شن.
  6. درخت قدیمی می‌شه WIP رندر بعدی و چرخه از نو.

اگه یه جمله بخوای از این مقاله با خودت ببری، این باشه: React رندر رو از «اجرا کردن» جدا کرده. رندر یه محاسبه‌ی ارزونه که می‌شه شروع کرد، وسطش وایساد و حتی دور ریخت؛ و فقط وقتی همه‌چیز آماده بود، توی یه حرکت سریع و غیرقابل‌توقف به DOM اعمال می‌شه. بقیه‌ی قابلیت‌های جذاب React 19 هم نتیجه‌ی همین یه تصمیم معماریه.

منابع