Trong React, children là prop chứa mọi thứ bạn viết giữa thẻ mở và thẻ đóng của một component. Một component render {children} ở đâu đó trong đầu ra sẽ trở thành component bọc (wrapper): nó cung cấp khung, còn nơi gọi cung cấp nội dung.
Cả hai thẻ card dùng chung một kiểu viền, một khoảng đệm và một kiểu tiêu đề, nhưng mỗi thẻ chứa nội dung hoàn toàn khác nhau. Thêm một <Card title="Help"> thứ ba có đường link bên trong, nó cũng có ngay cái khung đó mà không tốn công gì.
children đến với component như thế nào
JSX biến nội dung lồng bên trong thành một prop. Hai dòng này tạo ra cùng một phần tử:
<Card title="Profile"><p>Ada</p></Card>
<Card title="Profile" children={<p>Ada</p>} />
Vậy children là một prop bình thường, chỉ có cách truyền đặc biệt. Bạn destructure nó như mọi prop khác (function Card({ children })) hoặc đọc qua props.children. Trang về props trình bày props nói chung.
Thứ được đưa vào children phụ thuộc vào những gì bạn viết giữa hai thẻ. Một phần tử con thì nhận đúng phần tử đó. Nhiều phần tử con thì nhận một mảng. Văn bản thì nhận một chuỗi, còn không có gì thì nhận undefined.
Console hiển thị object, array of 2, string và undefined. Bạn hiếm khi phải bận tâm: render {children} xử lý được tất cả các trường hợp này, và null, undefined, true, false thì không render ra gì. Hình dạng của children chỉ quan trọng khi bạn cố kiểm tra hoặc sửa đổi nó, và đó chính là điều phần cuối khuyên bạn đừng làm.
Component layout
Cách dùng children phổ biến nhất là component layout: khung trang, bố cục có thanh bên, một cột căn giữa. Component nắm cấu trúc và khoảng cách, còn mỗi trang dùng nó thì truyền vào nội dung riêng.
function PageLayout({ children }) {
return (
<div className="page">
<Header />
<main className="page-content">{children}</main>
<Footer />
</div>
);
}
function AboutPage() {
return (
<PageLayout>
<h1>About us</h1>
<p>We teach people to code.</p>
</PageLayout>
);
}
PageLayout không biết và cũng không cần biết trang giới thiệu chứa gì. Sự tách biệt đó chính là mục đích: sửa header một lần, mọi trang được bọc trong layout đều nhận thay đổi.
Nhiều slot với các prop có tên
children là một slot. Khi component cần nội dung ở nhiều chỗ, hãy truyền các phần thêm qua các prop có tên. Prop nào cũng chứa được JSX, nên một modal có thể nhận title, footer và phần thân qua children.
Khung modal quản lý viền, khoảng đệm và thứ tự của ba vùng, còn nơi gọi điền nội dung cho từng vùng. Các framework khác gọi chúng là slot; trong React, chúng chỉ là props. Cặp <>...</> quanh hai nút là một fragment, giúp nhóm chúng lại mà không thêm phần tử thừa. Một dialog thật còn được render vào document.body bằng portal để nằm trên toàn bộ trang.
Chuyển thẻ <p> vào prop title, phần văn bản thân sẽ hiện trong header: component quyết định mỗi prop nằm ở đâu, nơi gọi chỉ quyết định bên trong có gì.
children dạng hàm
Đôi khi component bọc giữ một state nào đó và nơi gọi nên được quyết định cách vẽ nó. Thay vì một phần tử, nơi gọi truyền một hàm làm children, và component bọc gọi hàm đó với các giá trị. Mẫu này được gọi là render prop.
Một Toggle vẽ ra nút bấm, cái kia vẽ ra checkbox, từ cùng một logic. Trước khi có hook, render prop là cách chính để chia sẻ logic có state. Ngày nay một custom hook như useToggle() thường dễ đọc hơn, nhưng bạn vẫn sẽ gặp render prop trong các thư viện về danh sách, form và animation.
API Children và cloneElement
React xuất ra một đối tượng Children với các hàm hỗ trợ duyệt qua children: Children.map, Children.forEach, Children.count, Children.toArray và Children.only. Cùng với cloneElement, hàm sao chép một phần tử kèm props mới, chúng cho phép component cha kiểm tra và thay đổi những gì nó được truyền vào. Đội ngũ React xếp chúng vào nhóm API cũ (legacy): vẫn chạy được, nhưng code mới nên tránh.
Lý do là chúng chỉ thấy các phần tử được viết trực tiếp giữa hai thẻ. Chúng không thể thấy những gì các phần tử đó render ra.
Danh sách hiển thị bốn mục, nhưng console lại báo Children.count = 3, vì TwoMore là một phần tử con dù nó render ra bao nhiêu mục đi nữa. cloneElement cũng đưa prop style cho TwoMore, nhưng component này bỏ qua nó, nên mục thứ ba và thứ tư không có màu xen kẽ. Bất kỳ ai tách vài phần tử <li> ra thành một component cũng sẽ làm hỏng component cha mà không hề chạm vào nó.
Cách sửa là thôi can thiệp vào children và truyền dữ liệu thay thế:
function NumberedList({ items }) {
return (
<ol>
{items.map((item, i) => (
<li key={item.id} style={{ color: i % 2 ? 'gray' : 'black' }}>
{item.label}
</li>
))}
</ol>
);
}
Giờ danh sách tự lo việc render từng hàng, và không có gì phụ thuộc vào cách nơi gọi viết JSX. Khi component cha cần chia sẻ giá trị với các component con lồng sâu (tab đang chọn, theme), hãy dùng context thay vì sao chép props vào chúng.
Khai báo kiểu cho children trong TypeScript
Trong TypeScript, hãy khai báo children với kiểu React.ReactNode. Kiểu này bao gồm mọi thứ React có thể render: phần tử, chuỗi, số, mảng các giá trị đó, null, undefined và boolean.
import type { ReactNode } from 'react';
type CardProps = {
title: string;
children: ReactNode;
};
function Card({ title, children }: CardProps) {
return (
<section>
<h3>{title}</h3>
{children}
</section>
);
}
Với render prop, hãy khai báo đúng kiểu hàm của nó: children: (on: boolean, toggle: () => void) => ReactNode. Đặt children là tùy chọn (children?: ReactNode) khi component vẫn có nghĩa lúc để trống.
Lỗi thường gặp
Quên render children. Nếu một component nhận children nhưng không bao giờ đặt {children} vào đầu ra, nội dung sẽ âm thầm biến mất. Không có cảnh báo nào cả.
Gọi children như hàm khi nó không phải là hàm. children() chỉ chạy được khi nơi gọi truyền vào một hàm. Nếu component cần một render prop, hãy nói rõ điều đó qua tên hoặc tài liệu của nó, hoặc nhận một prop có tên như render để ý định được rõ ràng.
Sửa đổi children. Các phần tử là chỉ đọc. React đóng băng chúng ở chế độ phát triển, nên gán giá trị vào children.props sẽ ném lỗi ở đó. Hãy tạo đầu ra mới thay vì sửa.
Câu hỏi thường gặp
props.children trong React là gì?
Đó là nội dung được viết giữa thẻ mở và thẻ đóng của một component. Trong <Card><p>Hi</p></Card>, hàm Card nhận thẻ <p> qua props.children và tự quyết định hiển thị nó ở đâu.
children có phải là một prop đặc biệt không?
Chỉ đặc biệt ở cách truyền. JSX đưa nội dung lồng bên trong vào một prop tên children, nhưng bên trong component nó là một prop bình thường. Bạn cũng có thể truyền nó tường minh, như <Card children={<p>Hi</p>} />, và kết quả vẫn như nhau.
Làm sao để truyền nhiều khối nội dung vào một component?
Dùng các prop có tên cho những khối thêm. Một prop có thể chứa JSX giống hệt children, nên <Modal title={<h2>Delete?</h2>} footer={<button>OK</button>}>Body</Modal> cho component ba slot.
Có nên dùng Children.map và cloneElement không?
Hãy tránh chúng trong code mới. Chúng chỉ thấy các phần tử được viết trực tiếp giữa hai thẻ, không thấy những gì các phần tử đó render ra, nên sẽ hỏng ngay khi ai đó bọc một phần tử con trong một component khác. Thay vào đó, hãy truyền một mảng dữ liệu qua prop, hoặc dùng context.
children có kiểu gì trong TypeScript?
Dùng React.ReactNode. Kiểu này bao gồm mọi thứ React có thể render: phần tử, chuỗi, số, mảng, null, undefined và boolean.