Nếu coi việc xây dựng ứng dụng React như lắp ráp những khối LEGO, thì Component Composition (kết hợp thành phần) chính là nghệ thuật để bạn tạo ra những công trình vĩ đại từ những viên gạch nhỏ nhất. Bất kỳ ai cũng có thể truyền props
, nhưng để xây dựng các component linh hoạt, dễ tái sử dụng và có khả năng mở rộng tuyệt vời, bạn cần nắm vững các mẫu composition nâng cao.
Bài viết này sẽ giúp bạn đi từ một "thợ xây" trở thành một "kiến trúc sư" thực thụ trong React. Hãy cùng khám phá cách biến những component phức tạp, rối rắm thành các cấu trúc thanh lịch và mạnh mẽ.
Tại sao lại là "Composition" mà không phải "Inheritance"?
Trước khi bàn luận sâu hơn, hãy nhớ lại triết lý vàng của React: Composition over Inheritance (Ưu tiên kết hợp hơn kế thừa).
Thay vì tạo ra các chuỗi kế thừa phức tạp (Component B kế thừa từ A, C kế thừa từ B), React khuyến khích chúng ta xây dựng các component chuyên biệt, nhỏ gọn và "cắm" chúng vào nhau như những mảnh ghép. Cách tiếp cận này giúp tránh được các vấn đề như:
- "Prop Drilling": Phải truyền props qua nhiều tầng component không cần dùng đến.
- Khó tái sử dụng: Logic và UI bị ràng buộc chặt chẽ với nhau.
- Cấu trúc cứng nhắc: Khó thay đổi hoặc mở rộng về sau.
Giờ thì, hãy thắt dây an toàn và cùng đi khám phá các mẫu composition sẽ thay đổi cách bạn viết React mãi mãi!
1. Containment & Specialization
Đây là mẫu composition cơ bản nhất nhưng lại cực kỳ mạnh mẽ, thường bị bỏ qua. Nó sử dụng props.children
để tạo ra các "hộp chứa" linh hoạt.
🤔 Vấn đề:
Bạn có một component Card
nhưng nội dung bên trong nó có thể là bất cứ thứ gì: một bài viết, một sản phẩm, một hồ sơ người dùng... Làm sao để Card
không cần quan tâm đến nội dung chi tiết bên trong?
✨ Giải pháp: Dùng props.children
Component Card
chỉ cần định nghĩa phần khung (viền, bóng đổ,...) và dành một "lỗ hổng" cho nội dung bên ngoài truyền vào qua props.children
.
// Card.js
function Card(props) {
// Thêm các class để tạo style cho card
return <div className="card">{props.children}</div>
}
// App.js
function App() {
return (
<Card>
{/* Mọi thứ bên trong Card sẽ là `children` */}
<h1>Chào mừng đến với Blog!</h1>
<p>Đây là một ví dụ về Containment Pattern.</p>
</Card>
)
}
🧠 Nâng cao hơn với "Slots" (Specialization)
Khi bạn cần nhiều "lỗ hổng" hơn thì sao? Ví dụ, một Dialog
cần có title
, message
và actions
(các nút bấm).
// Dialog.js
function Dialog(props) {
return (
<div className="dialog-container">
<header className="dialog-header">{props.title}</header>
<main className="dialog-body">{props.message}</main>
<footer className="dialog-footer">{props.actions}</footer>
</div>
)
}
// App.js
function App() {
return (
<Dialog
title={<h1>Cảnh báo!</h1>}
message={<p>Bạn có chắc chắn muốn xóa không?</p>}
actions={
<>
<button>Hủy</button>
<button>Xóa</button>
</>
}
/>
)
}
Khi nào dùng: Khi bạn muốn tạo ra các component layout chung (Modal, Card, Sidebar, PageLayout) mà không cần quan tâm đến nội dung cụ thể bên trong.
2. Higher-Order Components (HOCs)
HOC là một khái niệm kinh điển trong React. Đây là một function nhận vào một component và trả về một component mới đã được "gia cố" thêm logic hoặc props.
🤔 Vấn đề:
Nhiều component khác nhau (ví dụ: UserProfile
, CommentList
, Article
) đều cần lấy dữ liệu từ một API. Lẽ nào chúng ta phải lặp lại logic fetch
dữ liệu ở khắp mọi nơi?
✨ Giải pháp: Tạo một HOC withData
HOC này sẽ bao bọc component gốc, đảm nhiệm việc lấy dữ liệu và truyền kết quả vào component gốc dưới dạng props.
// withData.js
import React, { useState, useEffect } from 'react'
// Đây là một HOC
function withData(WrappedComponent, dataSource) {
// Nó trả về một component mới
return function (props) {
const [data, setData] = useState(null)
useEffect(() => {
fetch(dataSource)
.then((res) => res.json())
.then((data) => setData(data))
}, [])
if (!data) {
return <div>Loading...</div>
}
// Truyền data đã fetch được và các props gốc vào component được bọc
return <WrappedComponent data={data} {...props} />
}
}
// CommentList.js
function CommentList({ data, ...props }) {
return (
<ul>
{data.map((comment) => (
<li key={comment.id}>{comment.body}</li>
))}
</ul>
)
}
// "Gia cố" CommentList với HOC
export const CommentListWithData = withData(
CommentList,
'https://api.example.com/comments',
)
Khi nào dùng:
- Chia sẻ logic xuyên suốt nhiều component (cross-cutting concerns) như: xác thực, logging, lấy dữ liệu.
- Thao tác với props, thêm/bớt/chỉnh sửa props trước khi truyền vào component.
✅ Ưu điểm: Logic được tái sử dụng triệt để.
❌ Nhược điểm: Có thể gây ra "wrapper hell" (quá nhiều component lồng nhau trong React DevTools), khó theo dõi props đến từ đâu.
3. Render Props
Render Props là một kỹ thuật mạnh mẽ để chia sẻ logic bằng cách truyền một function vào component qua props. Component này sẽ gọi function đó và truyền cho nó những dữ liệu cần thiết để render UI.
🤔 Vấn đề:
Bạn muốn tạo một component MouseTracker
có thể theo dõi vị trí con trỏ chuột. Nhưng bạn muốn các component khác nhau có thể hiển thị vị trí đó theo những cách khác nhau (một cái hiển thị tọa độ, một cái di chuyển hình ảnh theo chuột,...).
✨ Giải pháp: Dùng render
prop
Component MouseTracker
sẽ quản lý logic theo dõi chuột và gọi props.render
với tọa độ (x, y)
. Việc render cái gì là do component cha quyết định.
// MouseTracker.js
import React, { useState } from 'react'
function MouseTracker(props) {
const [position, setPosition] = useState({ x: 0, y: 0 })
const handleMouseMove = (event) => {
setPosition({
x: event.clientX,
y: event.clientY,
})
}
return (
<div style={{ height: '100vh' }} onMouseMove={handleMouseMove}>
{/* Gọi function được truyền qua prop và cung cấp state cho nó */}
{props.render(position)}
</div>
)
}
// App.js
function App() {
return (
<div>
<h1>Di chuyển chuột quanh màn hình!</h1>
<MouseTracker
render={({ x, y }) => (
// Logic render nằm hoàn toàn ở đây
<p>
Tọa độ chuột là ({x}, {y})
</p>
)}
/>
</div>
)
}
Lưu ý: Tên prop không nhất thiết phải là render
. Thường thì người ta sẽ dùng children
như một function (<MouseTracker>{mouse => ...}</MouseTracker>
), cách này còn được gọi là "Function as Children Pattern".
Khi nào dùng:
- Khi bạn muốn chia sẻ state hoặc logic động cho các component khác.
- Là một giải pháp thay thế linh hoạt cho HOCs, tránh được "wrapper hell".
✅ Ưu điểm: Rất rõ ràng về nguồn gốc của props, linh hoạt cao.
❌ Nhược điểm: Có thể tạo ra các cấu trúc lồng nhau trong code JSX.
4. Custom Hooks (vị vua hiện đại)
Với sự ra đời của React Hooks, Custom Hooks đã trở thành cách tiếp cận hiện đại và được ưa chuộng nhất để chia sẻ logic có state giữa các component. Nó giải quyết các vấn đề của HOCs và Render Props một cách thanh lịch hơn.
🤔 Vấn đề:
Cũng là bài toán chia sẻ logic (lấy dữ liệu, theo dõi chuột, truy cập local storage...), nhưng chúng ta muốn một cách đơn giản, không cần component bọc, không cần render prop.
✨ Giải pháp: Tạo một Custom Hook
Một Custom Hook đơn giản là một function JavaScript có tên bắt đầu bằng use
và có thể gọi các Hook khác bên trong nó (useState
, useEffect
...).
// useMousePosition.js
import { useState, useEffect } from 'react'
export function useMousePosition() {
const [position, setPosition] = useState({ x: 0, y: 0 })
useEffect(() => {
const handleMouseMove = (e) => {
setPosition({ x: e.clientX, y: e.clientY })
}
window.addEventListener('mousemove', handleMouseMove)
// Đừng quên cleanup listener!
return () => {
window.removeEventListener('mousemove', handleMouseMove)
}
}, []) // Chạy effect một lần duy nhất
return position
}
// App.js
import { useMousePosition } from './useMousePosition'
function App() {
// Sử dụng hook như một phép màu!
const { x, y } = useMousePosition()
return (
<div>
<h1>
Tọa độ chuột hiện tại: ({x}, {y})
</h1>
</div>
)
}
Khi nào dùng: Gần như mọi lúc bạn muốn chia sẻ logic có state. Đây thường là lựa chọn hàng đầu trong React hiện đại.
✅ Ưu điểm: Siêu đơn giản, không tạo thêm component lồng nhau, dễ đọc, dễ kiểm thử.
❌ Nhược điểm: Chỉ hoạt động trong các Function Component.
5. Compound Components
Mẫu này cho phép bạn tạo ra các component có mối quan hệ ngầm với nhau, hoạt động như một thể thống nhất, mang lại một API rất biểu cảm và linh hoạt cho người dùng. Hãy nghĩ đến cặp thẻ <select>
và <option>
trong HTML.
🤔 Vấn đề:
Bạn muốn xây dựng một component Tabs
phức tạp. Người dùng cần toàn quyền kiểm soát cấu trúc và style của từng Tab
và TabPanel
, nhưng vẫn muốn logic chuyển tab được quản lý tự động.
✨ Giải pháp: Dùng React Context
Chúng ta sẽ tạo một component cha (Tabs
) quản lý toàn bộ state (tab nào đang active) thông qua Context. Các component con (Tab
, TabPanel
) sẽ đọc từ Context đó để biết cách tự render.
// Tabs.js
import React, { useState, useContext, createContext } from 'react'
const TabContext = createContext()
export function Tabs({ children }) {
const [activeTab, setActiveTab] = useState('tab1')
const value = { activeTab, setActiveTab }
return <TabContext.Provider value={value}>{children}</TabContext.Provider>
}
export function TabList({ children }) {
return <div className="tab-list">{children}</div>
}
export function Tab({ id, children }) {
const { activeTab, setActiveTab } = useContext(TabContext)
const isActive = activeTab === id
return (
<button
onClick={() => setActiveTab(id)}
className={isActive ? 'active' : ''}
>
{children}
</button>
)
}
export function TabPanel({ id, children }) {
const { activeTab } = useContext(TabContext)
return activeTab === id ? <div>{children}</div> : null
}
// App.js
import { Tabs, TabList, Tab, TabPanel } from './Tabs'
function App() {
return (
<Tabs>
<TabList>
<Tab id="tab1">Tab 1</Tab>
<Tab id="tab2">Tab 2</Tab>
</TabList>
<TabPanel id="tab1">
<p>Đây là nội dung của Tab 1.</p>
</TabPanel>
<TabPanel id="tab2">
<p>Và đây là nội dung của Tab 2.</p>
</TabPanel>
</Tabs>
)
}
Khi nào dùng: Khi xây dựng các thư viện component UI phức tạp và cần một API khai báo rõ ràng như Tabs, Accordion, Dropdown Menu...
Tổng kết: Khi nào dùng gì?
Mẫu Pattern | Kịch bản tốt nhất | Ưu điểm |
---|---|---|
Containment/Slots | Xây dựng các component layout (Card, Modal, Dialog). | Đơn giản, trực quan. |
Higher-Order Comp. | Thêm logic chung cho các Class Component, hoặc trong các codebase cũ. | Tái sử dụng logic tốt. |
Render Props | Chia sẻ state động khi cần sự linh hoạt tối đa trong việc render. | Rõ ràng, mạnh mẽ. |
Custom Hooks | Lựa chọn mặc định để chia sẻ logic có state trong React hiện đại. | Sạch sẽ, đơn giản, không wrapper. |
Compound Comp. | Xây dựng các hệ thống component UI phức tạp với API biểu cảm. | API khai báo, linh hoạt cho người dùng. |
Việc nắm vững các mẫu component composition này không chỉ giúp code của bạn sạch hơn, dễ bảo trì hơn, mà còn mở ra một tư duy mới về cách cấu trúc ứng dụng. Hãy bắt đầu áp dụng chúng vào dự án tiếp theo, và bạn sẽ thấy sự khác biệt diệu kỳ.
Chúc bạn viết code vui vẻ với React!