use: خواندن Promise و Context در حین Render

آخرین API مدرن این فصل که پیش از آزمون می‌بینیم، use است. در فصل بیست‌ویکم، چند بار اشاره کردیم که Suspense با «پرتاب Promise» کار می‌کند و گفتیم React برای این کار یک ابزار رسمی دارد. این ابزار همین است: به‌جای اینکه خودمان مثل مثال createResource یک Promise را پرتاب کنیم، use این کار را به‌درستی و به‌صورت استاندارد انجام می‌دهد.

use چیست

use یک API است که مقدار یک «منبع» را در حین Render می‌خواند. منبع می‌تواند یکی از این دو باشد:

  • یک Promise: اگر هنوز حل نشده باشد، کامپوننت Suspend می‌شود تا مقدار آماده شود.
  • یک Context: مقدار فعلی آن را برمی‌گرداند، مثل useContext فصل هشتم.

خواندن یک Promise

jsx
import { use } from "react";

function UserName({ userPromise }) {
  const user = use(userPromise);
  return <p>نام: {user.name}</p>;
}

تا وقتی userPromise حل نشده، use کامپوننت را Suspend می‌کند و نزدیک‌ترین Suspense، همان‌طور که در فصل بیست‌ویکم دیدیم، fallback را نشان می‌دهد. به‌محض حل شدن، کامپوننت دوباره Render می‌شود و use مقدار نهایی را برمی‌گرداند. چون کد بعد از use فقط وقتی اجرا می‌شود که مقدار آماده است، نه نیاز به State برای loading داریم و نه شرط if (loading) در خود کامپوننت.

jsx
<ErrorBoundary fallback={<p>مشکلی پیش آمد.</p>}>
  <Suspense fallback={<p>در حال بارگذاری...</p>}>
    <UserName userPromise={userPromise} />
  </Suspense>
</ErrorBoundary>

شرط حیاتی: Promise باید پایدار باشد

مهم‌ترین نکته این درس این است. Promise که به use می‌دهید باید بین رندرها همان Promise بماند. دلیلش همان Referential Equality فصل سیزدهم است: اگر کامپوننتی که use را صدا می‌زند، خودش در حین Render یک Promise تازه بسازد، با هر Render یک Promise جدید دارد. چون کامپوننت Suspend شده و هنوز به صفحه نرسیده، React نمی‌تواند State یا مقدار قبلی آن را نگه دارد؛ نتیجه، یک چرخه بی‌پایان از ساخت Promise جدید و Suspend دوباره است. React در این حالت هشدار می‌دهد که کامپوننت به‌خاطر یک Promise «کش‌نشده» Suspend شده است.

jsx
// نادرست: هر Render یک Promise تازه می‌سازد
function UserName({ id }) {
  const user = use(fetchUser(id));
  return <p>{user.name}</p>;
}

در برنامه‌های کلاینتی، Promise پایدار معمولاً از یکی از این راه‌ها می‌آید:

  • در یک کامپوننت والد ساخته و به‌عنوان Prop پایین فرستاده شود، مثل مثال اول و مثال قابل اجرای همین درس.
  • در یک Event Handler ساخته و در State نگه‌داری شود.
  • از کش یک کتابخانه سازگار با Suspense بیاید، همان کتابخانه‌های تخصصی Server State که در فصل چهاردهم و شانزدهم دیدیم.

در معماری Server Components، Promise می‌تواند روی سرور ساخته و به یک Client Component فرستاده شود و در آن‌جا پایدار می‌ماند؛ جزئیات این الگو در فصل بیست‌وچهارم می‌آید.

مدیریت خطا

اگر Promise رد شود (Reject)، use خطا را پرتاب می‌کند و نزدیک‌ترین Error Boundary، طبق فصل هفدهم، آن را می‌گیرد. نکته مهم این است که نمی‌توان use را داخل بلوک try/catch صدا زد. دو راه‌حل مجاز وجود دارد: یا یک Error Boundary بیرون از کامپوننت بگذارید، یا هنگام ساختن Promise، با .catch() یک مقدار جایگزین برگردانید:

jsx
const messagePromise = fetchMessage().catch(() => "پیام پیش‌فرض");

خواندن Context با use

jsx
function Price({ show }) {
  if (!show) {
    return null;
  }

  const theme = use(ThemeContext);
  return <p className={theme}>قیمت</p>;
}

از نظر نتیجه، use(ThemeContext) همان کاری را می‌کند که useContext(ThemeContext) در فصل هشتم می‌کرد. تفاوت در محل فراخوانی است، که در بخش بعد می‌آید.

تفاوت اصلی با Hookهای دیگر

در فصل پنجم، Rules of Hooks را دیدیم: Hook نباید داخل شرط یا حلقه صدا زده شود، چون React هویت هر Hook را از ترتیب ثابت فراخوانی آن‌ها تشخیص می‌دهد. use از این قانون مستثنا است. می‌توان آن را داخل شرط، حلقه یا بعد از یک return زودهنگام صدا زد، مثل کد بالا. اما همچنان چند محدودیت دارد:

  • باید از داخل یک کامپوننت یا یک Custom Hook صدا زده شود، نه یک تابع معمولی.
  • نمی‌توان آن را داخل Event Handler صدا زد؛ خواندن مقدار در حین Render است، نه در واکنش به رویداد.
  • نمی‌توان آن را داخل try/catch صدا زد.

رابطه با الگوی دستی فصل چهاردهم

use الگوی useEffect به‌علاوه loading و error را برای خواندن داده جایگزین می‌کند، نه برای هر کاری. درخواستی که قرار است در واکنش به یک عمل کاربر، مثل ارسال فرم، انجام شود، همچنان از جنس Action (درس‌های قبل) است. use برای خواندن مقداری است که کامپوننت برای Render شدن به آن نیاز دارد. وقتی با use یک Promise تازه جایگزین قبلی می‌شود، کامپوننت دوباره Suspend می‌شود و طبق فصل بیست‌ویکم، پیش‌فرض نمایش فوری fallback است. اگر بخواهید محتوای قبلی کمی بیشتر بماند، تغییر Promise را داخل startTransition (فصل بیست‌ودوم) انجام دهید.

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

در این مثال، Promise در کامپوننت والد و داخل State نگه‌داری می‌شود و به‌عنوان Prop پایین می‌رود. دکمه، در Event Handler یک Promise تازه می‌سازد. با هر کلیک، UserName دوباره Suspend می‌شود و fallback نشان داده می‌شود.

خواندن یک Promise پایدار با use
import { Suspense, use, useState } from "react";

function fetchUser(id) {
  return new Promise((resolve) => {
    setTimeout(() => resolve({ name: "کاربر " + id }), 1000);
  });
}

function UserName({ userPromise }) {
  const user = use(userPromise);
  return <p>نام: {user.name}</p>;
}

function App() {
  const [id, setId] = useState(1);
  const [userPromise, setUserPromise] = useState(() => fetchUser(1));

  function handleNext() {
    const nextId = id + 1;
    setId(nextId);
    setUserPromise(fetchUser(nextId));
  }

  return (
    <div>
      <button onClick={handleNext}>کاربر بعدی</button>
      <Suspense fallback={<p>در حال بارگذاری...</p>}>
        <UserName userPromise={userPromise} />
      </Suspense>
    </div>
  );
}
برای دیدن پیش‌نمایش کامپوننت، روی Run بزنید.

جمع‌بندی

use مقدار یک Promise یا یک Context را در حین Render می‌خواند. برای Promise، کامپوننت را تا حل شدن Suspend می‌کند، خطا را به Error Boundary می‌رساند و به یک Promise پایدار نیاز دارد، نه Promiseی که هر Render از نو ساخته شود. برخلاف Hookهای دیگر می‌توان آن را داخل شرط و حلقه صدا زد، ولی فقط در کامپوننت یا Custom Hook، نه در Event Handler و نه داخل try/catch. با این درس، APIهای مدرن React که در این فصل بود به پایان می‌رسد؛ دو مورد دیگر، ViewTransition و Fragment Refs، در درس‌های بعدی می‌آیند.