خوب تو قسمت قبل یکسری ملزوماتی رو برای پیادهسازی یک Polymorphic Component گفتیم، توی این قسمت قراره بیشتر دست به کد بشیم.
همونطور که در بخش اول گفتیم اگه بخواییم یک کامپوننت Polymorphic رو بدون Type Safety ایجاد کنیم، مشکل خاصی نداریم، پس از همین بخش شروع میکنیم و کمکم کامپوننت رو توسعه میدیم تا به چیزی که مدنظرمون هست تبدیل بشه.
سادهترین روش ایجاد یک کامپوننت Polymorphic به صورت زیر هست:
با استفاده از کد بالا، ما به همین راحتی سه مورد اول از ملزومات پیادهسازی یک Polymorphic Component در ریاکت رو ایجاد کردیم.
اگر یک توضیح مختصری بخوام در خصوص موارد گفته شده بگم، همونطور که مشاهده میکنید:
1. کامپوننت ما یک پراپس به عنوان as دریافت میکند (مورد اول از ملزومات)
2. این پراپرتی تعیین میکند که المنتی در DOM باید رندر شود چه چیزی میباشد (مورد دوم از ملزومات)
3. همچنین با استفاده از Rest parameters، کامپوننت ما از سایر اتریبیوتها و ویژگیها پیشتبانی میکند. (مورد سوم از ملزومات)
احتمالا با مواردی که در بخش قبل گفته شد، میتونید حدس بزنید که کامپوننت بالا چه مشکلاتی داره. قبل از برطرف کردن این مشکلات، بهتره اونها رو به صورت شفاف بشناسیم:
1- پراپرتی as هر چیزی رو به عنوان ورودی قبول میکند، حتی یک HTML المنت غیرمعتبر (تگ mostafa یک HTML المنت غیرمعتبر میباشد)
2- حتی اگر مقدار as یک HTML المنت معتبر باشد، ممکن است اتریبیوتهای اشتباه وارد شود (اتریبیوت href متعلق به تگ span نمیباشد)
3- سومین مشکل هم در خصوص forwardRef هست. اگر کامپوننت ما از این ویژگی پشتیبانی کند، میتواند مقدار ref دریافت شده از کامپوننت، خارج از مقدار تعیین شده در تایپاسکریپت باشد. در مثال زیر مقدار ref را یک HTMLButtonElement تعیین کردهایم اما پراپرتی as برابر با تگ span میباشد!
برای رفع مشکل ابتدا ما باید به دنبال راهحلی باشیم که بتوانیم نوع/تایپ کامپوننت را بر اساس مقدار ورودی اون (همون پراپس as) تعیین کنیم که فقط مقادیری را به عنوان ورودی بپذیرد که یا جزء HTML المنتها معتبر باشند و یا یک کامپوننت ریاکتی باشد.
اینجاست که Genericها در تایپاسکریپت به کمک ما مییان.
با Generic ها میتونیم کامپوننت (تابع، کلاس و ...)هایی بنویسیم که با نوعهای دادهای مختلفی کار کنن. بجای اینکه کامپوننتمون وابسته به یک نوع داده خاص مثلا عددی یا رشتهای باشه. (منبع)
برای اینکه بتونیم نوع یک المنت رو در ریاکت مشخص کنیم باید به سراغ یک Utility Type در ریاکت به نام React.ElementType بریم که خودش یک جنریک تایپ هست.
همانطور که در کد بالا در خط 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 // ? یک چیزی شبیه به این }
برای اینکه این مورد رو برطرف کنیم نیاز هست به سراغ یک Utility Type دیگر از ریاکت بریم که عبارتند از:
برای این متوجه بشیم این تایپها چی هستن و کاربردشون چی هست من از 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 میریم و کد قبلیمون رو به صورت زیر اصلاح میکنیم:
الان با استفاده از این Utility Type، ما تنوستیم بخشی از مشکل دوم رو هم برطرف کنیم. به عبارت دیگر کامپوننت Polymorphic ما در حال حاظر از اتریبیوتهای گلوبال، سفارشی و ... نیز به صورت Type Safety پشتیبانی میکند.
فکر کنم تنوسته باشید حدس بزنید که با انجام کد مطابق با موارد بالا، همچنان دو مشکل دیگر باقی مانده که نیازه برطرف بشه:
<Button>click here</Button>
جهت برطرف کردن این مورد ما باید در زمان تعریف کامپوننت مقدار پیشفرض جنریک تایپ را هم تعیین کنیم، یعنی آن را برابر با button قرار دهیم:
مورد بعدی اینکه فرض کنید که Polymorphic کامپوننت ما پراپسهای دیگری مانند color یا font و ... داشته باشد که این پراپرتیها نیز در مقدار پاس داده شده به عنوان as نیز وجود داشته باشد. (یعنی پراپسها با هم تداخل یا همپوشانی داشته باشند) در این صورت برای برطرف کردن این مشکل باید چه کاری انجام دهیم؟
فکر کنم این مورد رو در قالب کد توضیح بدم، بهتر باشه.
فعلا این کد رو داشته باشید، چون در ادامه می خوام این کد رو اصلاح کنیم و به چند تایپ مستقل تبدیلش کنیم:
کد بالا رو به صورت زیر ریفکتور کردیم:
برای رفع عدم تداخل یا همپوشانی پراپرتی های تعیین شده (کاستوم) با پراپرتی های گلوبال یا Third Party کاری که باید انجام بدیم، این هست که از Omit و Keyof در تایپ اسکریپت کمک میگیریم، که در نهایت به کد زیر میرسیم:
الان میتونیم در کامپوننتی که نوشتیم از پراپرتی تعریف شده color استفاده کنیم. در مثال زیر ورودی این پراپرتی رو در اتریبیوت style استفاده میکنیم.
الان ما یک کامپوننت چندریختی یا polymorphic داریم که تمام مواردی که گفتیم رو پشتیبانی میکنه. برای اینکه بتونیم از این تایپ در موارد گوناگون استفاده کنیم و نیاز نباشه به ازای هر کامپوننت یکبار تمام این مراجل رو پیش بریم، پیشنهاد می کنم که یک تایپ مجزا مثلا به نام PolymorphicWithoutRef در تایپ اسکریپت ایجاد کنید و حالا به ازای هر کامپوننت تنها نیاز هست که این تایپ رو بهش پاس بدید. (طبیعتا این تایپ هم باید جنریک باشد)
این بخش آخر رو میسپارم به خودتون.
فقط یک مورد دیگه باقی موند و اونم بحث ref در مورد کامپوننت ها هست و اینکه چطوری این مقدار رو در کامپوننت های polymorphic به صورت داینامیک مدیریت کنیم، که انشاءالله این مورد رو در قسمت بعدی بهش میپردازیم.
امیدوارم تا اینجا مفید واقع شده باشد.