Component Pattern باید یک رابطه مشخص در رابط را روشن کند: چه کسی State را نگه میدارد، کدام جزء ظاهر را میسازد و منطق مشترک کجا قرار میگیرد. انتخاب Compound Component، Render Prop یا Hook بدون این سؤالها فقط شکل کد را عوض میکند. یک Component با چند Prop روشن ممکن است برای نیاز کوچک کافی باشد. هدف، قراردادی است که مصرفکننده بتواند بدون دانستن جزئیات داخلی از آن استفاده کند و تغییرات بعدی رفتار غیرمنتظره نسازند.
برای مثال فرضی، Accordion از چند بخش مرتبط تشکیل میشود و باز بودن یک بخش بر بقیه اثر دارد. State میتواند داخل آن نگه داشته شود یا از صفحه کنترل شود. این دو حالت نیاز متفاوتی دارند: یک مجموعه پرسش مستقل در برابر مجموعهای که انتخاب بیرونی باید بخش خاصی را باز کند. Pattern باید همین رابطه را بیان کند. افزودن Context و Namespace به هر کارت ساده، لزوماً انعطاف مفیدی ایجاد نمیکند و میتواند هزینه فهم را افزایش دهد.
Composition را پیش از پیچیدگی بررسی کنید
راهنمای Props در React انتقال داده و children را توضیح میدهد. Wrapper میتواند قاب را بسازد و محتوا از children بیاید. این راه برای اشتراک چیدمان بدون کنترل همه جزئیات محتوا مناسب است. اگر یک Card به ده Prop برای عنوان، توضیح، تصویر، Badge و چند اقدام نیاز دارد، شاید Slotهای مشخص یا Composition قرارداد روشنتری بدهند. از طرف دیگر، محتوای کاملاً آزاد میتواند هویت و دسترسیپذیری جزء را از کنترل خارج کند.
مرز آزادی را بنویسید. مصرفکننده بداند کدام بخش اختیاری است و کدام ساختار لازم. نام Prop باید معنی محصولی یا رفتاری داشته باشد، نه فقط ظاهر. hasSomething و customMode در قرارداد بزرگ میتوانند مبهم شوند. اگر دو حالت واقعاً رفتار متفاوت دارند، آنها را جدا تعریف کنید. مقاله TypeScript با React درباره Union برای Props ناسازگار مفید است. قرارداد نوعی میتواند کمک کند، اما ساختار HTML و تعامل باید همان انتظار را اجرا کنند.
مالکیت State را روشن نگه دارید
اشتراک State در React ایده منبع واحد برای داده مشترک را توضیح میدهد. Controlled یعنی مقدار اصلی از بیرون میآید و جزء درخواست تغییر را اعلام میکند. Uncontrolled یعنی جزء مقدار را در محدوده خودش نگه میدارد و میتواند Default داشته باشد. وجود Callback به تنهایی Controlled نمیسازد؛ سؤال این است که مقدار معتبر نمایش از کجا میآید. نگهداری دو نسخه مستقل از همان انتخاب، خطر اختلاف ایجاد میکند.
defaultValue معمولاً مقدار آغازین است، نه فرمان دائمی برای همگام شدن. تغییر آن پس از شروع نباید بدون قرارداد به Reset ناگهانی تبدیل شود. در Controlled، والد باید درخواست تغییر را به مقدار تازه برگرداند؛ اگر این کار انجام نشود، ظاهر به مقدار معتبر قبلی وابسته میماند. همچنین تغییر میان Controlled و Uncontrolled در طول عمر جزء نیازمند سیاست روشن است. راهنمای input در React این تفاوت را برای کنترلهای ورودی بررسی میکند.
Compound Component برای بخشهای مرتبط است
Compound Component چند جزء مرتبط را زیر یک قرارداد مشترک قرار میدهد. Root مالک هماهنگی است و Itemها رفتار مناسب را از آن میگیرند. این الگو برای Accordion، Menu یا بعضی انتخابگرها قابل بررسی است، اما هر کدام رفتار دسترسیپذیری خود را دارند. نام Compound نباید باعث شود Tab، Menu و Accordion یک پیادهسازی عمومی با نقشهای نامناسب پیدا کنند. رابطه State و ساختار عمومی مشترک است؛ جزئیات تعامل از الگوی واقعی میآید.
Context میتواند داده هماهنگی را به بخشها برساند. مرجع useContext خواندن از Provider نزدیک و بهروزرسانی مصرفکنندگان را توضیح میدهد. Context محل ذخیره خود داده نیست؛ مقدار از Provider میآید. Context بسیار بزرگ با تغییرهای نامرتبط میتواند مصرفکنندگان زیادی را درگیر کند. برای مجموعه کوچک نمونه، قرارداد ساده کافی است؛ برای کتابخانه بزرگ، مرز ارزش و هزینه بهروزرسانی باید با نیاز واقعی بررسی شود.
نمونه کامل Accordion کنترلشده و مستقل
سه فایل زیر پروژه آموزشی مستقلی میسازند. Node.js سازگار با Vite 7 و npm لازماند. package.json و index.html در ریشه و main.jsx در پوشه src قرار میگیرند. npm install و npm run dev را در همان ریشه اجرا و نشانی اعلامشده ابزار را باز کنید. نمونه شبکه یا تصویر جدا ندارد. Accordion اول Controlled است و دکمه بیرونی آن را تغییر میدهد؛ Accordion دوم مقدار آغازین داخلی دارد. State این دو مجموعه مستقل است.
{"name":"component-pattern-demo","version":"1.0.0","private":true,"type":"module","scripts":{"dev":"vite","build":"vite build"},"dependencies":{"react":"^19.0.0","react-dom":"^19.0.0"},"devDependencies":{"vite":"^7.0.0"}}
<!doctype html><html lang="fa" dir="rtl"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>نمونه Component Patterns</title><style>body{font:1rem/1.8 Tahoma,sans-serif;max-width:44rem;margin:2rem auto;padding:1rem}button{font:inherit;padding:.5rem;margin:.3rem}button:focus-visible{outline:3px solid #111;outline-offset:3px}.item{border:2px solid #111;padding:1rem;margin:1rem 0}.item h3{margin:0}[hidden]{display:none}</style></head><body><div id="root"></div><script type="module" src="/src/main.jsx"></script></body></html>
در main.jsx، Root بر اساس وجود value تصمیم میگیرد مقدار داخلی یا بیرونی را نمایش دهد. null در این قرارداد یعنی هیچ بخشی باز نیست و undefined یعنی حالت Uncontrolled. Item از Context همان Root استفاده میکند و شناسههای DOM را با useId میسازد. شناسه تجاری id برای انتخاب بخش است و با شناسه DOM یکی فرض نشده است. اگر Item بیرون Root استفاده شود، خطای روشن میدهد تا نبود قرارداد به رفتار خاموش و مبهم تبدیل نشود.
import React,{createContext,useContext,useId,useState} from 'react';
import {createRoot} from 'react-dom/client';
const AccordionContext=createContext(null);
function AccordionRoot({value,defaultValue=null,onValueChange,children}){const [internal,setInternal]=useState(defaultValue);const controlled=value!==undefined;const open=controlled?value:internal;function toggle(id){const next=open===id?null:id;if(!controlled)setInternal(next);onValueChange?.(next)}return <AccordionContext.Provider value={{open,toggle}}><div>{children}</div></AccordionContext.Provider>}
function AccordionItem({id,title,children}){const context=useContext(AccordionContext);const generated=useId();if(!context)throw new Error('Accordion.Item requires Accordion root');const expanded=context.open===id;const buttonId=generated+'-button',panelId=generated+'-panel';return <section className="item"><h3><button id={buttonId} type="button" aria-expanded={expanded} aria-controls={panelId} onClick={()=>context.toggle(id)}>{title}</button></h3><div id={panelId} role="region" aria-labelledby={buttonId} hidden={!expanded}>{children}</div></section>}
const Accordion=Object.assign(AccordionRoot,{Item:AccordionItem});
function App(){const [open,setOpen]=useState('first');return <main><h1>مالکیت State</h1><h2>Controlled</h2><button type="button" onClick={()=>setOpen('second')}>باز کردن بخش دوم از والد</button><p role="status">انتخاب والد: {open??'هیچ'}</p><Accordion value={open} onValueChange={setOpen}><Accordion.Item id="first" title="بخش اول"><p>مقدار این مجموعه در App نگه داشته میشود.</p></Accordion.Item><Accordion.Item id="second" title="بخش دوم"><p>دکمه بیرونی نیز همان مقدار معتبر را تغییر میدهد.</p></Accordion.Item></Accordion><h2>Uncontrolled</h2><Accordion defaultValue="local"><Accordion.Item id="local" title="بخش مستقل"><p>این مجموعه State داخلی خودش را دارد و به انتخاب مجموعه بالا وابسته نیست.</p></Accordion.Item></Accordion></main>}
createRoot(document.getElementById('root')).render(<App/>);
دکمه واقعی در Heading قرار دارد و aria-expanded و aria-controls به وضعیت و پنل مربوطاند. پنل بسته با hidden از نمایش خارج میشود. الگوی Accordion در W3C رابطه Heading، Button و Panel را توضیح میدهد. نمونه از رفتار استاندارد دکمه برای Enter و Space استفاده میکند و ناوبری اختیاری با کلیدهای جهت را اضافه نکرده است. پس آن را پیادهسازی همه امکانات احتمالی کتابخانه Accordion معرفی نکنید.
در حالت Controlled، Item فقط onValueChange را فراخوانی میکند و والد State را عوض میکند. در حالت مستقل، Root مقدار خودش را تغییر میدهد. تابع toggle با کلیک روی بخش باز، مقدار را null میکند و هر بار حداکثر یک بخش باز است. این قاعده مخصوص همین نمونه است؛ برای چند بخش باز، شکل State و قرارداد باید عوض شود. اضافه کردن یک Boolean بدون اصلاح مدل مجموعه، رفتار چندانتخابی درست ایجاد نمیکند.
Hook منطق را مشترک میکند، نه خود State را
راهنمای Custom Hooks استفاده مجدد از منطق Stateدار را توضیح میدهد. دو فراخوانی Hook لزوماً State مشترک ندارند؛ هر مصرفکننده میتواند نمونه مستقل داشته باشد. اشتراک واقعی باید از منبع مشترک مناسب بیاید. Hook برای رفتار قابل استفاده مجدد مفید است، اما استخراج هر دو خط کد به نام useSomething الزاماً مرز خوبی نمیسازد. ورودی، خروجی و منابع ایجادشده باید مشخص باشند.
Hookی که Listener، Timer یا Subscription ایجاد میکند به Cleanup نیاز دارد. Rules of Hooks محل فراخوانی Hookها را مشخص میکند؛ شرطی کردن آنها بر اساس Prop میتواند ترتیب فراخوانی را بشکند. Contract منطق مشترک باید این محدودیت را برای مصرفکننده روشن کند. مقاله Closures درباره محیط Callback و عمر منابع زمینه دارد. اشتراک منطق، مسئولیت مالکیت و آزادسازی را حذف نمیکند.
Render Props و HOC هزینه قرارداد دارند
Render Prop تابعی از مصرفکننده میگیرد تا نمایش با داده فراهمشده ساخته شود. این روش وقتی کنترل ظاهر لازم است قابل بررسی است، اما تابع و داده باید قرارداد واضح داشته باشند. اگر children معمولی کافی است، تبدیل آن به تابع میتواند پیچیدگی بیدلیل بسازد. مصرفکننده باید بداند تابع چه زمانی و با چه دادهای اجرا میشود و اثر جانبی در آن نگذارد. نمایش قابل ترکیب با شروع عملیات شبکه هنگام Render یک کار نیست.
HOC یک Component را به Component دارای رفتار افزوده تبدیل میکند. در کدهای موجود ممکن است برای سازگاری یا نگرانی مشترک مفید باشد. باید درباره عبور Props، Ref، نام نمایشی و ترکیب چند Wrapper تصمیم روشن داشت. withAuth اگر فقط ظاهر را مخفی کند، مجوز Backend نمیسازد. مقاله طراحی API درباره بررسی حق عملیات توضیح دارد. Hook میتواند بعضی استفادهها را ساده کند، اما جایگزینی مکانیکی همه HOCها بدون بررسی قرارداد ضرورتی ندارد.
انعطاف را با محدودیت مفید همراه کنید
Component عمومی باید حالتهای مجاز را روشن کند و استفاده اشتباه را زود نشان دهد. صدها Prop برای هر تغییر ظاهری، مصرفکننده را به جزئیات داخلی وابسته میکند. تعداد زیادی Wrapper نیز میتواند پیدا کردن مالک State را دشوار سازد. مقاله Atomic Design درباره رابطه جزء و صفحه زمینه دارد. اشتراک واقعی از تکرار نیاز میآید؛ دو بخش با ظاهر نزدیک ولی رفتار متفاوت نباید بدون دلیل به یک قرارداد بسیار پیچیده تبدیل شوند.
در تحویل، مالک State، محدودیت Children، روش تغییر و رفتار دسترسیپذیری را ثبت کنید. اگر جزء فقط یک انتخاب باز دارد یا بعضی Keyboard Interactionها را پشتیبانی نمیکند، همین محدوده روشن باشد. مقاله عملکرد React برای هزینه Context و Render کمک میکند. شروع عملی، یک رابطه مشخص مثل هماهنگی Itemهای Accordion است؛ سادهترین قرارداد قابل فهم را بسازید و Pattern را فقط برای حل همان رابطه به کار ببرید.
هنوز دیدگاهی ثبت نشده
اولین نفری باشید که نظر میدهد!