مقایسه تایپ React.ReactNode و JSX.Element

با بررسی JSX.Element و React.ReactElement می‌توانیم به این نتیجه برسیم که این دو از نظر عملکرد یک تایپ هستند. بنابراین می‌توانیم آن‌ها را به جای یکدیگر مورد استفاده قرار دهیم. این دو مورد همچنین چیزی را که عبارت JSX ایجاد می‌کند، نشان می‌دهند.

const node: JSX.Element = <div />;
 
const node2: React.ReactElement = <div />;

ما نمی‌توانیم از JSX.Element و React.ReactElement برای نمایش همه چیزهایی که React می‌تواند آن‌ها را رندر کند، مانند رشته‌ها و اعداد استفاده کنیم. در چنین شرایطی باید React.ReactNode را به کار بگیریم.

const node: React.ReactNode = <div />;
const node2: React.ReactNode = "hello world";
const node3: React.ReactNode = 123;
const node4: React.ReactNode = undefined;
const node5: React.ReactNode = null;
 
const node6: JSX.Element = "hello world";
Type 'string' is not assignable to type 'Element'.

به طور کلی در استفاده روزمره، باید از React.ReactNode استفاده کنیم. زیرا خیلی به ندرت پیش می‌آید که نیاز به استفاده از تایپ خاص JSX.Element داشته باشیم.

هنگامی که تیم تایپ اسکریپت کار خود را برای پشتیبانی از React آغاز کرد، JSX مانع بزرگی در این زمینه بود. زیرا سینتکس آن در جاوااسکریپت وجود نداشت. بنابراین مجبور شدند تا آن را در کامپایلر بسازند.

تیم تایپ اسکریپت ایده فایل‌های .tsxو گزینه jsxدر tsconfig.jsonرا مطرح کردند و درنهایت این عامل باعث شد تا مشکل پشتیانی از React برطرف گردد.

اما یک سوال جالب بدون پاسخ وجود داشت این که: این تابع باید به عنوان چه تایپی استنباط شود؟

بررسی JSX.Element

// When I hover this, what should I get?
const Component = () => {
  return <div>Hello world</div>;
};

پاسخ یک تایپ خاص به نام JSX.Element است. اگر ما ماوس را روی کامپوننت نگه داریم، احتمالاً خواهیم دید:

// const Component: () => JSX.Element

JSXچیزی است که global namespace نامیده می‌شود. یعنی مانند یک آبجکتی است که در global scope قرار دارد. namespace می‌تواند شامل انواع تایپ‌ها باشد و Elementیکی از آن تایپ‌ها است.

این موضوع به این معنی است که اگر React تایپ JSX.Element را تعریف کند توسط تایپ اسکریپت انتخاب می‌شود.

تعریف تایپ در React به صورت زیر است:

// Puts it in the global scope
declare global {
  // Puts it in the JSX namespace
  namespace JSX {
    // Defines the Element interface
    interface Element
      extends React.ReactElement<any, any> {}
  }
}

می‌توانیم JSX.Element را در نظر بگیریم که به عنوان چیزی که فراخوانی یک عبارت JSX را نشان می‌دهد تعریف شده است. این تایپ همان چیزی است که هنگام نوشتن JSX ایجاد می‌شود.

JSX.Element برای چه مواردی استفاده می‌شود؟

حال سوالی که پیش می‌آید این است که چرا این دانش برای ما مفید است؟ برای چه کاری می‌خواهیم از تایپ JSX.Element استفاده کنیم؟

این مورد بدیهی‌ترین انتخاب برای تایپ ویژگی childrenیک کامپوننت خواهد بود.

const Component = ({
  children,
}: {
  children: JSX.Element;
}) => {
  return <div>{children}</div>;
};

هنگامی که ما شروع به استفاده از این تایپ می‌کنیم، مشکلات شروع به آشکار شدن می‌کنند. به عنوان مثال، اگر بخواهیم یک رشته را رندر کنیم چه اتفاقی می‌افتد؟

// 'Component' components don't accept text as
// child elements. Text in JSX has the type
// 'string', but the expected type of 'children'
// is 'Element'.
<Component>hello world</Component>

این موضوع کاملاً معتبر است، React می‌تواند چیزهای مختلفی را به‌عنوان childهای کامپوننت‌ها مدیریت کند، مانند اعداد، رشته‌ها و حتی undefined.

اما مشکلی که وجود دارد این است که ما تایپ childها را JSX.Element در نظر گرفته‌ایم که فقط JSX را می‌پذیرد. در این صورت نیاز به یک تعریف تایپ متفاوت برای استفاده از childها داریم. ما به تایپی نیاز داریم که رشته‌ها، اعداد، undefined و JSX را بپذیرد.

بررسی React.ReactNode

اینجا جایی است که React.ReactNode مطرح می‌شود. تایپی است که هر چیزی که React می‌تواند رندر کند را می‌پذیرد.

جایگاه React.ReactNode در namespace قرار دارد به این صورت که:

declare namespace React {
  type ReactNode =
    | ReactElement
    | string
    | number
    | ReactFragment
    | ReactPortal
    | boolean
    | null
    | undefined;
}

به این ترتیب می‌توانیم از آن برای تایپ children prop خود استفاده کنیم:

const Component = ({
  children,
}: {
  children: React.ReactNode;
}) => {
  return <div>{children}</div>;
};

اکنون می‌توانیم رشته‌ها، اعداد، undefined و JSX را نیز ارسال کنیم:

<Component>hello world</Component>
<Component>{123}</Component>
<Component>{undefined}</Component>
<Component>
  <div>Hello world</div>
</Component>

چه زمانی نباید از React.ReactNode استفاده کنیم؟

در نسخه‌های قبل از ۵٫۱ تایپ اسکریپت، در یک مورد خاص نمی‌توانیم از React.ReactNode استفاده کنیم و آن تایپ بازگشتی یک کامپوننت است:

const Component = (): React.ReactNode => {
  return <div>Hello world</div>;
};

هنگامی که آن را تعریف می‌کنیم بدون مشکل به نظر می‌رسد اما هنگام استفاده، مشکل نمایان می‌شود:

// 'Component' cannot be used as a JSX component.
//   Its return type 'ReactNode' is not a valid JSX element.
<Component />

این موضوع به این دلیل است که تایپ اسکریپت از تعریف JSX.Element استفاده می‌کند تا بررسی کند که آیا چیزی می‌تواند به عنوان JSX رندر شود یا خیر. و از آنجایی که React.ReactNode حاوی مواردی است که JSX نیستند، نمی‌توان از آن به عنوان یک المنت JSX استفاده کرد.

اما از نسخه ۵٫۱ تایپ اسکریپت به بعد، React.ReactNode به خوبی کار می‌کند. زیرا تغییراتی ایجاد کرد که روشی که تایپ اسکریپت از آن برای استنباط تایپ‌های کامپوننت‌های React استفاده می‌کرد، بهبود بخشید.

بررسی React.ReactElement

یک تایپ دیگر به نام React.ReactElement وجود دارد که در این بخش می‌خواهیم درمورد آن صحبت کنیم.

React.ReactElement یک تایپ آبجکت است که به صورت زیر تعریف می‌شود:

interface ReactElement<
  P,
  T extends string | JSXElementConstructor<any>
> {
  type: T;
  props: P;
  key: Key | null;
}

این نشان دهنده object representation المنتی است که ما در حال رندر کردن آن هستیم. اگر بخواهیم خروجی یک عبارت JSX را در console.log بینیم، به شکل زیر خواهد بود:

// { type: 'div', props: { children: [] }, key: null }
console.log(<div />);

ما می‌توانیم از React.ReactElement در هر قسمتی به جای تایپ JSX.Element استفاده کنیم، تقریباً مانند یک alias name (نام مستعار) عمل می‌کند. در واقع، بسیاری از کامپوننت‌ها به این شکل حاشیه‌نویسی می‌شوند:

const Component = (): React.ReactElement => {
  return <div>Hello world</div>;
};

اما، درست مانند JSX.Element، هنگامی که سعی می‌کنیم رشته، عدد یا undefined را به عنوان child ارسال کنیم، مشکل ایجاد می‌شود. ما یک خطا مانند خطای زیر دریافت خواهیم کرد:

Type 'string' is not assignable to type 'ReactElement<any, string | JSXElementConstructor<any>>'.
const Component = (): React.ReactElement => {
  // Type 'string' is not assignable to type
  // 'ReactElement<any, string | JSXElementConstructor<any>>'.
  return "123";
};

بنابراین، React.ReactElement مانند نام مستعار برای JSX.Element است. قوانین مشابهی دارند بنابراین ما نباید از آن استفاده کنیم.

جمع‌بندی

با بررسی نتایجی که از مطالعه این مقاله به دست آوریم، تقریباً هرگز نباید از JSX.Element یا React.ReactElement در کد خود استفاده کنیم. آن‌ها تایپ‌هایی هستند که به صورت داخلی توسط تایپ اسکریپت برای نشان دادن نوع بازگشتی عبارات JSX مورد استفاده قرار می‌گیرند.

در عوض، باید از React.ReactNode برای تایپ childهای کامپوننت‌های خود استفاده کنیم. همچنین پیشنهاد می‌شود برای جلوگیری از سردرگمی، تایپ‌های بازگشتی کامپوننت‌های خود را حاشیه‌نویسی نکنیم، اما اگر از نسخه ۵٫۱ تایپ اسکریپت استفاده می‌کنیم، باید به همان ترتیب قبلی راه خود را ادامه دهیم.

دیدگاه‌ها:

افزودن دیدگاه جدید