راهنمای جامع API createPortal در React، شامل تکنیکهای ساخت پورتال، استراتژیهای مدیریت رویداد، و موارد استفاده پیشرفته برای ساخت رابطهای کاربری انعطافپذیر و دسترسپذیر.
React createPortal: تسلط بر ساخت پورتال و مدیریت رویدادها
در توسعه وب مدرن با React، ایجاد رابطهای کاربری که به طور یکپارچه با ساختار سند زیرین ادغام میشوند، حیاتی است. در حالی که مدل کامپوننت React در مدیریت DOM مجازی عالی عمل میکند، گاهی اوقات نیاز داریم که المانها را خارج از سلسلهمراتب عادی کامپوننتها رندر کنیم. اینجاست که createPortal وارد میشود. این راهنما به طور عمیق createPortal را بررسی میکند و هدف، کاربرد و تکنیکهای پیشرفته برای مدیریت رویدادها و ساخت المانهای UI پیچیده را پوشش میدهد. ما ملاحظات بینالمللیسازی، بهترین شیوههای دسترسپذیری و اشتباهات رایج برای اجتناب را پوشش خواهیم داد.
React createPortal چیست؟
createPortal یک API در React است که به شما امکان میدهد فرزندان یک کامپوننت React را در بخش دیگری از درخت DOM، خارج از سلسلهمراتب کامپوننت والد، رندر کنید. این قابلیت به ویژه برای ایجاد المانهایی مانند مودالها، تولتیپها، منوهای کشویی و روکشها (overlays) مفید است که باید در سطح بالای سند یا درون یک کانتینر خاص قرار گیرند، صرفنظر از اینکه کامپوننتی که آنها را فعال میکند در کجای درخت کامپوننت React قرار دارد.
بدون createPortal، دستیابی به این هدف اغلب نیازمند راهحلهای پیچیدهای مانند دستکاری مستقیم DOM یا استفاده از موقعیتدهی مطلق CSS است که میتواند منجر به مشکلاتی در زمینههای انباشتگی (stacking contexts)، تداخلات z-index و دسترسپذیری شود.
چرا از createPortal استفاده کنیم؟
در اینجا دلایل کلیدی که چرا createPortal ابزاری ارزشمند در جعبه ابزار React شماست، آورده شده است:
- ساختار DOM بهبودیافته: از تودرتو کردن عمیق کامپوننتها در DOM جلوگیری میکند و به ساختاری تمیزتر و قابل مدیریتتر منجر میشود. این امر به ویژه برای برنامههای پیچیده با بسیاری از المانهای تعاملی مهم است.
- استایلدهی سادهتر: به راحتی المانها را نسبت به viewport یا کانتینرهای خاص موقعیتدهی میکند بدون اینکه به ترفندهای پیچیده CSS تکیه کند. این کار استایلدهی و طرحبندی را ساده میکند، به خصوص هنگام کار با المانهایی که نیاز به قرار گرفتن روی محتوای دیگر دارند.
- دسترسپذیری پیشرفته: با امکان مدیریت فوکوس و ناوبری با صفحهکلید به طور مستقل از سلسلهمراتب کامپوننت، ایجاد رابطهای کاربری دسترسپذیر را تسهیل میکند. به عنوان مثال، اطمینان از باقی ماندن فوکوس در داخل یک پنجره مودال.
- مدیریت بهتر رویدادها: اجازه میدهد رویدادها به درستی از محتوای پورتال به درخت React منتشر شوند (propagate)، و اطمینان میدهد که شنوندگان رویداد (event listeners) متصل به کامپوننتهای والد همچنان طبق انتظار کار میکنند.
کاربرد پایه createPortal
API createPortal دو آرگومان را میپذیرد:
- نود React (JSX) که میخواهید رندر کنید.
- المان DOM که میخواهید نود را در آن رندر کنید. این المان DOM در حالت ایدهآل باید قبل از mount شدن کامپوننتی که از
createPortalاستفاده میکند، وجود داشته باشد.
در اینجا یک مثال ساده آورده شده است:
مثال: رندر کردن یک مودال
فرض کنید یک کامپوننت مودال دارید که میخواهید آن را در انتهای المان body رندر کنید.
import React from 'react';
import ReactDOM from 'react-dom';
function Modal({ children, isOpen, onClose }) {
if (!isOpen) return null;
const modalRoot = document.getElementById('modal-root'); // Assumes you have a <div id="modal-root"></div> in your HTML
if (!modalRoot) {
console.error('Modal root element not found!');
return null;
}
return ReactDOM.createPortal(
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
{children}
</div>
</div>,
modalRoot
);
}
export default Modal;
توضیح:
- ما
ReactDOMرا ایمپورت میکنیم زیراcreatePortalیک متد از شیءReactDOMاست. - ما فرض میکنیم که یک المان DOM با شناسه
modal-rootدر فایل HTML شما وجود دارد. اینجاست که مودال رندر خواهد شد. اطمینان حاصل کنید که این المان وجود دارد. یک روش متداول این است که یک<div id="modal-root"></div>درست قبل از تگ پایانی</body>در فایلindex.htmlخود اضافه کنید. - ما از
ReactDOM.createPortalبرای رندر کردن JSX مودال در المانmodalRootاستفاده میکنیم. - ما از
e.stopPropagation()استفاده میکنیم تا از فعال شدن کنترلکنندهonCloseروی روکش (overlay) توسط رویدادonClickروی محتوای مودال جلوگیری کنیم. این تضمین میکند که کلیک کردن داخل مودال آن را نمیبندد.
نحوه استفاده:
import React, { useState } from 'react';
import Modal from './Modal';
function App() {
const [isModalOpen, setIsModalOpen] = useState(false);
return (
<div>
<button onClick={() => setIsModalOpen(true)}>Open Modal</button>
<Modal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)}>
<h2>Modal Content</h2>
<p>This is the content of the modal.</p>
<button onClick={() => setIsModalOpen(false)}>Close</button>
</Modal>
</div>
);
}
export default App;
این مثال نشان میدهد که چگونه یک مودال را خارج از سلسلهمراتب عادی کامپوننت رندر کنیم، که به شما امکان میدهد آن را به صورت مطلق در صفحه موقعیتدهی کنید. استفاده از createPortal به این روش، مشکلات رایج مربوط به زمینههای انباشتگی (stacking contexts) را حل میکند و به شما امکان میدهد به راحتی استایلدهی مودال ثابتی را در سراسر برنامه خود ایجاد کنید.
مدیریت رویداد با createPortal
یکی از مزایای کلیدی createPortal این است که رفتار عادی حبابزدن رویداد (event bubbling) در React را حفظ میکند. این بدان معناست که رویدادهایی که از داخل محتوای پورتال سرچشمه میگیرند، همچنان به سمت بالای درخت کامپوننت React منتشر میشوند و به کامپوننتهای والد اجازه میدهند آنها را مدیریت کنند.
با این حال، مهم است که بفهمیم رویدادها هنگام عبور از مرز پورتال چگونه مدیریت میشوند.
مثال: مدیریت رویدادها در خارج از پورتال
import React, { useState, useRef, useEffect } from 'react';
import ReactDOM from 'react-dom';
function OutsideClickExample() {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef(null);
const portalRoot = document.getElementById('portal-root');
useEffect(() => {
function handleClickOutside(event) {
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
setIsOpen(false);
}
}
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [dropdownRef]);
return (
<div>
<button onClick={() => setIsOpen(!isOpen)}>Toggle Dropdown</button>
{isOpen && portalRoot && ReactDOM.createPortal(
<div ref={dropdownRef} style={{ position: 'absolute', top: '50px', left: '0', border: '1px solid black', padding: '10px', backgroundColor: 'white' }}>
Dropdown Content
</div>,
portalRoot
)}
</div>
);
}
export default OutsideClickExample;
توضیح:
- ما از یک
refبرای دسترسی به المان دراپداون رندر شده در داخل پورتال استفاده میکنیم. - ما یک شنونده رویداد
mousedownبهdocumentمتصل میکنیم تا کلیکهای خارج از دراپداون را تشخیص دهیم. - در داخل شنونده رویداد، با استفاده از
dropdownRef.current.contains(event.target)بررسی میکنیم که آیا کلیک خارج از دراپداون رخ داده است یا خیر. - اگر کلیک خارج از دراپداون رخ داده باشد، با تنظیم
isOpenبهfalseآن را میبندیم.
این مثال نشان میدهد که چگونه رویدادهایی را که خارج از محتوای پورتال رخ میدهند مدیریت کنیم، و به شما امکان میدهد المانهای تعاملی ایجاد کنید که به اقدامات کاربر در سند اطراف پاسخ میدهند.
موارد استفاده پیشرفته
createPortal تنها به مودالها و تولتیپهای ساده محدود نمیشود. میتوان از آن در سناریوهای پیشرفته مختلفی استفاده کرد، از جمله:
- منوهای زمینه (Context Menus): رندر کردن دینامیک منوهای زمینه در نزدیکی مکاننمای ماوس هنگام کلیک راست.
- اعلانها (Notifications): نمایش اعلانها در بالای صفحه، صرفنظر از سلسلهمراتب کامپوننت.
- پاپاورهای سفارشی (Custom Popovers): ایجاد کامپوننتهای پاپاور سفارشی با موقعیتدهی و استایلدهی پیشرفته.
- ادغام با کتابخانههای شخص ثالث: استفاده از
createPortalبرای ادغام کامپوننتهای React با کتابخانههای شخص ثالث که به ساختارهای DOM خاصی نیاز دارند.
مثال: ایجاد یک منوی زمینه
import React, { useState, useRef, useEffect } from 'react';
import ReactDOM from 'react-dom';
function ContextMenuExample() {
const [contextMenu, setContextMenu] = useState(null);
const menuRef = useRef(null);
useEffect(() => {
function handleClickOutside(event) {
if (menuRef.current && !menuRef.current.contains(event.target)) {
setContextMenu(null);
}
}
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [menuRef]);
const handleContextMenu = (event) => {
event.preventDefault();
setContextMenu({
x: event.clientX,
y: event.clientY,
});
};
const portalRoot = document.getElementById('portal-root');
return (
<div onContextMenu={handleContextMenu} style={{ border: '1px solid black', padding: '20px' }}>
Right-click here to open context menu
{contextMenu && portalRoot && ReactDOM.createPortal(
<div
ref={menuRef}
style={{
position: 'absolute',
top: contextMenu.y,
left: contextMenu.x,
border: '1px solid black',
padding: '10px',
backgroundColor: 'white',
}}
>
<ul>
<li>Option 1</li>
<li>Option 2</li>
<li>Option 3</li>
</ul>
</div>,
portalRoot
)}
</div>
);
}
export default ContextMenuExample;
توضیح:
- ما از رویداد
onContextMenuبرای تشخیص کلیکهای راست روی المان هدف استفاده میکنیم. - ما با استفاده از
event.preventDefault()از نمایش منوی زمینه پیشفرض جلوگیری میکنیم. - ما مختصات ماوس را در متغیر state به نام
contextMenuذخیره میکنیم. - ما منوی زمینه را در داخل یک پورتال، در مختصات ماوس، رندر میکنیم.
- ما همان منطق تشخیص کلیک خارجی مثال قبل را برای بستن منوی زمینه هنگام کلیک کاربر در خارج از آن، لحاظ کردهایم.
ملاحظات دسترسپذیری
هنگام استفاده از createPortal، توجه به دسترسپذیری برای اطمینان از اینکه برنامه شما برای همه قابل استفاده است، بسیار مهم است.
مدیریت فوکوس
هنگامی که یک پورتال باز میشود (مانند یک مودال)، باید اطمینان حاصل کنید که فوکوس به طور خودکار به اولین المان تعاملی داخل پورتال منتقل میشود. این به کاربرانی که با صفحهکلید یا صفحهخوان ناوبری میکنند کمک میکند تا به راحتی به محتوای پورتال دسترسی پیدا کنند.
هنگامی که پورتال بسته میشود، باید فوکوس را به المانی که باعث باز شدن پورتال شده بود، بازگردانید. این کار یک جریان ناوبری منسجم را حفظ میکند.
ویژگیهای ARIA
از ویژگیهای ARIA برای ارائه اطلاعات معنایی درباره محتوای پورتال استفاده کنید. به عنوان مثال، از aria-modal="true" روی المان مودال استفاده کنید تا نشان دهید که این یک گفتگوی مودال است. از aria-labelledby برای مرتبط کردن مودال با عنوان آن و از aria-describedby برای مرتبط کردن آن با توضیحاتش استفاده کنید.
ناوبری با صفحهکلید
اطمینان حاصل کنید که کاربران میتوانند با استفاده از صفحهکلید در محتوای پورتال ناوبری کنند. از ویژگی tabindex برای کنترل ترتیب فوکوس استفاده کنید و اطمینان حاصل کنید که تمام المانهای تعاملی با صفحهکلید قابل دسترسی هستند.
در نظر بگیرید که فوکوس را در داخل پورتال به دام بیندازید تا کاربران نتوانند به طور تصادفی از آن خارج شوند. این کار را میتوان با گوش دادن به کلید Tab و انتقال برنامهریزیشده فوکوس به اولین یا آخرین المان تعاملی داخل پورتال انجام داد.
مثال: مودال دسترسپذیر
import React, { useState, useRef, useEffect } from 'react';
import ReactDOM from 'react-dom';
function AccessibleModal({ children, isOpen, onClose, labelledBy, describedBy }) {
const modalRef = useRef(null);
const firstFocusableElementRef = useRef(null);
const [previouslyFocusedElement, setPreviouslyFocusedElement] = useState(null);
const modalRoot = document.getElementById('modal-root');
useEffect(() => {
if (isOpen) {
// Save the currently focused element before opening the modal.
setPreviouslyFocusedElement(document.activeElement);
// Focus the first focusable element in the modal.
if (firstFocusableElementRef.current) {
firstFocusableElementRef.current.focus();
}
// Trap focus within the modal.
function handleKeyDown(event) {
if (event.key === 'Tab') {
const focusableElements = modalRef.current.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstFocusableElement = focusableElements[0];
const lastFocusableElement = focusableElements[focusableElements.length - 1];
if (event.shiftKey) {
// Shift + Tab
if (document.activeElement === firstFocusableElement) {
lastFocusableElement.focus();
event.preventDefault();
}
} else {
// Tab
if (document.activeElement === lastFocusableElement) {
firstFocusableElement.focus();
event.preventDefault();
}
}
}
}
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
// Restore focus to the element that had focus before opening the modal.
if(previouslyFocusedElement && previouslyFocusedElement.focus) {
previouslyFocusedElement.focus();
}
};
}
}, [isOpen, previouslyFocusedElement]);
if (!isOpen) return null;
return ReactDOM.createPortal(
<div
className="modal-overlay"
onClick={onClose}
aria-modal="true"
aria-labelledby={labelledBy}
aria-describedby={describedBy}
ref={modalRef}
>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<h2 id={labelledBy}>Modal Title</h2>
<p id={describedBy}>This is the modal content.</p>
<button ref={firstFocusableElementRef} onClick={onClose}>
Close
</button>
{children}
</div>
</div>,
modalRoot
);
}
export default AccessibleModal;
توضیح:
- ما از ویژگیهای ARIA مانند
aria-modal،aria-labelledbyوaria-describedbyبرای ارائه اطلاعات معنایی درباره مودال استفاده میکنیم. - ما از هوک
useEffectبرای مدیریت فوکوس هنگام باز و بسته شدن مودال استفاده میکنیم. - ما المان فوکوسشده فعلی را قبل از باز کردن مودال ذخیره میکنیم و هنگام بسته شدن مودال، فوکوس را به آن بازمیگردانیم.
- ما با استفاده از یک شنونده رویداد
keydown، فوکوس را در داخل مودال به دام میاندازیم.
ملاحظات بینالمللیسازی (i18n)
هنگام توسعه برنامهها برای مخاطبان جهانی، بینالمللیسازی (i18n) یک ملاحظه حیاتی است. هنگام استفاده از createPortal، چند نکته وجود دارد که باید در نظر داشته باشید:
- جهت متن (RTL/LTR): اطمینان حاصل کنید که استایلدهی شما هم زبانهای چپ به راست (LTR) و هم راست به چپ (RTL) را در بر میگیرد. این ممکن است شامل استفاده از ویژگیهای منطقی در CSS (مانند
margin-inline-startبه جایmargin-left) و تنظیم مناسب ویژگیdirروی المان HTML باشد. - بومیسازی محتوا: تمام متنهای داخل پورتال باید به زبان ترجیحی کاربر بومیسازی شوند. از یک کتابخانه i18n (مانند
react-intl,i18next) برای مدیریت ترجمهها استفاده کنید. - قالببندی اعداد و تاریخ: اعداد و تاریخها را مطابق با منطقه کاربر قالببندی کنید. API
Intlقابلیتهایی برای این کار فراهم میکند. - قراردادهای فرهنگی: از قراردادهای فرهنگی مربوط به المانهای UI آگاه باشید. به عنوان مثال، محل قرارگیری دکمهها ممکن است در فرهنگهای مختلف متفاوت باشد.
مثال: i18n با react-intl
import React from 'react';
import { FormattedMessage } from 'react-intl';
function MyComponent() {
return (
<div>
<FormattedMessage id="myComponent.greeting" defaultMessage="Hello, world!" />
</div>
);
}
export default MyComponent;
کامپوننت FormattedMessage از react-intl پیام ترجمه شده را بر اساس منطقه کاربر بازیابی میکند. react-intl را با ترجمههای خود برای زبانهای مختلف پیکربندی کنید.
اشتباهات رایج و راهحلها
در حالی که createPortal ابزاری قدرتمند است، مهم است که از برخی اشتباهات رایج و نحوه اجتناب از آنها آگاه باشید:
- نبود المان ریشه پورتال: اطمینان حاصل کنید که المان DOM که به عنوان ریشه پورتال استفاده میکنید، قبل از mount شدن کامپوننتی که از
createPortalاستفاده میکند، وجود داشته باشد. یک روش خوب این است که آن را مستقیماً درindex.htmlقرار دهید. - تداخلات Z-Index: هنگام موقعیتدهی المانها با
createPortalبه مقادیر z-index توجه کنید. از CSS برای مدیریت زمینههای انباشتگی استفاده کنید و اطمینان حاصل کنید که محتوای پورتال شما به درستی نمایش داده میشود. - مشکلات مدیریت رویداد: نحوه انتشار رویدادها از طریق پورتال را درک کرده و آنها را به درستی مدیریت کنید. از
e.stopPropagation()برای جلوگیری از اینکه رویدادها اقدامات ناخواسته را فعال کنند، استفاده کنید. - نشت حافظه (Memory Leaks): شنوندگان رویداد و ارجاعات را هنگام unmount شدن کامپوننتی که از
createPortalاستفاده میکند، به درستی پاکسازی کنید تا از نشت حافظه جلوگیری شود. از هوکuseEffectبا یک تابع پاکسازی برای این کار استفاده کنید. - مشکلات اسکرول غیرمنتظره: پورتالها گاهی اوقات میتوانند با رفتار اسکرول مورد انتظار صفحه تداخل داشته باشند. اطمینان حاصل کنید که استایلهای شما مانع اسکرول نمیشوند و المانهای مودال هنگام باز و بسته شدن باعث پرش صفحه یا رفتار اسکرول غیرمنتظره نمیشوند.
نتیجهگیری
React.createPortal ابزاری ارزشمند برای ایجاد رابطهای کاربری انعطافپذیر، دسترسپذیر و قابل نگهداری در React است. با درک هدف، کاربرد و تکنیکهای پیشرفته برای مدیریت رویدادها و دسترسپذیری، میتوانید از قدرت آن برای ساخت برنامههای وب پیچیده و جذابی که تجربه کاربری برتری را برای مخاطبان جهانی فراهم میکنند، بهرهمند شوید. به یاد داشته باشید که بهترین شیوههای بینالمللیسازی و دسترسپذیری را در نظر بگیرید تا اطمینان حاصل کنید که برنامههای شما فراگیر و برای همه قابل استفاده هستند.
با پیروی از دستورالعملها و مثالهای این راهنما، میتوانید با اطمینان از createPortal برای حل چالشهای رایج UI و ایجاد تجربیات وب خیرهکننده استفاده کنید.