درواقع Portals در ری اکت یکی ویژگی خیلی جالب و کاربردی هست که به ما اجازه میده کامپوننت خودمون رو خارج از سلسله بندی کامپوننت پدر در DOM رندر کنیم. اگه این جمله براتون گنگ بود یا متوجه کاربرد پورتال در ری اکت نشدید، تو این مقاله با ما همراه باشید که میخوایم حسابی Portals در ری اکت رو به همراه تیکه کد و مثال عملی بررسی کنیم 🙂
درواقع Portals در ری اکت یک روش کاربردی برای render کردن کامپوننت، خارج از سلسه مراتب کامپوننت والد در DOM هست. یعنی ما کامپوننت خودمون رو بدون توجه به سلسه مراتبش داخل DOM قرار میدیم .
حالا شاید سوال برامون پیش بیاد که اصلا سلسله مراتب یعنی چی ؟
قبل از هرچیز لازمه اشاره کنم که برای درک پورتال در ری اکت، باید درمورد دام مجازی در ری اکت مطالعه کرده باشیم و بدونیم DOM چیه .. ( اگه درمورد دام مجازی اطلاعاتی ندارید، روی لینک آبی رنگ بالا کلیک کنید و مطالعه کنید )
موضوع این هست که کامپوننت های ما طبق یک سلسله مراتب مشخص داخل DOM مرورگر قرار میگیرن. سلسله مراتب یعنی هر کامپوننت یک پدر، پدربزرگ، فرزند و .. داره. طبیعی هست که خصوصیات پدر، به فرزند منتقل میشه.
گاهی اوغات ما نمیخوایم خصوصیات پدر به فرزند منتقل بشه.
این یعنی ما لازم داریم کامپوننت خودمون رو خارج از سلسله مراتب اون کامپوننت داخل DOM قرار بدیم. در چنین مواقعی باید از Portals در ری اکت استفاده کنیم.
ما اشاره کردیم که در React کامپوننت های فرزند، خصوصیات و ویژگی های کامپوننت پدرشون رو به ارث میبرن. مثلا اگه کامپوننت پدر 400px طول داشته باشه، کامپوننت فرزند نمیتونه بیشتر از 400px باشه.
حالا اگه ما لازم داشته باشیم که کامپوننت فرزند رو بصورت 800px نمایش بدیم باید چیکار کنیم ؟
راه حل استفاده از پورتال در React هست چون بدون در نظر گرفتن سلسه مراتبِ اون کامپوننت، کامپوننت رو داخل DOM رندر میکنه.
اینجوری کامپوننت ما دیگه خصوصیات کامپوننت پدرش رو به ارث نمیبره.
این مثال معمولا برای Modal ( مودال )، منو های پاپ آپ، نوتیفیکیشن ها و .. کاربرد داره چون ما میخوایم این موارد استایل های پدر خودشون رو به ارث نبرن.
نحوه ساخت Portals در React بصورت زیر هست :
ReactDOM.createPortal(child, container)
تابع createPortal، ساخت پورتال رو برای ما انجام میده. این تابع 2 آرگومان ورودی از ما میگیره:
آرگومان اول همون کامپوننت ماست که میخوایم خارج از سلسه مراتب render بشه ( مثل مودال )
آرگومان دوم یک اِلِمان هست که میخوایم کامپوننت ما داخل این اِلِمان رندر بشه ( ما باید این اِلِمان رو بسازیم، در ادامه میبینیم که چطور این اِلِمان رو میسازیم )
بیاید یه مثال عملی در مورد پورتال در ریکت رو باهمدیگه بررسی کنیم ..
تو مثال زیر من یک مودال ساده ساختم :
export const Modal = (props) => {
let { children, close, ...rest } = props;
if (!children) {
children = <p>This is a example modal</p>;
}
return (
<div id="modal-dialog" {...rest}>
<div className="flex flex-col justify-center items-center">
{children}
<button ={close}>
Close this modal
</button>
</div>
</div>
);
};
کامپوننت من چندین Props میگیره که کمک میکنن بتونیم کامپوننت خودمون رو کنترل کنیم مثل تابع close برای بستن مودال و children.
و فایل css کامپوننت بالارو بصورت زیر ایجاد میکنیم :
* {
font-size: 62.5%;
font-family: "Roboto"
margin: 0;
padding: 0;
}
#App {
overflow: hidden;
height: 20vh;
background-color: #ccc;
}
#App > h1 {
font-size: 2rem;
}
div#modal-dialog {
background-color: rgba(0, 0, 0, 0.8);
position: fixed;
z-index: 999;
height: 100vh;
width: 100vw;
top: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
}
div#modal-dialog > div {
background-color: #f5f5f5;
padding: 2rem;
border-radius: 1.2rem;
}
p {
margin: 1.4rem 0;
font-size: 1.5rem;
}
button {
padding: 1rem;
border-radius: 1rem;
border: none;
background-color: #9b59b6;
color: #fff;
cursor: pointer;
transition: all 0.3s ease-in-out;
}
button:hover {
background-color: #8e44ad;
}
.flex {
display: flex;
}
.flex-col {
flex-direction: column;
}
.flex-row {
flex-direction: row;
}
.justify-center {
justify-content: center;
}
.items-center {
align-items: center;
}
تا به اینجای کار فقط یک مودال ساده درست کردیم.
ما میدونیم که تو ری اکت، اپیکیشن ما داخل یک div با آیدی root رندر میشه. حالا من به فایل index.html میرم و یک div جدید با آیدی modals درست میکنم. این دو div باید رابطه خواهر برادری داشته باشن، دقیقا مثل زیر :
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<meta name="theme-color" content="#000000" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
<title>React App</title>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
<div id="modals"></div>
<script data-no-optimize="1">!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).LazyLoad=e()}(this,function(){"use strict"function e(){return(e=Object.assign||function(t){for(var e=1;e<arguments.length;e++){var n,a=arguments[e];for(n in a)Object.prototype.hasOwnProperty.call(a,n)&&(t[n]=a[n])}return t}).apply(this,arguments)}function i(t){return e({},it,t)}function o(t,e){var n,a="LazyLoad::Initialized",i=new t(e);try{n=new CustomEvent(a,{detail:{instance:i}})}catch(t){(n=document.createEvent("CustomEvent")).initCustomEvent(a,!1,!1,{instance:i})}window.dispatchEvent(n)}function l(t,e){return t.getAttribute(gt+e)}function c(t){return l(t,bt)}function s(t,e){return function(t,e,n){e=gt+e;null!==n?t.setAttribute(e,n):t.removeAttribute(e)}(t,bt,e)}function r(t){return s(t,null),0}function u(t){return null===c(t)}function d(t){return c(t)===vt}function f(t,e,n,a){t&&(void 0===a?void 0===n?t(e):t(e,n):t(e,n,a))}function _(t,e){nt?t.classList.add(e):t.className+=(t.className?" ":"")+e}function v(t,e){nt?t.classList.remove(e):t.className=t.className.replace(new RegExp("(^|\\s+)"+e+"(\\s+|$)")," ").replace(/^\s+/,"").replace(/\s+$/,"")}function g(t){return t.llTempImage}function b(t,e){!e||(e=e._observer)&&e.unobserve(t)}function p(t,e){t&&(t.loadingCount+=e)}function h(t,e){t&&(t.toLoadCount=e)}function n(t){for(var e,n=[],a=0;e=t.children[a];a+=1)"SOURCE"===e.tagName&&n.push(e);return n}function m(t,e){(t=t)&&"PICTURE"===t.tagName&&n(t).forEach(e)}function a(t,e){n(t).forEach(e)}function E(t){return!!t[st]}function I(t){return t[st]}function y(t){return delete t[st]}function A(e,t){var n;E(e)||(n={},t.forEach(function(t){n[t]=e.getAttribute(t)}),e[st]=n)}function k(a,t){var i;E(a)&&(i=I(a),t.forEach(function(t){var e,n;e=a,(t=i[n=t])?e.setAttribute(n,t):e.removeAttribute(n)}))}function L(t,e,n){_(t,e.class_loading),s(t,ut),n&&(p(n,1),f(e.callback_loading,t,n))}function w(t,e,n){n&&t.setAttribute(e,n)}function x(t,e){w(t,ct,l(t,e.data_sizes)),w(t,rt,l(t,e.data_srcset)),w(t,ot,l(t,e.data_src))}function O(t,e,n){var a=l(t,e.data_bg_multi),i=l(t,e.data_bg_multi_hidpi);(a=at&&i?i:a)&&(t.style.backgroundImage=a,n=n,_(t=t,(e=e).class_applied),s(t,ft),n&&(e.unobserve_completed&&b(t,e),f(e.callback_applied,t,n)))}function N(t,e){!e||0<e.loadingCount||0<e.toLoadCount||f(t.callback_finish,e)}function C(t,e,n){t.addEventListener(e,n),t.llEvLisnrs[e]=n}function M(t){return!!t.llEvLisnrs}function z(t){if(M(t)){var e,n,a=t.llEvLisnrs;for(e in a){var i=a[e];n=e,i=i,t.removeEventListener(n,i)}delete t.llEvLisnrs}}function R(t,e,n){var a;delete t.llTempImage,p(n,-1),(a=n)&&--a.toLoadCount,v(t,e.class_loading),e.unobserve_completed&&b(t,n)}function T(o,r,c){var l=g(o)||o;M(l)||function(t,e,n){M(t)||(t.llEvLisnrs={});var a="VIDEO"===t.tagName?"loadeddata":"load"C(t,a,e),C(t,"error",n)}(l,function(t){var e,n,a,i;n=r,a=c,i=d(e=o),R(e,n,a),_(e,n.class_loaded),s(e,dt),f(n.callback_loaded,e,a),i||N(n,a),z(l)},function(t){var e,n,a,i;n=r,a=c,i=d(e=o),R(e,n,a),_(e,n.class_error),s(e,_t),f(n.callback_error,e,a),i||N(n,a),z(l)})}function G(t,e,n){var a,i,o,r,c;t.llTempImage=document.createElement("IMG"),T(t,e,n),E(c=t)||(c[st]={backgroundImage:c.style.backgroundImage}),o=n,r=l(a=t,(i=e).data_bg),c=l(a,i.data_bg_hidpi),(r=at&&c?c:r)&&(a.style.backgroundImage='url("'.concat(r,'")'),g(a).setAttribute(ot,r),L(a,i,o)),O(t,e,n)}function D(t,e,n){var a;T(t,e,n),a=e,e=n,(t=It[(n=t).tagName])&&(t(n,a),L(n,a,e))}function V(t,e,n){var a;a=t,(-1<yt.indexOf(a.tagName)?D:G)(t,e,n)}function F(t,e,n){var a;t.setAttribute("loading","lazy"),T(t,e,n),a=e,(e=It[(n=t).tagName])&&e(n,a),s(t,vt)}function j(t){t.removeAttribute(ot),t.removeAttribute(rt),t.removeAttribute(ct)}function P(t){m(t,function(t){k(t,Et)}),k(t,Et)}function S(t){var e;(e=At[t.tagName])?e(t):E(e=t)&&(t=I(e),e.style.backgroundImage=t.backgroundImage)}function U(t,e){var n;S(t),n=e,u(e=t)||d(e)||(v(e,n.class_entered),v(e,n.class_exited),v(e,n.class_applied),v(e,n.class_loading),v(e,n.class_loaded),v(e,n.class_error)),r(t),y(t)}function $(t,e,n,a){var i;n.cancel_on_exit&&(c(t)!==ut||"IMG"===t.tagName&&(z(t),m(i=t,function(t){j(t)}),j(i),P(t),v(t,n.class_loading),p(a,-1),r(t),f(n.callback_cancel,t,e,a)))}function q(t,e,n,a){var i,o,r=(o=t,0<=pt.indexOf(c(o)));s(t,"entered"),_(t,n.class_entered),v(t,n.class_exited),i=t,o=a,n.unobserve_entered&&b(i,o),f(n.callback_enter,t,e,a),r||V(t,n,a)}function H(t){return t.use_native&&"loading"in HTMLImageElement.prototype}function B(t,i,o){t.forEach(function(t){return(a=t).isIntersecting||0<a.intersectionRatio?q(t.target,t,i,o):(e=t.target,n=t,a=i,t=o,void(u(e)||(_(e,a.class_exited),$(e,n,a,t),f(a.callback_exit,e,n,t))));var e,n,a})}function J(e,n){var t;et&&!H(e)&&(n._observer=new IntersectionObserver(function(t){B(t,e,n)},{root:(t=e).container===document?null:t.container,rootMargin:t.thresholds||t.threshold+"px"}))}function K(t){return Array.prototype.slice.call(t)}function Q(t){return t.container.querySelectorAll(t.elements_selector)}function W(t){return c(t)===_t}function X(t,e){return e=t||Q(e),K(e).filter(u)}function Y(e,t){var n;(n=Q(e),K(n).filter(W)).forEach(function(t){v(t,e.class_error),r(t)}),t.update()}function t(t,e){var n,a,t=i(t);this._settings=t,this.loadingCount=0,J(t,this),n=t,a=this,Z&&window.addEventListener("online",function(){Y(n,a)}),this.update(e)}var Z="undefined"!=typeof window,tt=Z&&!("onscroll"in window)||"undefined"!=typeof navigator&&/(gle|ing|ro)bot|crawl|spider/i.test(navigator.userAgent),et=Z&&"IntersectionObserver"in window,nt=Z&&"classList"in document.createElement("p"),at=Z&&1<window.devicePixelRatio,it={elements_selector:".lazy",container:tt||Z?document:null,threshold:300,thresholds:null,data_src:"src",data_srcset:"srcset",data_sizes:"sizes",data_bg:"bg",data_bg_hidpi:"bg-hidpi",data_bg_multi:"bg-multi",data_bg_multi_hidpi:"bg-multi-hidpi",data_poster:"poster",class_applied:"applied",class_loading:"litespeed-loading",class_loaded:"litespeed-loaded",class_error:"error",class_entered:"entered",class_exited:"exited",unobserve_completed:!0,unobserve_entered:!1,cancel_on_exit:!0,callback_enter:null,callback_exit:null,callback_applied:null,callback_loading:null,callback_loaded:null,callback_error:null,callback_finish:null,callback_cancel:null,use_native:!1},ot="src",rt="srcset",ct="sizes",lt="poster",st="llOriginalAttrs",ut="loading",dt="loaded",ft="applied",_t="error",vt="native",gt="data-",bt="ll-status",pt=[ut,dt,ft,_t],ht=[ot],mt=[ot,lt],Et=[ot,rt,ct],It={IMG:function(t,e){m(t,function(t){A(t,Et),x(t,e)}),A(t,Et),x(t,e)},IFRAME:function(t,e){A(t,ht),w(t,ot,l(t,e.data_src))},VIDEO:function(t,e){a(t,function(t){A(t,ht),w(t,ot,l(t,e.data_src))}),A(t,mt),w(t,lt,l(t,e.data_poster)),w(t,ot,l(t,e.data_src)),t.load()}},yt=["IMG","IFRAME","VIDEO"],At={IMG:P,IFRAME:function(t){k(t,ht)},VIDEO:function(t){a(t,function(t){k(t,ht)}),k(t,mt),t.load()}},kt=["IMG","IFRAME","VIDEO"];return t.prototype={update:function(t){var e,n,a,i=this._settings,o=X(t,i);{if(h(this,o.length),!tt&&et)return H(i)?(e=i,n=this,o.forEach(function(t){-1!==kt.indexOf(t.tagName)&&F(t,e,n)}),void h(n,0)):(t=this._observer,i=o,t.disconnect(),a=t,void i.forEach(function(t){a.observe(t)}));this.loadAll(o)}},destroy:function(){this._observer&&this._observer.disconnect(),Q(this._settings).forEach(function(t){y(t)}),delete this._observer,delete this._settings,delete this.loadingCount,delete this.toLoadCount},loadAll:function(t){var e=this,n=this._settings;X(t,n).forEach(function(t){b(t,e),V(t,n,e)})},restoreAll:function(){var e=this._settings;Q(e).forEach(function(t){U(t,e)})}},t.load=function(t,e){e=i(e);V(t,e)},t.resetStatus=function(t){r(t)},Z&&function(t,e){if(e)if(e.length)for(var n,a=0;n=e[a];a+=1)o(t,n);else o(t,e)}(t,window.lazyLoadOptions),t});!function(e,t){"use strict"function a(){t.body.classList.add("litespeed_lazyloaded")}function n(){console.log("[LiteSpeed] Start Lazy Load Images"),d=new LazyLoad({elements_selector:"[data-lazyloaded]",callback_finish:a}),o=function(){d.update()},e.MutationObserver&&new MutationObserver(o).observe(t.documentElement,{childList:!0,subtree:!0,attributes:!0})}var d,o;e.addEventListener?e.addEventListener("load",n,!1):e.attachEvent("onload",n)}(window,document);</body>
</html>
اگه کد بالا هم ریختس، بخاطر ویرگوله. لطفا همین مقاله رو در فِرانت اِندی مطالعه کنید تا بهم ریخته نباشه.
اگه به خط 19 دقت کنیم، میبینیم که یک div جدید با آیدی modals داریم که خارج از div اپیکیشن با آیدی root هست!
یعنی هرچیزی که داخل div دوم قرار بگیره، خارج از سلسه مراتب div اول هست و خصوصیات اون رو به ارث نمیبره!
حالا بریم فایل App.js رو بسازیم :
import { useState } from "react"
import ReactDOM from "react-dom"
import { Modal } from "./Components/Modal"
import "./styles.css"
const domElement = document.getElementById("modals");
export default function App() {
const [stateModal, setStateModal] = useState(false);
const openModal = () => setStateModal(true);
const closeModal = () => setStateModal(false);
return (
<div id="App" className="flex flex-col justify-center items-center">
<h1>Portals Example</h1>
<div className="flex flex-col items-center justify-center">
<p>This is a div with a defined height and overflow hidden</p>
<button ={openModal}>
Open modal
</button>
</div>
{stateModal &&
ReactDOM.createPortal(
<Modal close={closeModal}>
<p>Modal from App.js</p>
</Modal>,
domElement
)}
</div>
);
}
تو خط 3 ما Modal خودمون رو فراخوانی کردیم و در خط 6 تونستیم به div خودمون که آیدی portals داشت، دسترسی پیدا کنیم. ( همون اِلِمانی که میخوایم Portals رو داخلش قرار بدیم )
همچنین در خط 9 یک state برای مشخص کردن باز یا بسته بودن modal درست کردیم.
اما مهمترین بخش تیکه کد بالا، در خط 22 وجود داره. ما یک شرط گذاشتیم ( شرط باز بودن مودال بر اساس State ) و گفتیم زمانیکه این state مقدار True داشت یک portal جدید بساز.
ساختن portal جدید به کمک ReactDOM انجام میشه. تابع createPortal از ما 2 آرگومان میگیره :
آرگومان اول همون چیزی هست که میخوایم داخل پورتال render کنیم و نمایش بدیم.
آرگومان دوم اِلِمان Portal ما هست، اگه به خط 6 دقت کنیم میبینیم که این اِلِمان رو تعریف کردیم.
در حقیقت، کل ماجرای Portals در React تو تیکه کد زیر نهفته شده :
{stateModal &&
ReactDOM.createPortal(
<Modal close={closeModal}>
<p>Modal from App.js</p>
</Mode>,
domElement
)}
ما طبق یک شرط، یک Portals در ری اکت ساختیم و نمایش دادیم.
ساخت پورتال در React به همین سادگی امکانپذیره 🙂
نتیجه تیکه کد های بالا این هست که کامپوننت Modal ما خارج از سلسله مراتب کل اپیکیشن ما ( بیرون div با آیدی root ) رندر میشه.
لطفا برای مطالعه ادامه مقاله روی لینک زیر کلیک کنید :