Props và composition: truyền dữ liệu giữa component
Bài trước bạn đã dựng component đầu tiên và biết JSX cho phép viết markup ngay trong JavaScript. Nhưng nếu component chỉ hiển thị đúng một nội dung cố định thì gần như không dùng lại được, bạn sẽ phải copy paste. Props là cách React giải quyết việc đó. Bài này nằm trong cùng mạch dự án "ứng dụng quản lý chi tiêu" mà bạn sẽ hoàn thiện ở module 3.
Sau bài này, bạn có thể:
- Truyền props từ component cha xuống component con, đúng kiểu dữ liệu (chuỗi, số, biến).
- Destructuring props ngay trong tham số và đặt giá trị mặc định cho prop thiếu.
- Dùng
childrenđể bọc nội dung, tạo ra component layout tái sử dụng. - Nhận ra dấu hiệu nên tách component thay vì copy paste giao diện.
Props: tham số đầu vào của component
Bạn đã quen với attribute trong HTML:
<img src="avatar.png" alt="Ảnh đại diện" />
React component nhận dữ liệu theo đúng kiểu đó, gọi là props (viết tắt của properties). Khác biệt duy nhất: props do bạn tự định nghĩa.
function ExpenseItem(props) {
return (
<li className="expense-item">
<span>{props.title}</span>
<strong>{props.amount.toLocaleString('vi-VN')} đ</strong>
</li>
);
}
// Dùng ở component cha
<ExpenseItem title="Cà phê sáng" amount={35000} />
<ExpenseItem title="Gửi xe" amount={5000} />
Hai cách truyền giá trị, đừng lẫn lộn:
| Kiểu dữ liệu | Cách viết | Ví dụ |
|---|---|---|
| Chuỗi | dấu nháy | title="Cà phê sáng" |
| Số, biến, object, hàm | dấu ngoặc nhọn | amount={35000}, item={expense} |
Viết amount="35000" thì component nhận được chuỗi, gọi .toLocaleString() sẽ sai kiểu. Đây là lỗi số một của người mới.
Một điểm cần nhớ: props chỉ chảy một chiều, từ cha xuống con. Con không sửa được props của mình.
Important: Props là read-only. Viết
props.title = "Khác"bên trong component sẽ không có tác dụng và React sẽ cảnh báo. Muốn dữ liệu thay đổi, đó là việc của state ở bài sau.
flowchart TD
App["📱<br/>App (cha)"] -->|"title, amount"| Item["🧾<br/>ExpenseItem (con)"]
App -->|"title, children"| Card["🗂️<br/>Card (bọc nội dung)"]
Card --> Item
Thử ngay 1: Component ExpenseItem ở trên bị lỗi khi một khoản chi không có amount. Sửa bằng cách đặt amount = 0 làm giá trị mặc định khi destructuring, và kiểm tra lại kết quả hiển thị.
Xem gợi ý
Đổi thành function ExpenseItem({ title, amount = 0 }). Với amount = 0, toLocaleString vẫn chạy và hiển thị 0 đ thay vì crash.
Destructuring props và giá trị mặc định
Vì props là một object, bạn dùng được destructuring đã học ở module 1:
function ExpenseItem({ title, amount = 0, category = 'Khác' }) {
return (
<li className="expense-item">
<span>{title}</span>
<em>{category}</em>
<strong>{amount.toLocaleString('vi-VN')} đ</strong>
</li>
);
}
Cách viết này gọn hơn props.title, props.amount, và nhìn tham số là biết component cần gì. Giá trị mặc định chỉ kích hoạt khi prop là undefined, tức là không truyền, hoặc truyền undefined. Truyền amount={null} hay amount={0} thì mặc định không áp dụng.
Một mẹo nhỏ: giữ danh sách props dưới 5, 6 cái. Nếu vượt qua, thường là dấu hiệu component đang làm quá nhiều việc.
Thử ngay 2: Thêm prop note vào ExpenseItem để hiển thị ghi chú nhỏ, với mặc định là "Không có ghi chú". Sau đó render hai khoản chi: một khoản có note, một khoản không truyền note.
Xem đáp án
function ExpenseItem({ title, amount = 0, note = 'Không có ghi chú' }) {
return (
<li className="expense-item">
<span>{title}</span>
<small>{note}</small>
<strong>{amount.toLocaleString('vi-VN')} đ</strong>
</li>
);
}
<ExpenseItem title="Cà phê sáng" amount={35000} note="Uống với đồng nghiệp" />
<ExpenseItem title="Gửi xe" amount={5000} />
Khoản thứ hai hiển thị "Không có ghi chú" vì prop note là undefined.
children: khe cắm nội dung vào component
Thẻ <div> trong HTML bọc được mọi thứ bên trong nó. Component React cũng vậy, phần nội dung nằm giữa thẻ mở và thẻ đóng được React gom vào một prop đặc biệt tên là children.
function Card({ title, children }) {
return (
<section className="card">
<h2>{title}</h2>
<div className="card-body">{children}</div>
</section>
);
}
<Card title="Chi tiêu tháng 7">
<ExpenseItem title="Cà phê sáng" amount={35000} />
<ExpenseItem title="Gửi xe" amount={5000} />
</Card>
Card không cần biết bên trong là gì. Nó chỉ lo khung, viền, tiêu đề. Đây chính là composition: ghép component nhỏ vào component lớn thay vì viết một component khổng lồ biết mọi thứ.
Bạn cũng có thể truyền JSX qua prop thường, không chỉ children:
<Card title="Tổng kết" actions={<button>Thêm khoản chi</button>}>
<p>Chưa có dữ liệu</p>
</Card>
Bên trong Card, chỗ nào cần hiện nút thì render {actions}, chỗ nào cần nội dung thì render {children}.
Thử ngay 3: Tạo component Badge nhận children và hiển thị một nhãn nhỏ màu xám, dùng nó để gắn nhãn "Ăn uống" cạnh tên khoản chi.
Xem đáp án
function Badge({ children }) {
return <span className="badge">{children}</span>;
}
<Badge>Ăn uống</Badge>
children ở đây là chuỗi "Ăn uống", vẫn render bình thường vì JSX cho phép chèn chuỗi.
Tách component khi giao diện bắt đầu lặp lại
Giả sử bạn viết danh sách chi tiêu bằng cách copy paste:
<li className="expense-item"><span>Cà phê sáng</span><strong>35.000 đ</strong></li>
<li className="expense-item"><span>Gửi xe</span><strong>5.000 đ</strong></li>
<li className="expense-item"><span>Ăn trưa</span><strong>45.000 đ</strong></li>
Ba khối giống hệt nhau, chỉ khác dữ liệu. Quy tắc thực dụng: copy paste khối JSX lần thứ hai là lúc nên tách component, và biến phần khác nhau thành props. Ở đây khác nhau là title và amount, nên ExpenseItem ra đời.
Lợi ích không chỉ là code ngắn hơn. Khi cần thêm chức năng "xóa khoản chi", bạn sửa một chỗ trong ExpenseItem, cả danh sách được hưởng. Ngược lại, sửa ba khối copy paste là ba lần có thể sai.
Một lưu ý về cách đặt tên: component phải viết hoa chữ đầu (ExpenseItem). Viết thường, React sẽ hiểu đó là thẻ HTML và render ra thẻ rỗng.
Ôn tập nhanh: điền khuyết và flashcard
Điền vào chỗ trống để tự kiểm tra mức hiểu:
- Props truyền theo chiều _____, từ component cha xuống component _____.
- Bên trong component con, props là _____ (chỉ đọc), không được gán lại.
- Prop đặc biệt chứa nội dung nằm giữa thẻ mở và thẻ đóng là _____.
- Viết
amount="35000"thì component nhận kiểu _____, muốn nhận số phải viếtamount={35000}. - Giá trị mặc định của prop chỉ có tác dụng khi prop đó là _____.
Front: Props là gì? Back: Dữ liệu truyền từ component cha xuống component con, giống attribute của thẻ HTML.
Front: Truyền prop kiểu chuỗi trong JSX viết thế nào? Back: Dùng dấu nháy: title="Cà phê sáng".
Front: Truyền prop kiểu số hoặc biến trong JSX viết thế nào? Back: Đặt trong dấu ngoặc nhọn: amount={35000} hoặc item={expense}.
Front: Component con có được gán lại props không? Back: Không. Props là read-only, gán lại sẽ không có tác dụng và React cảnh báo.
Front: Destructuring props ngay trong tham số viết thế nào? Back: function ExpenseItem({ title, amount }) { ... }
Front: Giá trị mặc định của prop kích hoạt khi nào? Back: Khi prop đó là undefined, tức là không truyền hoặc truyền undefined.
Front: children là gì? Back: Prop đặc biệt chứa nội dung nằm giữa thẻ mở và thẻ đóng của component.
Front: Làm sao hiển thị children bên trong component? Back: Đặt {children} vào đúng vị trí trong JSX trả về.
Front: Composition trong React là gì? Back: Ghép component nhỏ vào component lớn qua props hoặc children thay vì viết một component làm mọi thứ.
Front: Có thể truyền JSX element qua prop thường không? Back: Có, ví dụ actions={} rồi render {actions}.
Front: Dấu hiệu nên tách một component mới? Back: Cùng một khối JSX bị copy paste nhiều lần và chỉ khác dữ liệu.
Front: Vì sao viết amount="35000" là lỗi tiềm ẩn? Back: Vì đó là chuỗi, gọi các hàm số như toLocaleString hay cộng trừ sẽ sai kiểu.
Bốn lỗi người mới hay mắc với props
- Quên dấu ngoặc nhọn với giá trị động.
amount=35000là sai cú pháp, phải làamount={35000}. Với chuỗi thì ngược lại, đừng bọc thêm ngoặc nhọn không cần thiết. - Cố sửa props trong component con. Nếu bạn thấy mình muốn
props.x = ..., hãy dừng lại: dữ liệu đó nên do component cha quản lý bằng state. - Truyền cả object rồi render trực tiếp.
{expense}vớiexpenselà object sẽ làm React báo lỗi. Hãy truyền từng thuộc tính, hoặc renderexpense.title. - Truyền quá nhiều props xuống quá nhiều tầng. Với hai, ba tầng thì bình thường, nhưng nếu bạn phải chuyển một prop xuyên qua component không dùng đến nó, đó là lúc cấu trúc component cần xem lại.
Mẹo cuối: đặt tên prop theo ý nghĩa nghiệp vụ (title, amount, category) chứ không theo kiểu hiển thị (text1, color2). Component đọc lên là hiểu.
Chốt lại: props là dây dẫn một chiều
- Props đưa dữ liệu từ cha xuống con, và chỉ đọc ở phía con.
- Destructuring trong tham số giúp component tự mô tả dữ liệu nó cần, kèm giá trị mặc định cho trường hợp thiếu.
childrenbiến component thành cái khung linh hoạt, mở đường cho composition.- Gặp JSX lặp lại lần thứ hai, hãy tách component và biến phần khác nhau thành props.
Bài này dừng ở dữ liệu tĩnh: props truyền vào rồi đứng yên. Bài tiếp theo sẽ thêm useState để component tự thay đổi giao diện khi người dùng bấm nút, ví dụ nút "Thêm khoản chi" thực sự làm danh sách dài ra.