Xin chào các bạn trên hành trình chinh phục React! Chúng ta đã cùng nhau đi qua rất nhiều cột mốc quan trọng trên Lộ trình học React 2025, từ những khái niệm cơ bản như React là gì, JSX, Functional Components, cách quản lý dữ liệu với Props vs State, xử lý sự kiện, cho đến các kỹ thuật nâng cao hơn như Context API hay Custom Hooks.
Thông thường, khi render một component con trong React, element DOM tương ứng của nó sẽ được đặt lồng bên trong element DOM của component cha. Cấu trúc DOM này thường song song với cấu trúc cây component React mà chúng ta xây dựng.
Tuy nhiên, có những tình huống đặc biệt trong phát triển giao diện người dùng (UI) mà bạn cần phá vỡ quy tắc này. Ví dụ điển hình là khi làm việc với modals (các cửa sổ pop-up), tooltips (chú thích), hay dropdown menus. Những thành phần UI này thường cần hiển thị “đè” lên mọi thứ khác trên trang, độc lập với vị trí của component cha trong cây DOM.
Đây chính là lúc React Portals ra đời để giải quyết vấn đề. Portals cho phép bạn render component con vào một node DOM khác, nằm ngoài cây DOM của component cha chứa nó, mà vẫn giữ nguyên được “liên kết” trong cây component React. Nghe có vẻ phức tạp đúng không? Đừng lo, chúng ta sẽ cùng nhau làm rõ qua bài viết này!
Mục lục
React Portal Là Gì?
Theo định nghĩa của React, Portal cung cấp một cách thức ưu việt để render các component con vào một node DOM tồn tại bên ngoài hệ thống phân cấp DOM của component cha.
Cú pháp sử dụng rất đơn giản:
ReactDOM.createPortal(child, container)
child
: Đây là React node có thể render được, chẳng hạn như một element, string hoặc fragment. Nó chính là component hoặc element mà bạn muốn hiển thị.container
: Đây là một DOM element. Các element con củachild
sẽ được đính vào đây. Nó có thể là bất kỳ DOM element hợp lệ nào nằm ngoài element gốc mà ứng dụng React của bạn đang render (thường là#root
).
Sự khác biệt cốt lõi ở đây là: Mặc dù element DOM của component con được render ở một vị trí khác trong cây DOM, nó vẫn “thuộc về” component cha trong cây component *logic* của React. Điều này có những ý nghĩa quan trọng về mặt quản lý state, context, và xử lý sự kiện.
Vì Sao Cần Sử Dụng Portals? Giải Quyết Những Vấn Đề Gì?
Như đã đề cập, Portals không phải là thứ bạn dùng hàng ngày cho mọi component. Chúng giải quyết các vấn đề cụ thể, thường liên quan đến vị trí hiển thị và z-indexing của các thành phần UI cần nằm “trên cùng”.
Giải quyết Vấn đề Styling Phức Tạp
Hãy tưởng tượng bạn có một component cha với CSS overflow: hidden
hoặc position: relative
và một giá trị z-index
cụ thể. Nếu bạn render một modal, tooltip hoặc dropdown *bên trong* component cha này theo cách thông thường, các element pop-up đó có thể bị cắt bớt (do overflow: hidden
) hoặc bị ảnh hưởng bởi z-index
của các element xung quanh component cha, dẫn đến việc chúng không hiển thị “đè” lên mọi thứ như mong muốn.
Ví dụ, một dropdown menu mở ra từ một button nằm trong một div có overflow: hidden
sẽ bị nội dung của div đó che khuất hoặc cắt đi:
<div style="overflow: hidden; height: 100px; border: 1px solid black;">
<div>Nội dung bên trong div cha.</div>
<!-- Dropdown sẽ bị cắt nếu render ở đây theo cách thông thường -->
<Dropdown />
</div>
Sử dụng Portal cho phép bạn “nhảy” ra khỏi div cha đó và render dropdown (hoặc modal, tooltip) vào một node DOM khác, ví dụ như trực tiếp vào <body>
hoặc một div riêng được tạo ra cho mục đích này. Khi đó, element DOM của dropdown không còn là con của div cha bị giới hạn bởi overflow
hay z-index
nữa, giúp nó hiển thị đúng vị trí và “đè” lên các element khác.
Giữ Nguyên Cơ Chế Event Bubbling Của React
Đây là một trong những lợi ích mạnh mẽ và đôi khi gây ngạc nhiên của Portals. Mặc dù element DOM của component con được render ở một vị trí hoàn toàn khác trong cây DOM, các sự kiện (events) từ component đó *vẫn sẽ nổi bọt (bubble up)* theo cây component React, chứ không phải cây DOM.
Điều này có nghĩa là nếu bạn có một Context Provider hoặc một event handler được định nghĩa ở một component cha *trong cây React*, và một component con được render thông qua Portal phát ra một sự kiện (ví dụ: click), sự kiện đó vẫn sẽ nổi bọt lên đến component cha trong cây React và có thể được xử lý bởi handler đó.
Hãy xem xét ví dụ modal. Bạn có thể có một button để mở modal nằm trong một component App
. Modal component được render qua Portal, nhưng handler đóng modal (ví dụ: khi click bên ngoài modal hoặc nhấn nút đóng) vẫn có thể gọi một hàm được truyền xuống từ component App
thông qua props hoặc Context. Điều này giữ cho logic ứng dụng của bạn được tổ chức mạch lạc theo cây component React, bất kể cấu trúc DOM vật lý.
Khả năng này đặc biệt hữu ích khi kết hợp Portals với các kỹ thuật quản lý state hoặc context. Bạn có thể đặt một Context Provider ở cấp cao trong cây React (Sử dụng useContext để Quản lý Global State), và các component con render qua Portal vẫn có thể truy cập context đó. Tương tự, việc xử lý sự kiện (Xử Lý Sự Kiện trong React) vẫn tuân theo quy tắc của React.
Cách Sử Dụng React Portal: Ví Dụ Thực Tế
Cách sử dụng Portals bao gồm hai bước chính:
- Xác định hoặc tạo DOM node đích mà bạn muốn render component vào đó.
- Sử dụng
ReactDOM.createPortal()
trong component con.
Bước 1: Chuẩn bị DOM Node Đích
Thông thường, bạn sẽ muốn render modals, tooltips, v.v., trực tiếp vào <body>
hoặc một div ngay dưới <body>
và bên cạnh div gốc chứa ứng dụng React (thường là #root
). Điều này đảm bảo chúng không bị ảnh hưởng bởi CSS của các component cha.
Mở file index.html
(hoặc tương đương) trong dự án React của bạn và thêm một div mới:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!-- Thêm div này cho portal -->
<div id="modal-root"></div>
</body>
</html>
Ở đây, chúng ta tạo một div với id="modal-root"
. Đây sẽ là nơi mà modal của chúng ta được render vào.
Bước 2: Tạo Component Sử Dụng Portal
Bây giờ, hãy tạo một component Modal
sử dụng createPortal
.
import React, { useEffect, useRef } from 'react';
import ReactDOM from 'react-dom';
// Tìm DOM node đích khi component mount lần đầu
const modalRoot = document.getElementById('modal-root');
// Kiểm tra nếu modalRoot không tồn tại (trường hợp hiếm nếu index.html đúng)
// Có thể thêm logic tạo dynamic node nếu cần, nhưng với modal-root ở index.html thì không cần.
// if (!modalRoot) {
// modalRoot = document.createElement('div');
// modalRoot.setAttribute('id', 'modal-root');
// document.body.appendChild(modalRoot);
// }
const Modal = ({ children, onClose }) => {
// Tạo một div element cho nội dung modal trong component này
// Thay vì render trực tiếp children vào modalRoot
const el = useRef(document.createElement('div'));
useEffect(() => {
// Khi component mount, thêm div element vào modalRoot
modalRoot.appendChild(el.current);
// Khi component unmount, dọn dẹp: xóa div element khỏi modalRoot
return () => {
modalRoot.removeChild(el.current);
};
}, []); // Dependency rỗng đảm bảo effect chỉ chạy 1 lần khi mount và unmount
// Use the portal
return ReactDOM.createPortal(
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
{children}
<button onClick={onClose}>Đóng</button>
</div>
</div>,
el.current // Render nội dung modal vào element div mà chúng ta vừa tạo và thêm vào modalRoot
);
};
export default Modal;
Giải thích code:
- Chúng ta tìm kiếm
modal-root
element một lần bên ngoài component (hoặc có thể trong useEffect nếu node được tạo dynamic). - Sử dụng
useRef
(Khi nào và Vì sao nên dùng useCallback, useMemo, và useRef) để tạo và giữ tham chiếu đến một div element mới. Element này sẽ chứa nội dung modal của chúng ta. Chúng ta làm vậy để dễ dàng thêm/xóa nó khỏimodal-root
DOM node khi component mount/unmount. useEffect
(useState và useEffect): Khi component mount, chúng ta thêmel.current
(div mới) vàomodal-root
. Hàm clean-up tronguseEffect
sẽ xóael.current
khi component unmount, giúp dọn dẹp DOM.ReactDOM.createPortal(..., el.current)
: Đây là điểm mấu chốt. Thay vì render trực tiếp JSX của modal (overlay, content, children) vào vị trí của componentModal
trong cây DOM thông thường, chúng ta dùngcreatePortal
để yêu cầu React render nó vàoel.current
. Vìel.current
đã được thêm vào#modal-root
(nằm ngoài#root
), nội dung modal sẽ hiển thị ở đó.e.stopPropagation()
trong modal content click handler: Ngăn chặn việc click vào nội dung modal làm đóng modal (do click overlay).
Bạn cũng cần thêm CSS cơ bản cho .modal-overlay
(position: fixed
, top: 0
, left: 0
, right: 0
, bottom: 0
, background: rgba(0,0,0,0.5)
, display: flex
, justify-content: center
, align-items: center
) và .modal-content
(background: white
, padding: 20px
, border-radius: 8px
, position: relative
, z-index: 1000
– ví dụ). Do modal render vào #modal-root
, các CSS position: fixed
và z-index
cao sẽ hoạt động đúng như mong đợi mà không bị ảnh hưởng bởi các component cha trong #root
.
Bước 3: Sử Dụng Modal Component
Bây giờ bạn có thể sử dụng component Modal
trong bất kỳ component nào khác như bình thường. Ví dụ trong App.js
:
import React, { useState } from 'react';
import Modal from './Modal'; // Giả sử file Modal.js
function App() {
const [isModalOpen, setIsModalOpen] = useState(false); // useState: Xem bài useState và useEffect
const handleOpenModal = () => {
setIsModalOpen(true);
};
const handleCloseModal = () => {
setIsModalOpen(false);
};
return (
<div className="App">
<h1>Ứng dụng Demo React Portal</h1>
<p>Nội dung chính của ứng dụng.</p>
<button onClick={handleOpenModal}>Mở Modal</button>
{/* Conditional Rendering: Chỉ render Modal khi isModalOpen là true */}
{/* Xem bài Làm Chủ Conditional Rendering */}
{isModalOpen && (
<Modal onClose={handleCloseModal}>
<h2>Đây là Modal</h2>
<p>Nội dung của modal được render bằng portal!</p>
</Modal>
)}
<div style={{ overflow: 'hidden', height: '50px', border: '1px dashed blue', marginTop: '20px' }}>
<p>Div cha có overflow: hidden.</p>
{/* Modal RENDERED VIA PORTAL would NOT be clipped by this div's overflow */}
</div>
</div>
);
}
export default App;
Trong ví dụ này, component Modal
được render có điều kiện (Làm Chủ Conditional Rendering) dựa trên state isModalOpen
. Mặc dù thẻ <Modal>
nằm trong cây JSX của App
, element DOM của modal lại được render vào #modal-root
nhờ có Portal.
Khi bạn click nút “Mở Modal”, state isModalOpen
thay đổi, khiến component Modal
được render. createPortal
bên trong Modal
sẽ đưa nội dung modal ra ngoài #root
. Khi click overlay hoặc nút đóng trong modal, hàm onClose
(chính là handleCloseModal
được truyền từ App
) sẽ được gọi, cập nhật state, và React sẽ unmount component Modal
, đồng thời clean-up effect sẽ xóa element modal khỏi DOM.
Những Lưu Ý Quan Trọng Khi Sử Dụng Portals
Context Vẫn Hoạt Động Như Mong Đợi
Như đã đề cập, một trong những lợi ích lớn của Portals là cách chúng tương tác với hệ thống của React. Mặc dù element được render ở vị trí DOM khác, component vẫn nằm trong cây component logic của React. Điều này có nghĩa là Context API vẫn hoạt động bình thường. Nếu component cha (trong cây React) cung cấp Context, component con được render qua Portal *vẫn có thể* tiêu thụ Context đó.
Ví dụ, nếu bạn có <ThemeProvider>
bao bọc ứng dụng của mình và bạn sử dụng Portal để render một modal, component bên trong modal vẫn có thể sử dụng useContext(ThemeContext)
để truy cập theme hiện tại.
Xử Lý Sự Kiện (Event Handling)
Event bubbling qua cây component React là một đặc điểm quan trọng cần ghi nhớ. Sự kiện từ portal buble lên qua tổ tiên của component *trong cây React*, bất kể vị trí DOM của chúng. Điều này rất tiện lợi vì bạn có thể tập trung vào logic của component mà không lo lắng về cấu trúc DOM phức tạp.
Accessibility (A11y)
Khi sử dụng Portals cho các thành phần như modal, việc đảm bảo khả năng truy cập là cực kỳ quan trọng. Mặc dù Portals giải quyết vấn đề hiển thị, bạn cần tự xử lý các khía cạnh về Accessibility như:
- Focus Management: Khi modal mở, focus của bàn phím nên được chuyển vào bên trong modal (focus trap) để người dùng không thể tương tác với nội dung phía sau modal. Khi modal đóng, focus nên quay trở lại element đã mở nó.
- ARIA Attributes: Sử dụng các thuộc tính ARIA phù hợp (ví dụ:
role="dialog"
,aria-modal="true"
) để các công cụ hỗ trợ (screen readers) hiểu được vai trò và trạng thái của modal. - Keyboard Interaction: Đảm bảo modal có thể đóng bằng phím ESC.
Việc xử lý A11y cho modals có thể khá phức tạp. Có nhiều thư viện UI components (như Mantine, Material UI, Ant Design – xem Xây Dựng Giao Diện Đẹp Mắt Với Mantine) đã tích hợp sẵn các tính năng A11y cho các component như modal, nên việc sử dụng chúng có thể giúp bạn tiết kiệm rất nhiều công sức.
Vị Trí của DOM Node Đích
Vị trí tốt nhất cho node DOM đích của portal thường là trực tiếp bên trong <body>
và ngoài div gốc của ứng dụng. Điều này đảm bảo node đích có thể nhận position: fixed
và z-index
cao mà không bị ảnh hưởng bởi các element cha.
So Sánh: Rendering Thông Thường vs. React Portal
Để làm rõ sự khác biệt, hãy cùng xem bảng so sánh sau:
Đặc điểm | Rendering Thông Thường | React Portal |
---|---|---|
Vị trí trong cây DOM | Element con được đặt bên trong element DOM của component cha. Cây DOM phản ánh cây component React. | Element con được đặt tại một node DOM đích *bất kỳ*, nằm ngoài cây DOM của component cha. |
Kế thừa CSS/Stacking Context | Component con bị ảnh hưởng trực tiếp bởi CSS của component cha (ví dụ: overflow: hidden , z-index của cha). |
Component con độc lập với CSS của component cha. Có thể dễ dàng đặt position: fixed và z-index cao để hiển thị đè lên mọi thứ. |
Event Bubbling | Sự kiện nổi bọt lên theo cây DOM *và* cây component React (vì chúng song song). | Sự kiện nổi bọt lên theo cây component React *chứ không phải* cây DOM nơi element được render. Handlers trên các component tổ tiên trong cây React vẫn hoạt động. |
Use Cases Phổ biến | Phần lớn các component UI thông thường. | Modals, Tooltips, Popovers, Dropdown menus, Loaders toàn màn hình, các thành phần cần hiển thị “đè” hoặc tại vị trí cố định. |
Khi Nào KHÔNG Nên Dùng Portals?
Đừng lạm dụng Portals. Chúng là một công cụ mạnh mẽ cho các trường hợp đặc biệt. Hầu hết các component trong ứng dụng của bạn nên được render theo cách thông thường, kế thừa cấu trúc DOM của component cha. Chỉ sử dụng Portal khi bạn thực sự cần “nhảy” ra khỏi cây DOM hiện tại để giải quyết vấn đề về hiển thị (styling) hoặc để tận dụng cơ chế event bubbling độc đáo của nó.
Kết Luận
React Portals là một tính năng hữu ích giúp các lập trình viên React giải quyết các thách thức phổ biến trong việc xây dựng các thành phần UI phức tạp như modal, tooltip, hay loader toàn màn hình. Bằng cách cho phép render component con vào một node DOM tùy ý bên ngoài cây DOM của component cha, Portals giúp chúng ta dễ dàng quản lý styling (đặc biệt là z-index
và overflow
) mà không làm mất đi những lợi ích cốt lõi của React như quản lý state, context, và cơ chế event bubbling theo cây component logic.
Hiểu và sử dụng Portals đúng lúc, đúng chỗ sẽ nâng cao kỹ năng xây dựng giao diện của bạn, giúp ứng dụng của bạn hoạt động ổn định và có khả năng mở rộng tốt hơn. Đây là một kiến thức quan trọng trên Lộ trình học React của bạn.
Hãy thử áp dụng Portals vào các dự án của mình khi đối mặt với những vấn đề về hiển thị mà rendering thông thường khó giải quyết. Chúc các bạn thành công!
Hẹn gặp lại trong bài viết tiếp theo của series React Roadmap!