پورتال ها (Portals)
پورتال های React راهی هستند برای رندر کردن یک بخش UI بیرون از جای معمولش در DOM. مثل این که یک پنجره شناور روی کل صفحه باز کنی، حتی اگر کامپوننت داخل یک جعبه کوچیک باشد.
پورتال های React چیست؟
به صورت عادی، خروجی هر کامپوننت بچه همان کامپوننت در DOM است. یعنی div داخل همان بخش صفحه قرار می گیرد. اما پورتال های React اجازه می دهند HTML را در جای دیگری از DOM رندر کنیم.
تابع createPortal از پکیج react-dom این کار را انجام می دهد. این کار برای مودال ها، تولتیپ ها و منوهای شناور خیلی کاربردی است؛ چون باید از محدودیت های والد فرار کنند.
مثال ساده بدون استفاده از پورتال
اول یک مثال معمولی داریم. در این حالت خروجی کامپوننت دقیقاً زیر خودش در DOM رندر می شود.
function myChild() {
return (
<div>
Welcome
</div>
);
}
در این حالت div حاوی متن Welcome بچه مستقیم myChild در DOM است. یعنی هر استایل یا محدودیت والد روی این div هم تأثیر می گذارد.
همان مثال با استفاده از پورتال
حالا همان خروجی را با createPortal رندر می کنیم. این بار div مستقیماً داخل document.body قرار می گیرد.
import { createPortal } from 'react-dom';
function myChild() {
return createPortal(
<div>
Welcome
</div>,
document.body
);
}
اینجا کامپوننت هنوز همان myChild است. اما محتوای آن از درخت DOM والد جدا شده و به بدنه اصلی صفحه وصل شده است. این یعنی استایل هایی مثل overflow والد دیگر محتوای ما را محدود نمی کنند.
سینتکس تابع createPortal در پورتال های React
برای ساخت پورتال های React باید از تابع createPortal استفاده کنیم. این تابع در پکیج react-dom قرار دارد.
import { createPortal } from 'react-dom';
createPortal(children, domNode);
آرگومان children می تواند هر محتوای React باشد؛ مثل المنت ها یا رشته ها. آرگومان domNode یک المنت واقعی DOM است که می خواهیم خروجی در آن جا رندر شود؛ مثلاً document.body یا یک div مخصوص مودال.
ساخت مودال با پورتال ها در React
یک استفاده خیلی رایج از پورتال ها ساخت پنجره مودال است. مودال باید روی کل صفحه بنشیند، نه فقط داخل یک کارت یا بخش کوچک.
در این مثال، کامپوننت Modal محتوایش را با createPortal داخل document.body رندر می کند. در عین حال، کنترل باز و بسته شدن مودال در MyApp و state آن مدیریت می شود.
import { createRoot } from 'react-dom/client';
import { useState } from 'react';
import { createPortal } from 'react-dom';
function Modal({ isOpen, onClose, children }) {
if (!isOpen) {
return null;
}
return createPortal(
<div
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
<div
style={{
background: 'white',
padding: '20px',
borderRadius: '8px'
}}
>
{children}
<button onClick={onClose}>
Close
</button>
</div>
</div>,
document.body
);
}
function MyApp() {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<h1>
My App
</h1>
<button
onClick={() => {
setIsOpen(true);
}}
>
Open Modal
</button>
<Modal
isOpen={isOpen}
onClose={() => {
setIsOpen(false);
}}
>
<h2>
Modal Content
</h2>
<p>
This content is rendered outside the App component!
</p>
</Modal>
</div>
);
}
createRoot(document.getElementById('root')).render(
<MyApp />
);
در اینجا state isOpen تعیین می کند مودال باز باشد یا بسته. وقتی دکمه Open Modal را می زنی، isOpen برابر true می شود و Modal محتوا را از طریق createPortal روی کل صفحه نشان می دهد.
چرا از پورتال های React استفاده می کنیم؟
گاهی والد روی خودش overflow: hidden یا z-index خاص دارد. در این حالت، منوها یا مودال های داخل آن بریده می شوند یا زیر عناصر دیگر می روند.
با پورتال ها می توانیم این اجزا را در document.body رندر کنیم. اما همچنان کامپوننت ها و state در جای اصلی خودشان در درخت React می مانند.
- ساخت مودال ها و دیالوگ ها بدون مشکل لایه بندی.
- تولتیپ ها و منوهای شناور روی کل صفحه.
- نوتیفیکیشن ها و پیام های گوشه صفحه.
حبابی شدن رویدادها در پورتال ها
حبابی شدن رویداد (Event Bubbling) یعنی رویداد از بچه به والدها بالا می رود. نکته مهم این است که در پورتال های React، رویدادها طبق درخت React حبابی می شوند، نه طبق جای واقعی در DOM.
یعنی حتی اگر دکمه در document.body رندر شود، هنوز می تواند رویداد onClick والد خود در درخت React را فعال کند.
مثال دکمه شناور با پورتال
در این مثال، یک دکمه شناور با PortalButton داریم. این دکمه بیرون از div اصلی در DOM رندر می شود؛ اما کلیک آن هنوز روی div والد در React هم اثر می گذارد.
import { createRoot } from 'react-dom/client';
import { useState } from 'react';
import { createPortal } from 'react-dom';
function PortalButton({ onClick, children }) {
return createPortal(
<button
onClick={onClick}
style={{
position: 'fixed',
bottom: '20px',
right: '20px',
padding: '10px',
background: 'blue',
color: 'white'
}}
>
{children}
</button>,
document.body
);
}
function App() {
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
return (
<div
style={{
padding: '20px',
border: '2px solid black',
margin: '20px'
}}
onClick={() => {
setCount1(c => c + 1);
}}
>
<h2>
Div Clicked: {count1}
</h2>
<h2>
Button Clicked: {count2}
</h2>
<p>
The floating button is rendered outside this box using a portal,
but its clicks still bubble up to this parent div!
</p>
<p>
Try to click the div element as well, to see the count increase
</p>
<PortalButton
onClick={() => {
setCount2(c => c + 1);
}}
>
Floating Button
</PortalButton>
</div>
);
}
createRoot(document.getElementById('root')).render(
<App />
);
وقتی روی دکمه کلیک می کنی، اول شمارنده دکمه زیاد می شود. سپس رویداد روی div هم حبابی می شود و شمارنده Div Clicked هم افزایش پیدا می کند.
گام به گام ساخت یک پورتال ساده
برای تمرین، یک پورتال ساده در پروژه خودت بساز. این مراحل را به ترتیب انجام بده.
- در فایل بالا، createPortal را از react-dom ایمپورت کن.
- یک کامپوننت جدید مثل PortalBox بساز.
- در PortalBox، از createPortal استفاده کن و یک div ساده رندر کن.
- به عنوان domNode از document.body استفاده کن.
- در کامپوننت اصلی، PortalBox را صدا بزن و نتیجه را در صفحه ببین.
نکته: اگر در بخش فرم ها مودال تأیید داری، می توانی آن را با پورتال های React تمیزتر و بدون مشکل overflow بسازی.
جمع بندی سریع پورتال ها
- پورتال های React محتوا را بیرون از DOM والد رندر می کنند.
- تابع createPortal(children, domNode) هسته ساخت پورتال است.
- برای مودال ها، تولتیپ ها و نوتیفیکیشن ها عالی عمل می کنند.
- رویدادها طبق درخت React حبابی می شوند، نه محل واقعی DOM.
- برای مرور، صفحه پورتال های React را ذخیره کن.