مصطفی رحمتی
مصطفی رحمتی
خواندن ۷ دقیقه·۱ سال پیش

ایجاد کامپوننت‌های چندشکلی در ری‌اکت (Polymorphic Component) - بخش دوم

Polymorphic React Components With TypeScript - Part 2
Polymorphic React Components With TypeScript - Part 2


خوب تو قسمت قبل یکسری ملزوماتی رو برای پیاده‌سازی یک Polymorphic Component گفتیم، توی این قسمت قراره بیشتر دست به کد بشیم.

همونطور که در بخش اول گفتیم اگه بخواییم یک کامپوننت Polymorphic رو بدون Type Safety ایجاد کنیم، مشکل خاصی نداریم، پس از همین بخش شروع می‌کنیم و کم‌کم کامپوننت رو توسعه می‌دیم تا به چیزی که مدنظرمون هست تبدیل بشه.

ساده‌ترین روش ایجاد یک کامپوننت Polymorphic به صورت زیر هست:

https://gist.github.com/MR-Mostafa/e03a010c438c8e7744e339ac1e28dbfd

با استفاده از کد بالا، ما به همین راحتی سه مورد اول از ملزومات پیاده‌سازی یک Polymorphic Component در ری‌اکت رو ایجاد کردیم.


اگر یک توضیح مختصری بخوام در خصوص موارد گفته شده بگم، همونطور که مشاهده می‌کنید:

1. کامپوننت ما یک پراپس به عنوان as دریافت می‌کند (مورد اول از ملزومات)

2. این پراپرتی تعیین می‌کند که المنتی در DOM باید رندر شود چه چیزی می‌باشد (مورد دوم از ملزومات)

3. همچنین با استفاده از Rest parameters، کامپوننت ما از سایر اتریبیوت‌ها و ویژگی‌ها پیشتبانی می‌کند. (مورد سوم از ملزومات)

https://stackblitz.com/edit/example-polymorphic-step1?embed=1&file=src%2FApp.tsx&hideExplorer=1&hideNavigation=1

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



1- پراپرتی as هر چیزی رو به عنوان ورودی قبول می‌کند، حتی یک HTML المنت غیرمعتبر (تگ mostafa یک HTML المنت غیرمعتبر می‌باشد)

HTML المنت غیرمعتبر
HTML المنت غیرمعتبر

2- حتی اگر مقدار as یک HTML المنت معتبر باشد، ممکن است اتریبیوت‌های اشتباه وارد شود (اتریبیوت href متعلق به تگ span نمی‌باشد)

اتریبیوت اشتباه
اتریبیوت اشتباه

3- سومین مشکل هم در خصوص forwardRef هست. اگر کامپوننت ما از این ویژگی پشتیبانی کند، می‌تواند مقدار ref دریافت شده از کامپوننت، خارج از مقدار تعیین شده در تایپ‌اسکریپت باشد. در مثال زیر مقدار ref را یک HTMLButtonElement تعیین کرده‌ایم اما پراپرتی as برابر با تگ span می‌باشد!

مقدار ref اشتباه
مقدار ref اشتباه

رفع مشکلات گفته شده و Type Safety کردن کامپوننت

برای رفع مشکل ابتدا ما باید به دنبال راه‌حلی باشیم که بتوانیم نوع/تایپ کامپوننت را بر اساس مقدار ورودی اون (همون پراپس as) تعیین کنیم که فقط مقادیری را به عنوان ورودی بپذیرد که یا جزء HTML المنت‌ها معتبر باشند و یا یک کامپوننت ری‌اکتی باشد.

اینجاست که Generic‌ها در تایپ‌اسکریپت به کمک ما می‌یان.

با Generic ها می‌تونیم کامپوننت (تابع، کلاس‌ و ...)هایی بنویسیم که با نوع‌های داده‌ای مختلفی کار کنن. بجای اینکه کامپوننت‌مون وابسته به یک نوع داده خاص مثلا عددی یا رشته‌ای باشه. (منبع)


برای اینکه بتونیم نوع یک المنت رو در ری‌اکت مشخص کنیم باید به سراغ یک Utility Type در ری‌اکت به نام React.ElementType بریم که خودش یک جنریک تایپ هست.

https://gist.github.com/MR-Mostafa/6bec49dbc1d26e791cf2044260563480
همانطور که در کد بالا در خط 8 مشاهده می‌کنید، برای تعیین نوع جنریک تایپ (یعنی C) از کلید واژه extends استفاده کرده‌ایم که این بدین معناست که C باید یک نوع (type) معتبر برای React.ElementType باشد. که خودش یک Utility Type برای ری‌اکت است که بیانگر تمامی تگ‌های ولید و معتبر HTML و یا یک کامپوننت ری‌اکتی می‌باشد. (در نتیجه مقدار پراپس as می‌تواند یکی از این دو مورد باشد)

در خط 27 نیز مقدار پیش‌فرض المنت رندرشده در DOM را برابر با button قرارداده‌ایم.


با انجام این کار، مشکل اول برطرف می‌شه و کامپوننت Polymorphic ما فقط مقادیر مجاز رو قبول می‌کنه.



بریم سراغ رفع مشکل دوم، همونطور که در بخش قبلی گفتیم کامپوننت Polymorphic ما باید دارای این ویژگی باشد:

کامپوننت باید موارد زیر نیز پشتیبانی کند: (مورد سوم از ملزومات)
- اتریبیوت‌های سراسری/گلوبال مانند id یا class و ...
- از اتریبیوت‌های مختص با المنت تعیین شده، مانند src در تگ img یا href در تگ a و ...
- پراپس‌های کاستوم/سفارشی در کامپوننت‌های دیگر یا Third Party؛ مثلا کامپوننت Link در فریم‌ورک NextJs پراپس‌هایی مانند replace یا prefetch و ... را دارد.


دقیقا مشکل همینجاست که کامپوننت ما از هیچ اتریبیوتی (چه گلوبال و ...) به صورت Type Safety پشتیبانی نمی‌کنه. مثلا اگر مقدار پراپرس as را برابر با تگ a قرار دهید، طبیعتاً نیاز به اتریبیوت‌های مختص این تگ مانند href, target و ... داریم.

{ as?: C; children: React.ReactNode; } & { ...otherValidPropsBasedOnTheValueOfAs // ? یک چیزی شبیه به این }


How To Support Other Valid Props, Based On The Value Of As
How To Support Other Valid Props, Based On The Value Of As


برای اینکه این مورد رو برطرف کنیم نیاز هست به سراغ یک Utility Type دیگر از ری‌اکت بریم که عبارتند از:

  1. React.ComponentProps
  2. React.ComponentPropsWithRef
  3. React.ComponentPropsWithoutRef

برای این متوجه بشیم این تایپ‌ها چی هستن و کاربردشون چی هست من از Chat GPT برای پاسخ به این سوال کمک گرفتم، که در ادامه پاسخ Chat GPT رو می‌خونید:

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

React.ComponentProps:
این نوع تایپ برای نام‌گذاری و استفاده از پارامترهای ورودی کامپوننت‌ها استفاده می‌شود. به عبارتی، نوع این تایپ برابر است با نوع خصوصیت‌های پیش‌فرض کامپوننت. در واقع مجموعه‌ای از تمام پارامترهای ورودی کامپوننت را نمایش می‌دهد. این شامل پارامترهای خاصی می‌شود که در کد مورد استفاده مشخص شده است و پارامترهایی که برای کاربرد عمومی هستند (مانند کلاس‌های CSS، رویدادها، و غیره).

React.ComponentPropsWithRef:
این نوع تایپ نسخه‌ای از React.ComponentProps است که جهت استفاده در کامپوننت‌هایی که قابل استفاده از ref هستند، طراحی شده است. Refs در React به شما اجازه می‌دهند تا به طور مستقیم به عنصر DOM یا کامپوننت‌هایی که به آن تعلق دارند، دسترسی داشته باشید. React.ComponentPropsWithRef شامل پارامترهای ورودی عمومی کامپوننت است و همچنین یک پارامتر ref برای استفاده از ref به صورت ضروری دارد.

React.ComponentPropsWithoutRef:
این نوع تایپ همچنین نسخه‌ای از React.ComponentProps است، اما برخلاف React.ComponentPropsWithRef، پارامتر ref در آن حذف شده است. این برای استفاده در کامپوننت‌هایی مناسب است که نیازی به دسترسی مستقیم به ref ندارند و این پارامتر برای آن‌ها مورد نیاز نیست.


چون در این بخش قصد این رو نداریم که به موضوع Ref بپردازیم، پس به سراغ React.ComponentPropsWithoutRef می‌ریم و کد قبلی‌مون رو به صورت زیر اصلاح می‌کنیم:

https://gist.github.com/MR-Mostafa/f31ec08813dafd4fa6650dedfe7c2a89


الان با استفاده از این Utility Type، ما تنوستیم بخشی از مشکل دوم رو هم برطرف کنیم. به عبارت دیگر کامپوننت Polymorphic ما در حال حاظر از اتریبیوت‌های گلوبال، سفارشی و ... نیز به صورت Type Safety پشتیبانی می‌کند.



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

  1. در کد بالا، در خط 13 گفتیم که اگر مقدار as پاس داده نشده باشد، مقدار پیش‌فرض آن برابر با button در نظر گرفته شود (از نظر جاوا اسکریپت)، اما مشکلی که وجود دارد این هست که در این صورت اتریبیوت‌های مرتبط با تگ button مانند type، disabled و ... پشتیبانی نمی‌شود. چون جنریک تایپ ما در انتظار انتخاب یک نوع تگ هست و هیچ تگی هم انتخاب نشده است. (از نظر تایپ‌اسکریپت)
<Button>click here</Button>

جهت برطرف کردن این مورد ما باید در زمان تعریف کامپوننت مقدار پیش‌فرض جنریک تایپ را هم تعیین کنیم، یعنی آن را برابر با button قرار دهیم:

https://gist.github.com/MR-Mostafa/bc593f38eb347e4224c917424bc61109



مورد بعدی اینکه فرض کنید که Polymorphic کامپوننت ما پراپس‌های دیگری مانند color یا font و ... داشته باشد که این پراپرتی‌ها نیز در مقدار پاس داده شده به عنوان as نیز وجود داشته باشد. (یعنی پراپس‌ها با هم تداخل یا همپوشانی داشته باشند) در این صورت برای برطرف کردن این مشکل باید چه کاری انجام دهیم؟

فکر کنم این مورد رو در قالب کد توضیح بدم، بهتر باشه.
فعلا این کد رو داشته باشید، چون در ادامه می خوام این کد رو اصلاح کنیم و به چند تایپ مستقل تبدیلش کنیم:


https://gist.github.com/MR-Mostafa/1ad15ad8045aeaffd9652cf98a736b8b


کد بالا رو به صورت زیر ریفکتور کردیم:


https://gist.github.com/MR-Mostafa/24655ea37ebbe6c2003d88b8193a5f0d


برای رفع عدم تداخل یا هم‌پوشانی پراپرتی های تعیین شده (کاستوم)‌ با پراپرتی های گلوبال یا Third Party کاری که باید انجام بدیم، این هست که از Omit و Keyof در تایپ اسکریپت کمک می‌گیریم، که در نهایت به کد زیر می‌رسیم:


https://gist.github.com/MR-Mostafa/8be7ad06481748c7097606c5fd0bdea5#file-polymorphic-components-react-7-tsx-L11


الان می‌تونیم در کامپوننتی که نوشتیم از پراپرتی تعریف شده color استفاده کنیم. در مثال زیر ورودی این پراپرتی رو در اتریبیوت style استفاده می‌کنیم.


https://gist.github.com/MR-Mostafa/5e19836e824deadb27d570cc321cb524



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

این بخش آخر رو می‌سپارم به خودتون.




فقط یک مورد دیگه باقی موند و اونم بحث ref در مورد کامپوننت ها هست و اینکه چطوری این مقدار رو در کامپوننت های polymorphic به صورت داینامیک مدیریت کنیم، که ان‌شاءالله این مورد رو در قسمت بعدی بهش می‌پردازیم.


امیدوارم تا اینجا مفید واقع شده باشد.


reactری اکت
FrontEnd Developer (Javascript/ReactJS/NextJS)
شاید از این پست‌ها خوشتان بیاید