Skip to content

Thay đổi giao diện với State và useState Hook (phần 3)

Thực hành tạo, mở, đóng Modal sử dụng useState, State Lifting, Conditional Rendering, Children Props.
vị trí đặt modalIsVisible và state lifting
sử dụng childern để chèn nội component khác vào modal
condition rendering để mở và đóng modal
conditional rendering để thay đổi trạng thái nút
Bài này chốt lại toàn bộ những gì đã học về useState bằng một ví dụ thực tế: Modal — component xuất hiện ở khắp mọi nơi trong production, và là bài tập tổng hợp hoàn hảo cho state lifting, conditional rendering, và children props.
Mục tiêu cuối bài: click “New Post” → modal mở ra chứa form → submit hoặc click backdrop/nút X → modal đóng lại.

State đặt ở đâu?

Câu hỏi đầu tiên cần trả lời trước khi viết code: isModalOpen đặt trong component nào?
Muốn biết, phải hiểu rõ cấu trúc của các component.

Cấu trúc component

PostList
├── Modal (backdrop + khung trắng)
│ └── NewPost (form bên trong modal)
└── Post (lặp lại cho từng bài)
Nút “New Post” nằm trong PostList.
Nút X và backdrop nằm trong Modal.
NewPost chỉ là form thuần — không biết gì về modal.
Áp dụng quy tắc từ bài trước: state đặt ở component thấp nhất có thể, nhưng phải đủ cao để tất cả component cần dùng đều có thể nhận qua props.
Những component nào cần biết modal đang mở hay đóng?
PostList — cần biết để render nút “New Post” hay “Đang mở…”
Modal — cần biết để render hay không render
NewPost — không cần biết, chỉ lo render form
PostList là cha chung của cả Modal lẫn nút “New Post” — nên isModalOpen đặt ở đây.

Children Props và render cái gì bên trong Modal

Trước khi viết Modal, cần hiểu children props — một loại props đặc biệt trong React.
Thông thường bạn truyền data qua props có tên cụ thể: title, author, onSubmit… Nhưng Modal không biết trước bên trong nó sẽ chứa gì — lúc thì NewPost, lúc thì form đăng nhập, lúc thì hộp thoại xác nhận xóa.
children giải quyết điều đó: bất cứ thứ gì bạn đặt giữa thẻ mở và thẻ đóng của component sẽ được truyền vào dưới dạng props.children.
// Dùng Modal như một "wrapper"
<Modal onClose={handleClose}>
<NewPost onSubmit={handleSubmit} />
</Modal>

// Bên trong Modal, props.children là <NewPost />
const Modal = (props: ModalProps) => {
return (
<div class="backdrop">
<div class="modal-box">
{props.children} {/* render bất cứ thứ gì được truyền vào */}
</div>
</div>
);
};
Modal không cần biết nội dung bên trong là gì — nó chỉ lo phần backdrop, khung trắng, và nút đóng. Nội dung là việc của component cha quyết định.

Viết Modal component

// Modal.tsx
interface ModalProps {
onClose: () => void
children: any
}

const Modal = (props: ModalProps) => {
return (
<>
{/* Backdrop — click vào sẽ đóng modal */}
<div
class="fixed inset-0 bg-black/50 z-40"
onClick={props.onClose}
/>

{/* Modal box — nằm trên backdrop */}
<div class="fixed inset-0 z-50 flex items-center justify-center p-4">
<div class="w-full max-w-lg bg-white border-2 border-black shadow-[4px_4px_0_0] p-6">

{/* Nút đóng */}
<button
onClick={props.onClose}
class="float-right text-sm font-semibold hover:underline"
>
✕ Đóng
</button>

{/* Nội dung do cha truyền vào */}
{props.children}
</div>
</div>
</>
);
};

export default Modal
onClose được dùng ở hai chỗ: backdrop và nút X — cả hai đều chỉ gọi một hàm duy nhất từ cha truyền xuống, không tự quản lý state.

Conditional Rendering cho Modal đóng mở

isModalOpen là boolean — dùng nó để quyết định render Modal hay không. Có 3 cách phổ biến:

Cách 1 — Tách ra biến trước khi return

Dễ đọc nhất khi nội dung phức tạp:
let modalContent = null; // mặc định không render gì

if (isModalOpen) {
modalContent = (
<Modal onClose={handleCloseModal}>
<NewPost
onTitleChange={handleTitleChange}
onAuthorChange={handleAuthorChange}
onSubmit={handleSubmit}
/>
</Modal>
Want to print your doc?
This is not the way.
Try clicking the ··· in the right corner or using a keyboard shortcut (
CtrlP
) instead.