Bạn đã bao giờ nhìn vào một codebase React và cảm thấy "ngợp" trước sự phức tạp, các component lồng vào nhau chằng chịt và logic bị lặp lại ở khắp mọi nơi chưa? Nếu câu trả lời là có, bạn không hề đơn độc. Khi một ứng dụng lớn dần lên, việc giữ cho code có tổ chức, dễ đọc và dễ bảo trì trở thành một thách thức thực sự.
Đây chính là lúc React Design Patterns tỏa sáng. 🌟
Giống như các bản thiết kế chi tiết cho một ngôi nhà, React Design Patterns là những giải pháp đã được kiểm chứng qua thời gian cho các vấn đề phổ biến trong quá trình phát triển ứng dụng React. Chúng không phải là một thư viện hay framework cụ thể, mà là những phương pháp và tư duy giúp bạn cấu trúc component và quản lý state một cách hiệu quả.
Trong bài viết này, chúng ta sẽ cùng nhau khám phá những design pattern quan trọng và phổ biến nhất, từ kinh điển đến hiện đại, giúp bạn nâng tầm kỹ năng React của mình.
Tại sao phải quan tâm đến Design Patterns?
Sử dụng design patterns mang lại vô số lợi ích:
- Tái sử dụng (Reusability): Viết logic một lần và sử dụng lại ở nhiều nơi, giảm thiểu sự lặp lại code (Don't Repeat Yourself - DRY).
- Dễ bảo trì (Maintainability): Code được tổ chức tốt, rõ ràng giúp bạn và đồng đội dễ dàng tìm lỗi, sửa chữa và nâng cấp.
- Khả năng mở rộng (Scalability): Một cấu trúc vững chắc giúp ứng dụng dễ dàng thêm tính năng mới mà không phá vỡ những gì đã có.
- Tách biệt logic (Separation of Concerns): Giữ cho logic giao diện (UI) và logic nghiệp vụ (business logic) tách biệt, giúp các component trở nên đơn giản và tập trung hơn.
1. Container and Presentational Components Pattern
Đây là một trong những pattern nền tảng và kinh điển nhất, giúp tách biệt rõ ràng giữa "dữ liệu" và "giao diện".
-
Presentational Components:
- Nhiệm vụ: Chỉ quan tâm đến việc trông như thế nào. Chúng nhận dữ liệu và các hàm xử lý qua
props
và hiển thị ra UI. - Đặc điểm: Thường là các stateless functional component, không chứa logic phức tạp, không gọi API. Chúng rất dễ để tái sử dụng.
- Ví dụ: Một component
UserList
chỉ nhận một mảngusers
và hiển thị danh sách đó.
- Nhiệm vụ: Chỉ quan tâm đến việc trông như thế nào. Chúng nhận dữ liệu và các hàm xử lý qua
-
Container Components:
- Nhiệm vụ: Quan tâm đến việc hoạt động như thế nào. Chúng chứa logic nghiệp vụ, quản lý state, gọi API để lấy dữ liệu.
- Đặc điểm: Chúng cung cấp dữ liệu và các hàm xử lý (callbacks) cho các Presentational Components thông qua
props
. - Ví dụ: Một component
UserListContainer
sẽ gọi API để lấy danh sách người dùng, lưu vào state, và sau đó truyền danh sách này vào componentUserList
.
Ví dụ thực tế:
// 1. Presentational Component (chỉ lo hiển thị)
const UserList = ({ users }) => {
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)
}
// 2. Container Component (lo logic, lấy dữ liệu)
const UserListContainer = () => {
const [users, setUsers] = useState([])
useEffect(() => {
// Giả lập gọi API
fetch('https://api.example.com/users')
.then((res) => res.json())
.then((data) => setUsers(data))
}, [])
return <UserList users={users} />
}
✨ Lợi ích: Pattern này giúp bạn dễ dàng thay đổi giao diện mà không ảnh hưởng đến logic, và ngược lại. Nó cũng thúc đẩy việc tái sử dụng các component trình bày.
2. Higher-Order Components (HOCs)
HOC là một pattern nâng cao để tái sử dụng logic của component. Về cơ bản, HOC là một hàm nhận vào một component và trả về một component mới với các props
đã được nâng cấp.
Hãy tưởng tượng HOC như một người "trang trí" cho component của bạn. Bạn đưa cho họ một component trơn, họ sẽ "trang trí" thêm các tính năng (props) như dữ liệu, hàm xử lý, ... rồi trả lại cho bạn.
Ví dụ: Tạo một HOC để thêm tính năng logging
// Đây là HOC: một hàm nhận vào một Component
const withLogger = (WrappedComponent) => {
// và trả về một component mới
return (props) => {
useEffect(() => {
console.log(`Component ${WrappedComponent.name} đã được mount.`)
}, [])
// Render component gốc với các props của nó
return <WrappedComponent {...props} />
}
}
// Component gốc
const MyComponent = ({ message }) => <div>{message}</div>
// Sử dụng HOC để "bọc" MyComponent
const ComponentWithLogger = withLogger(MyComponent)
// Khi bạn render <ComponentWithLogger message="Hello HOC!" />,
// message sẽ được log ra console.
🔑 Khi nào dùng: HOC rất hữu ích cho các tác vụ lặp lại xuyên suốt ứng dụng (cross-cutting concerns) như xác thực người dùng, logging, kết nối với Redux store (connect
của react-redux
là một HOC nổi tiếng).
3. Render Props Pattern
Tương tự HOC, Render Props cũng là một kỹ thuật để chia sẻ code giữa các component. Tuy nhiên, thay vì "bọc" component, nó sử dụng một prop
là một hàm để chia sẻ logic.
Component sẽ gọi hàm này và truyền vào dữ liệu mà nó quản lý. Hàm này sẽ trả về JSX để render. Tên prop
không nhất thiết phải là render
, mà thường là children
để có cú pháp gọn gàng hơn.
Ví dụ: Component theo dõi vị trí chuột
const MouseTracker = ({ children }) => {
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 hàm children và truyền state vào.
Hàm này sẽ quyết định render cái gì.
*/}
{children(position)}
</div>
)
}
// Cách sử dụng
const App = () => {
return (
<MouseTracker>
{/* Đây là hàm được truyền vào `children` */}
{(mousePosition) => (
<h1>
Vị trí chuột là: ({mousePosition.x}, {mousePosition.y})
</h1>
)}
</MouseTracker>
)
}
✨ So sánh với HOC: Render Props thường được coi là rõ ràng và linh hoạt hơn HOC vì bạn có thể thấy rõ nguồn gốc của props
ngay trong hàm render
. Nó cũng tránh được vấn đề "wrapper hell" (quá nhiều component bọc nhau) của HOC.
4. Provider Pattern (với React Context)
Bạn có bao giờ phải truyền props
qua nhiều tầng component chỉ để component con sâu nhất có thể sử dụng chúng không? Vấn đề này được gọi là "prop drilling". The Provider Pattern, được hiện thực hóa bởi React Context API, là giải pháp hoàn hảo.
Pattern này cho phép bạn tạo ra một "nguồn dữ liệu toàn cục" (provider) mà bất kỳ component nào trong cây con của nó cũng có thể truy cập trực tiếp mà không cần truyền props qua từng cấp.
Ví dụ: Tạo Theme Provider cho ứng dụng
// 1. Tạo Context
const ThemeContext = React.createContext('light') // Giá trị mặc định
// 2. Tạo Provider Component
const ThemeProvider = ({ children }) => {
const [theme, setTheme] = useState('light')
const toggleTheme = () => {
setTheme((prevTheme) => (prevTheme === 'light' ? 'dark' : 'light'))
}
return (
// Cung cấp 'theme' và hàm 'toggleTheme' cho toàn bộ cây component con
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
)
}
// 3. Sử dụng trong App
const App = () => {
return (
<ThemeProvider>
<Toolbar />
</ThemeProvider>
)
}
// 4. Component con sử dụng Context
const Toolbar = () => {
const { theme, toggleTheme } = useContext(ThemeContext) // Dùng hook useContext
return (
<div style={{ background: theme === 'light' ? '#fff' : '#333' }}>
<button onClick={toggleTheme}>Toggle Theme</button>
</div>
)
}
🚀 Khi nào dùng: Lý tưởng cho việc chia sẻ các dữ liệu "toàn cục" như thông tin người dùng đăng nhập, theme (sáng/tối), ngôn ngữ...
5. Custom Hooks Pattern
Đây là pattern hiện đại và mạnh mẽ nhất trong React hiện nay. Với sự ra đời của Hooks (useEffect, useState), chúng ta có một cách tuyệt vời để trích xuất và tái sử dụng logic có state (stateful logic) từ các component.
Custom Hook về cơ bản là một hàm JavaScript có tên bắt đầu bằng use
và có thể gọi các Hooks khác bên trong nó.
Ví dụ: Tạo một custom hook useFetch
để gọi API
Thay vì lặp lại logic useState
và useEffect
trong mỗi component cần lấy dữ liệu, chúng ta có thể tạo một hook.
// Custom Hook
const useFetch = (url) => {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
setLoading(true)
fetch(url)
.then((res) => res.json())
.then((data) => setData(data))
.catch((err) => setError(err))
.finally(() => setLoading(false))
}, [url]) // Chạy lại khi url thay đổi
return { data, loading, error }
}
// Cách sử dụng trong component
const UserProfile = ({ userId }) => {
const {
data: user,
loading,
error,
} = useFetch(`https://api.example.com/users/${userId}`)
if (loading) return <p>Đang tải...</p>
if (error) return <p>Có lỗi xảy ra!</p>
return (
<div>
<h2>{user?.name}</h2>
<p>{user?.email}</p>
</div>
)
}
🏆 Tại sao Custom Hooks là lựa chọn hàng đầu? Custom Hooks giải quyết vấn đề chia sẻ logic một cách thanh lịch nhất. Nó không tạo thêm component lồng nhau như HOC hay Render Props, giúp cây component phẳng và code cực kỳ dễ đọc. Đây là cách được cộng đồng React khuyến khích sử dụng hiện nay.
Kết luận: Lựa chọn Pattern nào cho phù hợp?
Không có một pattern nào là "tốt nhất" cho mọi trường hợp. Việc lựa chọn phụ thuộc vào vấn đề bạn đang giải quyết:
- Tách biệt giao diện và logic? Dùng Container/Presentational.
- Chia sẻ dữ liệu toàn cục? Dùng Provider Pattern (Context API).
- Tái sử dụng logic có state? Custom Hooks là lựa chọn ưu tiên hàng đầu trong React hiện đại. HOC và Render Props vẫn hữu ích, đặc biệt khi làm việc với codebase cũ hoặc class components.
Nắm vững các design pattern này không chỉ giúp bạn viết code React tốt hơn mà còn rèn luyện tư duy giải quyết vấn đề một cách có hệ thống. Hãy bắt đầu áp dụng chúng vào dự án của bạn ngay hôm nay, và bạn sẽ thấy sự khác biệt rõ rệt trong chất lượng và khả năng bảo trì của mã nguồn.
Chúc bạn thành công trên hành trình chinh phục React!