آیا تا به حال با کدهای یک پروژه React مواجه شدهاید و از خود پرسیدهاید که چرا همه چیز اینقدر آشفته است؟ آیا سعی کردهاید قابلیتی را به یک کامپوننت React که توسط شخص دیگری نوشته شده اضافه کنید و احساس کردهاید که نیاز به بازنویسی کامل آن دارید؟ آیا مدیریت وضعیت (state) و پراپس (props) یک کامپوننت و فرزندان آن برایتان کابوسوار بوده است؟ اگر پاسخ شما به این پرسشها مثبت است، تنها نیستید. این حس در میان بسیاری از توسعهدهندگان React در سراسر جهان رایج است. اما خود React مسئول این مشکلات نیست. چنین شرایطی به دلیل "بوی بد کد" (Code Smells) ایجاد میشوند؛ نشانههایی که اگرچه ممکن است کد در حال حاضر کار کند، اما نگهداری، استفاده مجدد، مقیاسپذیری و دیباگ کردن آن بسیار دشوار است. یکی از نمونههای بارز این وضعیت، پیادهسازیهای سنتی و بدون ساختار از کامپوننتهای مودال (Modal) است که با چالشهای جدی مواجه میشوند. این چالشها میتوانند تجربه توسعه را پیچیده و ناکارآمد کنند.
یکی از بارزترین مشکلات در کامپوننتهای مودال سنتی، کمبود شدید انعطافپذیری آنهاست. این کامپوننتها اغلب دارای یک ساختار صلب هستند که دقیقاً دیکته میکند چه چیزی را باید رندر کنند. برای مثال، یک مودال ممکن است به عنوان پراپس، یک عنوان (title)، بدنه (body) و دو دکمه عملیاتی (primaryAction و secondaryAction) را بپذیرد. اما اگر نیاز داشته باشید یک مودال بدون عنوان داشته باشید؟ یا مودالی با یک چیدمان کاملاً سفارشی؟ یا حتی بیش از دو دکمه عملیاتی؟ در چنین شرایطی، مجبورید برای هر مورد استفاده جدید، منطق اضافی بنویسید و پراپسهای بیشتری را اضافه کنید. این تغییرات مکرر در کامپوننت، منجر به مسائل نگهداری و افزایش "بوی بد کد" میشود، زیرا کامپوننت به طور مداوم برای سناریوهای خاص دستکاری میگردد و از هدف اصلی خود که ارائه یک راهحل عمومی و انعطافپذیر است، فاصله میگیرد.
کامپوننتهای مودال سنتی غالباً سعی میکنند چندین کار را به صورت همزمان انجام دهند. به عنوان مثال، یک کامپوننت مودال ممکن است هم مسئولیت مدیریت چیدمان (layout) مودال (مانند نمایش پسزمینه و موقعیتدهی) و هم مسئولیت رندر کردن محتوا (مانند عنوان، متن اصلی و دکمهها) را بر عهده داشته باشد. این رویکرد، اصل "جدایی دغدغهها" (Separation of Concerns) را نقض میکند که یکی از اصول اساسی در طراحی نرمافزار است و از الگوهای طراحی دیگر مانند الگوی Container-Presenter میآموزیم. هنگامی که یک کامپوننت مسئولیتهای متعددی را بر عهده میگیرد، قابلیت استفاده مجدد آن به شدت کاهش مییابد. به دلیل ساختار صلب و تداخل مسئولیتها، اگر نیاز به مودالی با محتوای متفاوت یا ساختار جزئی تغییریافته داشته باشید، اغلب نمیتوانید از کامپوننت موجود استفاده کنید و در نهایت مجبور به ایجاد یک کامپوننت مودال جدید و تکراری خواهید شد. این امر به مرور زمان به انباشت کدهای تکراری و افزایش پیچیدگی پروژه میانجامد.
یکی دیگر از مشکلات اساسی کامپوننتهای مودال سنتی، عدم مقیاسپذیری آنهاست. تصور کنید در حال توسعه یک کتابخانه کامپوننت هستید و نیاز به انواع مختلفی از مودالها دارید: مانند `ConfirmationModal` (مودال تأیید), `InfoModal` (مودال اطلاعات), `FormModal` (مودال فرم) و `ImageModal` (مودال تصویر). اگر بخواهید برای هر یک از این موارد، یک کامپوننت مودال کاملاً جدید ایجاد و نگهداری کنید، این امر ضربه بزرگی به مقیاسپذیری و پایداری کتابخانه کامپوننت شما وارد میکند. مدیریت و بهروزرسانی نسخههای متعدد از یک مفهوم اصلی، به سرعت غیرقابل کنترل میشود. علاوه بر این، پیادهسازیهای سنتی مودال به دلیل وابستگی شدید به پراپسها، آزمایشپذیری دشواری دارند. هرچه پراپسهای بیشتری برای کنترل ساختار و محتوای مودال پاس داده شوند، نوشتن تستهای جامع و موثر که تمام حالات ممکن را پوشش دهند، پیچیدهتر و زمانبرتر خواهد بود. این مشکلات در مجموع نشان میدهند که رویکرد سنتی به طراحی مودالها، راه حلی ناکارآمد برای پروژههای مدرن و مقیاسپذیر React است و نیاز به الگوهای طراحی پیشرفتهتری مانند الگوی کامپوننتهای ترکیبی (Compound Components Pattern) وجود دارد تا این چالشها را به شکلی کارآمد برطرف کند.
آیا تا به حال سورس کد یک پروژه React را باز کردهاید و از خود پرسیدهاید که چرا همه چیز اینقدر آشفته به نظر میرسد؟ آیا سعی کردهاید یک ویژگی را به کامپوننت React که توسط شخص دیگری ساخته شده اضافه کنید و احساس کردهاید که باید آن را از نو بنویسید؟ آیا در مدیریت وضعیت (state) و ویژگیها (props) برای یک کامپوننت و فرزندانش دچار کابوس شدهاید؟ اگر پاسخ شما به این پرسشها "بله!" است، تنها نیستید. این احساسی رایج در میان بسیاری از توسعهدهندگان React در سراسر جهان است.
اما خود React مسئول هیچیک از این مشکلات نیست. این وضعیتها به دلیل "بوی کد" (Code Smells) پدید میآیند، مشکلاتی مانند ارسال ویژگیها به شش سطح پایینتر (Props drilling)، یک کامپوننت حجیم که همه کارها را انجام میدهد، منطق تکراری در کامپوننتهای مختلف، و رندرینگهای بیدقت که باعث مشکلات عملکردی میشوند. "بوی کد" به معنای کد خراب نیست، بلکه نشانهای است که کد ممکن است اکنون کار کند، اما نگهداری، استفاده مجدد، مقیاسپذیری و دیباگ کردن آن بسیار دشوار است. دقیقاً همین جاست که نیاز به استفاده از الگوهای طراحی (Design Patterns) داریم. آنها راهحلهای آزمایششدهای برای مشکلات "بوی کد" هستند که توسعهدهندگان دهههاست با آنها روبرو بودهاند. وقتی میدانید چگونه از آنها به خوبی استفاده کنید، به یک کدبیس تمیز و قابل نگهداری دست مییابید که توسعه، دیباگ و مقیاسپذیری آن آسان است. امروز، ما به یکی از برجستهترین الگوهای طراحی در React به نام الگوی کامپوننتهای ترکیبی (Compound Components Pattern) میپردازیم. این الگو توسعهدهندگان React را از ارسال لیستهای طولانی ویژگیها نجات میدهد و به ساخت کامپوننتهای رابط کاربری قابل ترکیب کمک میکند.
الگوی کامپوننتهای ترکیبی در React یک الگوی طراحی است که در آن یک کامپوننت والد با کامپوننتهای فرزند خود همکاری میکند تا یک وضعیت و رفتار ضمنی را به اشتراک بگذارد. به جای ارسال لیستهای طولانی از ویژگیها (props)، کامپوننت والد وضعیت را مدیریت کرده و کامپوننتهای فرزندی انعطافپذیر (مانند `<Modal.Header>`، `<Modal.Body>` و `<Modal.Footer>`) را ارائه میدهد تا مصرفکنندگان بتوانند رابط کاربری را به طور طبیعی، درست مانند استفاده از عناصر HTML بومی، ترکیب کنند.
این الگو را مانند بلوکهای لگو در نظر بگیرید. کامپوننت والد مانند صفحه پایه لگو است. کامپوننتهای فرزند نیز مانند بلوکهای لگو (در، پنجره، سقف و غیره) هستند. شما هیچ ویژگی خاصی را به صفحه پایه نمیدهید که بگوید "یک در اینجا اضافه کن، یک پنجره آنجا اضافه کن." در عوض، به سادگی قطعات را در جایی که میخواهید قرار میدهید. صفحه پایه (والد) همچنان قوانین و ساختار (پایهها، تراز، پایداری) را فراهم میکند، اما شما انعطافپذیری لازم را برای مونتاژ مدل خود به هر شکلی که دوست دارید، خواهید داشت. این قدرت کامپوننتهای ترکیبی است: ترکیبی انعطافپذیر با وضعیت/رفتار مشترک در زیربنا. این الگو مشکلاتی نظیر عدم انعطافپذیری، مسئولیتهای ترکیبی، استفاده مجدد دشوار، مقیاسپذیری ضعیف و دشواری در تست را که در کامپوننتهای سنتیتر مشاهده میشود، برطرف میکند.
برای درک بهتر، بیایید به نحوه بازسازی یک کامپوننت Modal شلوغ با استفاده از این الگو بپردازیم. در یک کامپوننت Modal سنتی و غیرانعطافپذیر، ممکن است مجبور باشید ویژگیهایی مانند `title`، `body` و `primaryAction` را به آن ارسال کنید. این ساختار سفت و سخت، امکان ایجاد یک Modal بدون عنوان، با طرحبندی سفارشی یا با بیش از دو دکمه عملیاتی را دشوار میکند. اما با الگوی کامپوننتهای ترکیبی، کامپوننت Modal دیگر `title`، `body` و سایر موارد را به عنوان ویژگیها دریافت نمیکند. در عوض، یک ویژگی خاص به نام `children` را میپذیرد که امکان ارسال هر عنصر HTML، گروهی از HTMLها، JSX یا حتی یک کامپوننت React را فراهم میکند. این رویکرد انعطافپذیری بینظیری را به ارمغان میآورد؛ زیرا ما دیگر به ساختار خاصی برای ارسال به کامپوننت Modal محدود نیستیم.
کامپوننت JSXِ Modal فقط ویژگی `children` را همانطور که هست رندر میکند و قدرت کامل را به مصرفکننده کامپوننت Modal میدهد تا هر ساختاری را که میخواهد ارسال کند. این کامپوننت از `backdrop` و `container style` برای تعیین ظاهر و حس اولیه یک Modal استفاده میکند و دکمهای برای بستن Modal با کلیک بر روی 'X' دارد. همچنین، ویژگیهای `isOpen` و `onClose` برای مدیریت باز و بسته شدن Modal استفاده میشوند. ما کامپوننتهای `ModalHeader`، `ModalBody` و `ModalFooter` را تعریف میکنیم که به همان اندازه انعطافپذیر هستند و هر ساختار HTML یا کامپوننت React معتبری را از طریق ویژگی `children` میپذیرند. این کامپوننتهای فرعی تنها در بستر Modal معنا پیدا میکنند و وجود مستقلی فراتر از آن ندارند، که این رویکرد به کشفپذیری و جلوگیری از سوءاستفاده کمک میکند.
نمونه دیگر، ساخت کامپوننت Accordion است. در اینجا نیز از همین الگو پیروی میکنیم: یک کامپوننت `Accordion` که `children` را میپذیرد و `AccordionItem` که `title` و `children` را برای محتوا دریافت میکند. این انعطافپذیری به ما امکان میدهد تا حتی یک Accordion کامل را درون `<Modal.Body>` قرار دهیم، بدون اینکه نیازی به تغییر کامپوننت Modal باشد. این قابلیت ترکیبپذیری، مانند مونتاژ قطعات لگو برای ساخت هر نوع Modal یا Accordion دلخواه، بدون نیاز به کامپوننتهای مختلف برای هر سناریو، یک پیشرفت بزرگ است.
این الگو کاربردهای گستردهای دارد. میتوانید از آن برای ساخت کامپوننتهای قابل استفاده مجدد مانند جداول (`Table.Head`، `Table.Body`، `Table.Row`)، یا هر کامپوننتی که ترتیب و تو در تو بودن طرحبندی در آن اهمیت دارد، استفاده کنید. اگر در حال ساخت یک کتابخانه کامپوننت یا سیستم طراحی خود هستید، این الگو یک ضرورت است، همانطور که در کتابخانههایی مانند ShadCN، Material UI یا Radix UI مشاهده میشود.
با این حال، مانند هر الگوی قدرتمندی، چالشها و ضدهالگوهایی نیز وجود دارد که باید از آنها آگاه باشید:
درک و به کارگیری این ملاحظات به شما کمک میکند تا از مزایای کامل الگوی کامپوننتهای ترکیبی بهرهمند شوید و از بروز مشکلات احتمالی جلوگیری کنید. با استفاده صحیح از این الگو، به یک کدبیس قدرتمند و انعطافپذیر دست خواهید یافت که نه تنها از "بوی کد" دور میماند، بلکه نگهداری و گسترش آن در آینده آسان خواهد بود.
آیا تا به حال با کدهای نامنظم و به هم ریخته در پروژههای React مواجه شدهاید که افزودن یک ویژگی کوچک به کامپوننتهای موجود، احساس بازنویسی کامل آن را در شما ایجاد کرده باشد؟ بسیاری از توسعهدهندگان React در سراسر جهان با این چالشها دست و پنجه نرم میکنند و اغلب دلیل آن، نه خود React، بلکه وجود «Code Smells» یا "بوهای کد" در پروژه است. این بوهای کد نشان میدهند که کد ممکن است در حال حاضر کار کند، اما نگهداری، استفاده مجدد، مقیاسپذیری و دیباگ آن دشوار است. یکی از نمونههای رایج این مشکل، کامپوننت Modal است که اغلب با ساختاری سفت و سخت و لیست طولانی از پراپها، توسعهدهنده را با چالش مواجه میکند. در این بخش، به طور عملی به بازنویسی یک کامپوننت Modal نامرتب با استفاده از الگوی قدرتمند «Compound Components» میپردازیم تا کد تمیزتر، قابل نگهداریتر و مقیاسپذیرتری داشته باشیم.
تصور کنید یک پیادهسازی ساده از کامپوننت Modal در React دارید که عنوان (title)، بدنه (body) و دو دکمه عملیاتی (primaryAction و secondaryAction) را به عنوان پراپ میپذیرد. این کامپوننت به ظاهر ساده، میتواند مشکلات متعددی را در بلندمدت ایجاد کند:
با در نظر گرفتن این مسائل، روشن است که نیاز به یک رویکرد بهتر برای ساخت کامپوننتهای رابط کاربری داریم که انعطافپذیری، قابلیت استفاده مجدد و مقیاسپذیری را به ارمغان آورد.
الگوی Compound Components در React، راه حلی ظریف و قدرتمند برای چالشهای فوق ارائه میدهد. در این الگو، یک کامپوننت والد (Parent) با کامپوننتهای فرزند (Child) خود همکاری میکند تا یک حالت (State) و رفتار ضمنی را به اشتراک بگذارد. به جای ارسال یک لیست طولانی از پراپها، کامپوننت والد مسئولیت مدیریت حالت اصلی را بر عهده میگیرد و کامپوننتهای فرزند منعطف را (مانند <Modal.Header>, <Modal.Body>, <Modal.Footer>) نمایش میدهد تا مصرفکنندگان بتوانند رابط کاربری را به طور طبیعی، درست مانند استفاده از عناصر HTML بومی، ترکیب کنند.
این الگو را مانند قطعات لگو تصور کنید: کامپوننت والد مانند صفحه پایه لگو است و کامپوننتهای فرزند مانند بلوکهای لگو (در، پنجره، سقف و غیره). شما به صفحه پایه نمیگویید که "اینجا یک در بگذار، آنجا یک پنجره بگذار." بلکه به سادگی قطعات را در جایی که میخواهید قرار میدهید. صفحه پایه (والد) همچنان قوانین و ساختار (پایهها، تراز، پایداری) را فراهم میکند، اما شما انعطافپذیری لازم برای مونتاژ مدل خود به هر شکلی که دوست دارید را خواهید داشت. این قدرت کامپوننتهای ترکیبی است: ترکیبی منعطف با حالت و رفتار مشترک در زیر آن.
برای بازسازی کامپوننت Modal نامرتب خود، ابتدا یک ساختار پوشهای جدید ایجاد میکنیم (به عنوان مثال، src/with-pattern/modal). سپس، فایل Modal.jsx جدید را در این پوشه ایجاد میکنیم. در این پیادهسازی جدید، کامپوننت Modal دیگر پراپهایی مانند title یا body را نمیپذیرد. بلکه، یک پراپ ویژه به نام children را قبول میکند که میتواند شامل هر عنصر HTML، گروهی از HTMLها، JSX، یا حتی یک کامپوننت React باشد. این ویژگی، انعطافپذیری بینظیری را فراهم میکند؛ زیرا دیگر به هیچ ساختار خاصی برای انتقال محتوا به کامپوننت Modal محدود نیستیم.
کامپوننت Modal فقط پراپ children را همانطور که هست رندر میکند و قدرت کامل را به مصرفکننده میدهد تا هر ساختاری را که مایل است، به آن منتقل کند. این کامپوننت از استایلهای پایه برای پسزمینه (backdrop) و کانتینر Modal استفاده میکند و همچنین یک دکمه برای بستن Modal با کلیک بر روی 'x' را در خود جای میدهد. برای باز و بسته کردن Modal، دو پراپ isOpen و onClose اضافه شدهاند که isOpen نشاندهنده وضعیت باز یا بسته بودن Modal است و onClose تابعی است که وضعیت isOpen را به false تغییر میدهد.
در کنار کامپوننت اصلی Modal، سه کامپوننت دیگر با نامهای ModalHeader، ModalBody و ModalFooter تعریف میکنیم. این سابکامپوننتها نیز به همین اندازه منعطف هستند و هر ساختار HTML معتبر یا کامپوننت React را از طریق پراپ children میپذیرند. به این ترتیب، میتوان هر محتوایی را برای سربرگ، بدنه و پاورقی Modal به آنها منتقل کرد. نکته مهم این است که این ModalHeader، ModalBody و ModalFooter به عنوان زیرمجموعههایی از کامپوننت Modal در نظر گرفته میشوند و به عنوان ویژگیهایی از آن (مانند Modal.Header) اکسپورت میشوند. این رویکرد تضمین میکند که این سابکامپوننتها فقط در بستر Modal معنیدار باشند و از استفاده نادرست جلوگیری میکند. دلیلی برای ایجاد فایلهای جداگانه برای این سابکامپوننتها وجود ندارد، زیرا آنها فقط در زمینه Modal کاربرد دارند و مستقل از آن، فاقد کارایی هستند.
برای استفاده از Modal جدید، کافی است آن را در فایل App.jsx ایمپورت کرده و ساختار مورد نظر خود را با استفاده از سابکامپوننتها در داخل آن قرار دهید. به عنوان مثال:
<Modal isOpen={isOpen} onClose={() => setIsOpen(false)}>
<Modal.Header>
<h2>عنوان سفارشی Modal</h2>
</Modal.Header>
<Modal.Body>
<p>محتوای منعطف و دلخواه شما.</p>
<p>شامل پاراگراف، تصویر، یا حتی یک کامپوننت دیگر.</p>
</Modal.Body>
<Modal.Footer>
<button>لغو</button>
<button>تأیید</button>
<button>ذخیره</button>
</Modal.Footer>
</Modal>
با این روش، میتوانید کامپوننتها را مانند بلوکهای لگو با هم ترکیب کنید و هر نوع Modal مورد نیاز خود را بسازید، بدون اینکه نیازی به ایجاد کامپوننتهای Modal جداگانه برای هر سناریو باشد. این یک گام بزرگ به سمت دستیابی به کد تمیز، قابل نگهداری، قابل استفاده مجدد و مقیاسپذیر در پروژههای React است.
پس از آشنایی با مبانی الگوی Compound Components و کاربرد آن در ساخت کامپوننتهای انعطافپذیر مانند Modal، زمان آن رسیده که این الگو را در سناریویی دیگر پیادهسازی کنیم: ساخت یک کامپوننت Accordion. Accordion یک المان رابط کاربری رایج است که مجموعهای از آیتمها را نمایش میدهد و هر آیتم شامل یک سربرگ و یک بدنه محتوا است. با کلیک بر روی سربرگ، محتوای مربوطه نمایش داده شده یا پنهان میشود. این پیادهسازی کلاسیک، به خوبی تواناییهای الگوی کامپوننتهای ترکیبی را در ایجاد رابطهای کاربری قابل ترکیب و نگهداری نشان میدهد.
برای پیادهسازی Accordion با استفاده از الگوی Compound Components، مسیری مشابه کامپوننت Modal را دنبال میکنیم. ابتدا یک کامپوننت والد به نام Accordion ایجاد میکنیم که مسئول مدیریت وضعیت کلی و ارائه ساختار پایه است. این کامپوننت Accordion، خود هیچ محتوای خاصی را مستقیماً رندر نمیکند، بلکه تنها ویژگی خاص children را میپذیرد. این رویکرد به کامپوننت Accordion امکان میدهد تا هر نوع المان HTML، JSX یا حتی کامپوننت React دیگری را به عنوان فرزند بپذیرد و انعطافپذیری فوقالعادهای را به مصرفکننده آن اعطا کند.
در مرحله بعد، کامپوننتهای فرزند را تعریف میکنیم. برای Accordion، ما به AccordionItem نیاز داریم. هر AccordionItem دو پراپ کلیدی را دریافت میکند: title برای ایجاد سربرگ قابل کلیک و همچنین پراپ ویژه children که محتوای منعطف بدنه Accordion را شکل میدهد. سربرگ AccordionItem با استفاده از یک دکمه ایجاد میشود که وضعیت isOpen را برای نمایش یا پنهان کردن محتوا مدیریت میکند. محتوای بدنه یک AccordionItem میتواند هر چیزی باشد: یک پاراگراف، یک جدول، یک تصویر، یا حتی ترکیبی پیچیده از JSX. در نهایت، AccordionItem به عنوان یک زیرکامپوننت به کامپوننت والد Accordion اضافه میشود تا ساختار منطقی و کاربردی الگوی کامپوننتهای ترکیبی حفظ گردد. برای بهبود ظاهر، میتوانیم استایلهای CSS لازم را نیز به پروژه اضافه کنیم.
قدرت اصلی الگوی Compound Components در اینجا نمود پیدا میکند: کامپوننت Accordion قادر است تا مجموعهای از کامپوننتهای AccordionItem را بپذیرد. این بدان معناست که شما میتوانید به سادگی چندین AccordionItem را مستقیماً در داخل Accordion قرار دهید و هر یک از آنها محتوای خاص خود را داشته باشند. انعطافپذیری به اینجا ختم نمیشود؛ شما حتی میتوانید یک آرایه از کامپوننتهای AccordionItem را به صورت پویا ایجاد کرده و آنها را به کامپوننت Accordion ارسال کنید. این رویکرد برای سناریوهایی که محتوای Accordion از یک منبع داده خارجی یا یک لیست پویا میآید، بسیار کاربردی است.
هر AccordionItem، همانطور که اشاره شد، پراپ title را برای سربرگ خود میپذیرد و از پراپ children برای نمایش محتوای داخلی بهره میبرد. این محتوا میتواند یک متن ساده باشد، یا هر JSX معتبر دیگری که شامل کامپوننتهای پیچیدهتر، فرمها، یا نمایش دادههای ساختاریافته است. این قابلیت، به توسعهدهندگان اجازه میدهد تا بدون نیاز به تغییر منطق اصلی کامپوننت Accordion، رابطهای کاربری بسیار متنوع و سفارشیسازی شدهای را ایجاد کنند.
یکی از جذابترین جنبههای الگوی Compound Components، قابلیت ترکیب پذیری (Composability) آن است. فرض کنید میخواهیم کامپوننت Accordion را که ساختیم، در داخل کامپوننت Modal قرار دهیم. آیا این کار بدون ایجاد تغییرات در ساختار Modal امکانپذیر است؟ پاسخ قاطعانه "بله" است! همانطور که پیشتر دیدیم، کامپوننت Modal و به تبع آن Modal.Body، هر دو پراپ children را میپذیرند و میتوانند هر JSX معتبری را رندر کنند. این یعنی هیچ محدودیتی برای نوع محتوایی که میتوانیم به آنها بدهیم وجود ندارد.
بنابراین، میتوانیم به سادگی کامپوننت کامل AccordionDemo (که شامل منطق Accordion ما است) را به فایل App.jsx وارد کرده و آن را در داخل تگ <Modal.Body>…</Modal.Body> قرار دهیم. نتیجه این ترکیب، نمایش یک Accordion کاملاً کارا در داخل Modal خواهد بود. مهمتر از آن، وضعیتهای مستقل هر دو کامپوننت (باز و بسته شدن Modal، و نمایش یا پنهان شدن محتوای آیتمهای Accordion) بدون هیچ تداخلی حفظ میشوند. این نشاندهنده قدرت الگوی Compound Components در ایجاد کامپوننتهایی است که نه تنها قابل استفاده مجدد هستند، بلکه میتوانند به راحتی با یکدیگر ترکیب شوند تا رابطهای کاربری پیچیدهتری را بدون افزایش بکر و پیچیدگی کد ایجاد کنند.
نمونههای Modal و Accordion، تنها دو مورد از کاربردهای مهم الگوی Compound Components هستند. این الگو به طور مشابه میتواند برای ساخت کامپوننتهای قابل استفاده مجدد دیگری نیز به کار رود، مانند: جداول (Table.Head، Table.Body، Table.Row) و هر کامپوننت دیگری که ساختار و تو در تو بودن المانها در آن اهمیت دارد. در واقع، اگر در حال ساخت یک کتابخانه کامپوننت یا سیستم طراحی (Design System) هستید، این الگو یک ضرورت است. پروژههایی مانند ShadCN، Material UI و Radix UI از این الگو به شکل گسترده استفاده میکنند.
با این حال، مانند هر الگوی قدرتمند دیگری، الگوی Compound Components نیز دارای نکات و الگوهای اشتباهی (Anti-Patterns) است که باید از آنها آگاه باشید. اطمینان حاصل کنید که زیرکامپوننتها را به صورت تصادفی و بدون رعایت معنی و مفهوم به والد متصل نمیکنید؛ آنها باید از نظر معنایی به کامپوننت والد خود تعلق داشته باشند. همچنین، از صدور (re-export) جداگانه زیرکامپوننتها خودداری کنید. استفاده از یک ModalFooter بدون Modal والد، میتواند منجر به فاجعه شود، زیرا منطق و ظاهر آنها به هم وابسته است. در نهایت، تلاش نکنید که همه چیز را با الگوی Compound Components پیادهسازی کنید. قانون کلی این است که تنها زمانی از آن استفاده کنید که ساختار فرزندان اهمیت دارد و شما به دنبال حفظ انعطافپذیری در ترکیب UI هستید.
آیا تا به حال سورس کد یک پروژه React را باز کردهاید و از آشفتگی آن متعجب شدهاید؟ یا قابلیتی را به کامپوننتی که توسط دیگری ساخته شده اضافه کنید و احساس کنید باید آن را از نو بنویسید؟ این احساس در مدیریت State و Props، در بین بسیاری از توسعهدهندگان React رایج است. اما React به خودی خود مسئول این مشکلات نیست. این وضعیتها به دلیل "بوهای کد" (Code Smells) مانند انتقال Props در شش سطح پایینتر، کامپوننتهای بزرگ که همه کارها را انجام میدهند، منطق تکراری، و رندرینگهای بیمورد که باعث مشکلات عملکردی میشوند، ایجاد میگردند. یک Code Smell نشانهای است که کد ممکن است اکنون کار کند، اما نگهداری، استفاده مجدد، مقیاسپذیری و دیباگ آن بسیار دشوار خواهد بود. دقیقاً همینجاست که الگوهای طراحی (Design Patterns) به کمک ما میآیند. آنها راهحلهای تستشدهای برای مشکلات مختلف Code Smell هستند. با استفاده صحیح از آنها، به یک پایگاه کد تمیز، قابل نگهداری دست مییابید که توسعه، دیباگ و مقیاسپذیری آن آسان است. امروز به بررسی عمیق یکی از برجستهترین الگوهای طراحی در React، الگوی کامپوننتهای ترکیبی (Compound Components Pattern)، میپردازیم. این الگو توسعهدهندگان را از ارسال لیستهای طولانی Props نجات میدهد و به ساخت کامپوننتهای رابط کاربری منعطف و قابل ترکیب کمک میکند.
برای درک بهتر الگوی کامپوننتهای ترکیبی، ابتدا به بررسی مشکلات یک پیادهسازی ساده از کامپوننت Modal میپردازیم. در یک Modal سنتی که Propsهایی مانند title، body، primaryAction و secondaryAction را میپذیرد، با چالشهایی روبرو هستیم. این مشکلات عبارتند از:
فقدان انعطافپذیری: ساختار Modal سختگیرانه است. اگر بخواهید یک Modal بدون عنوان، با چیدمان سفارشی یا بیش از دو دکمه عملیاتی داشته باشید، باید منطق اضافی نوشته و Propsهای بیشتری ارسال کنید. این تغییرات، نگهداری کد را دشوار و "بوی کد" را افزایش میدهد.
مسئولیتهای ترکیبی: Modal چندین کار را همزمان انجام میدهد؛ هم مسئولیت چیدمان و هم محتوا را بر عهده دارد که اصل تفکیک مسئولیتها را نقض میکند.
قابلیت استفاده مجدد پایین: به دلیل ساختار سختگیرانه، Modal قابلیت استفاده مجدد کمی دارد. اگر نیاز به Modalهای با ساختارهای متفاوت داشته باشید، مجبور به ایجاد کامپوننتهای جدید خواهید شد.
مقیاسپذیری ضعیف: در یک کتابخانه کامپوننت، ایجاد و نگهداری نمونههای متعدد Modal، ضربه بزرگی به مقیاسپذیری وارد میکند.
دشواری در تست: این پیادهسازی Modal به دلیل وابستگی شدید به Props، تست کردن را دشوار میسازد. با توجه به این مشکلات، الگوی کامپوننتهای ترکیبی میتواند راهحلی قدرتمند ارائه دهد.
الگوی کامپوننتهای ترکیبی یک راهکار قدرتمند برای ساخت کامپوننتهای UI منعطف و قابل استفاده مجدد در React است. این الگو به خصوص در سناریوهایی که نیاز به ترکیب پذیری بالا و کنترل دقیق بر ساختار داخلی کامپوننتها دارید، کاربرد فراوانی دارد. دیدیم که چگونه میتوان یک کامپوننت Modal آشفته را به یک Modal منعطف تبدیل کرد که از طریق زیرکامپوننتهایی مانند Modal.Header، Modal.Body و Modal.Footer قابل سفارشیسازی است. همچنین، میتوانیم یک کامپوننت Accordion را با همین الگو پیادهسازی کنیم.
علاوه بر Modal و Accordion، این الگو برای ساخت کامپوننتهای قابل استفاده مجدد دیگری نیز مفید است، مانند:
جداول (Tables): با زیرکامپوننتهایی نظیر Table.Head، Table.Body و Table.Row، ساختار جدول را به صورت Declarative مدیریت کنید.
هر کامپوننتی که چیدمان و تو در تو بودن آن اهمیت دارد: هر زمان که میخواهید مصرفکننده کامپوننت، کنترل کاملی بر روی نحوه نمایش و ترکیب بخشهای مختلف آن داشته باشد، این الگو بسیار کارآمد است.
کتابخانههای کامپوننت و سیستمهای طراحی: اگر در حال ساخت کتابخانه کامپوننت یا سیستم طراحی خود هستید، این الگو یک ضرورت است. پروژههای بزرگی مانند ShadCN، Material UI و Radix UI از این الگو به طور گسترده استفاده میکنند تا کامپوننتهایی قدرتمند و به راحتی قابل شخصیسازی ارائه دهند. این رویکرد، امکان ساخت رابطهای کاربری پیچیده را با سادگی و نگهداری بالا فراهم میکند.
الگوی کامپوننتهای ترکیبی نیز مانند هر الگوی طراحی دیگری، دامها و ضدالگوهای خاص خود را دارد که باید از آنها آگاه باشید. برای استفاده مؤثر از این الگو، به نکات زیر توجه کنید:
زیرکامپوننتها را به صورت تصادفی وصل نکنید. آنها باید از نظر معنایی به کامپوننت والد تعلق داشته باشند و بخشی منطقی از آن باشند.
از export کردن جداگانه زیرکامپوننتها خودداری کنید. این کار میتواند فاجعهبار باشد، به خصوص اگر کسی ModalFooter را بدون Modal والد آن استفاده کند. تغییرات آینده ممکن است مشکلات پیشبینینشدهای ایجاد کند. بهتر است زیرکامپوننتها را به عنوان پراپرتیهای کامپوننت والد (مثلاً Modal.Header) export کنید.
سعی نکنید هر چیزی را با الگوی کامپوننتهای ترکیبی بسازید. تنها زمانی از آن استفاده کنید که ساختار فرزندان (children) اهمیت دارد و میخواهید انعطافپذیری بالایی در ترکیب آنها داشته باشید. برای کامپوننتهای ساده، استفاده از این الگو میتواند پیچیدگی غیرضروری ایجاد کند. با رعایت این نکات، میتوانید از قدرت الگوی کامپوننتهای ترکیبی به بهترین شکل بهرهبرداری کرده و از بروز مشکلات احتمالی جلوگیری نمایید.
در این مقاله، به بررسی عمیق الگوی کامپوننتهای ترکیبی در React پرداختیم و دیدیم که چگونه این الگو میتواند به ما کمک کند تا از مشکلات رایج مانند Prop Drilling و کامپوننتهای حجیم رهایی یابیم و به سمت کدی تمیزتر، منعطفتر و قابل نگهداریتر حرکت کنیم. ما با مثالهایی از Modal و Accordion، قدرت این الگو در ساخت رابطهای کاربری قابل ترکیب و مقیاسپذیر را مشاهده کردیم. همچنین، به اهمیت درک کاربردها، دامها و ضدالگوهای مرتبط با آن اشاره کردیم تا بتوانیم از این ابزار قدرتمند به شکلی مسئولانه و مؤثر استفاده کنیم.
توصیه نهایی این است که الگوهای طراحی React را جدی بگیرید. آنها نه تنها به شما کمک میکنند تا مشکلات موجود در کد را شناسایی و حل کنید، بلکه مهارتهای تفکر و طراحی شما را به عنوان یک توسعهدهنده به شدت ارتقا میبخشند. با یادگیری و به کارگیری صحیح این الگوها، میتوانید کامپوننتهایی بسازید که نه تنها اکنون کار میکنند، بلکه در آینده نیز به راحتی قابل توسعه، دیباگ و نگهداری خواهند بود. همواره به دنبال بهبود کیفیت کد خود باشید و بهترین شیوهها را در پروژههایتان به کار ببرید. یادگیری مستمر کلید موفقیت در دنیای پرسرعت توسعه نرمافزار است.