Chào mừng các bạn trở lại với chuỗi bài viết React Roadmap! Hành trình khám phá React của chúng ta đã đi qua những khái niệm cơ bản nhưng vô cùng quan trọng như React là gì, Component Class và Function, JSX, Props và State, cho đến các kỹ thuật nâng cao hơn một chút như kết hợp Component, Vòng đời Component, làm việc với Danh sách, Xử lý sự kiện, và cả Refs. Hôm nay, chúng ta sẽ lặn sâu vào hai mẫu thiết kế (design patterns) phổ biến trong React để chia sẻ logic và hành vi giữa các component: **Render Props** và **Higher-Order Components (HOCs)**.
Khi ứng dụng React của bạn phát triển, bạn sẽ nhận thấy mình cần chia sẻ những logic phức tạp – ví dụ: quản lý trạng thái loading/error khi fetch data, theo dõi vị trí chuột, xử lý các sự kiện bàn phím chung, v.v. – giữa nhiều component khác nhau. Việc sao chép và dán (copy-paste) logic này là không hiệu quả và khó bảo trì. Đây là lúc các mẫu thiết kế như Render Props và HOCs trở nên cực kỳ hữu ích.
Mặc dù Hooks hiện đại đã giải quyết rất nhiều vấn đề mà Render Props và HOCs từng giải quyết, việc hiểu rõ hai mẫu thiết kế này vẫn rất quan trọng. Chúng xuất hiện rất nhiều trong các thư viện phổ biến và codebases cũ, và việc nắm vững chúng giúp bạn hiểu sâu hơn về cách React hoạt động cũng như các kỹ thuật chia sẻ logic trước kỷ nguyên Hooks. Hơn nữa, việc hiểu nguyên lý đằng sau chúng còn giúp bạn viết Hooks tùy chỉnh (custom hooks) tốt hơn.
Trong bài viết này, chúng ta sẽ cùng tìm hiểu Render Props và HOCs là gì, cách chúng hoạt động, ưu nhược điểm của từng mẫu, và khi nào thì nên sử dụng chúng. Hãy bắt đầu nhé!
Mục lục
Higher-Order Components (HOCs): Nâng Cấp Component Của Bạn
Higher-Order Component (HOC) không phải là một tính năng đặc biệt của React, mà là một mẫu thiết kế xuất phát từ nguyên tắc hàm bậc cao (higher-order functions) trong JavaScript. Một 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 chức năng được bổ sung.
Ví dụ: bạn có thể tạo một HOC để thêm khả năng “loading state” cho bất kỳ component nào cần fetch data. Thay vì mỗi component phải tự quản lý state `isLoading` và hiển thị spinner, bạn bọc component đó trong HOC, và HOC sẽ xử lý việc này.
Cấu Trúc Của Một HOC Đơn Giản
Một HOC thường trông như thế này:
function withSomeEnhancement(WrappedComponent) {
// Hàm này trả về một React Component mới
return function EnhancedComponent(props) {
// Bạn có thể thêm logic ở đây (ví dụ: state, effects, fetch data)
const someExtraProp = 'Giá trị bổ sung';
// Render WrappedComponent, truyền các props ban đầu
// cộng thêm các props mới hoặc đã thay đổi
return <WrappedComponent {...props} someExtraProp={someExtraProp} />;
};
}
Và cách sử dụng:
// Tạo một component cơ bản
function MyComponent(props) {
return <div>Xin chào, {props.name}! {props.someExtraProp}</div>;
}
// Sử dụng HOC để "nâng cấp" MyComponent
const EnhancedMyComponent = withSomeEnhancement(MyComponent);
// Sử dụng EnhancedMyComponent trong JSX
<EnhancedMyComponent name="Thế giới" />
Ở đây, `EnhancedMyComponent` là component mới được tạo ra. Khi bạn render nó, bên trong HOC `withSomeEnhancement` sẽ chạy, thêm prop `someExtraProp`, và sau đó render `MyComponent` gốc với cả prop `name` ban đầu và `someExtraProp` mới.
Tại Sao Sử Dụng HOCs?
- Chia sẻ logic phi-UI: HOCs rất tốt cho việc chia sẻ logic như quản lý subscription (ví dụ: đăng ký sự kiện mạng, Redux store), fetch data, xử lý authentication state, v.v.
- Tái sử dụng code: Giúp bạn tránh lặp lại cùng một logic trong nhiều component.
- Tách biệt mối quan tâm (Separation of Concerns): Logic quản lý dữ liệu/state được tách ra khỏi UI component.
Ví Dụ Thực Tế: HOC `withLoading`
Hãy tạo một HOC đơn giản để hiển thị thông báo “Loading…” nếu prop `isLoading` là `true`.
import React from 'react';
function withLoading(WrappedComponent) {
return function LoadingWrapper({ isLoading, ...otherProps }) {
if (isLoading) {
return <div>Đang tải dữ liệu...</div>;
}
return <WrappedComponent {...otherProps} />;
};
}
// Component ví dụ cần hiển thị dữ liệu sau khi tải xong
function DataDisplay({ data }) {
if (!data) {
return <div>Không có dữ liệu để hiển thị.</div>;
}
return (
<ul>
{data.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
// Áp dụng HOC
const EnhancedDataDisplay = withLoading(DataDisplay);
// Cách sử dụng
function App() {
const [data, setData] = React.useState(null);
const [isLoading, setIsLoading] = React.useState(true);
React.useEffect(() => {
// Giả lập fetch data
setTimeout(() => {
setData([{ id: 1, name: 'Item A' }, { id: 2, name: 'Item B' }]);
setIsLoading(false);
}, 2000);
}, []);
return (
<div>
<h2>Ví dụ HOC withLoading</h2>
<EnhancedDataDisplay isLoading={isLoading} data={data} />
</div>
);
}
// Export component App để sử dụng trong ứng dụng React thực tế
// export default App;
Trong ví dụ này, `withLoading` HOC nhận prop `isLoading`. Nếu `isLoading` là `true`, nó render một div thông báo. Ngược lại, nó render `DataDisplay` gốc với các props còn lại.
Nhược Điểm Của HOCs
- Xung đột tên props: Nếu HOC thêm vào một prop có tên trùng với prop mà component gốc đã sử dụng, có thể gây ra lỗi hoặc hành vi không mong muốn. Bạn cần đặt tên props cẩn thận trong HOC hoặc cung cấp cách cấu hình tên prop.
- Khó truy vết nguồn gốc props: Khi một component bị bọc bởi nhiều HOC, rất khó để biết một prop cụ thể đến từ HOC nào hoặc đến từ đâu.
- Wrapper Hell: Việc kết hợp nhiều HOC có thể tạo ra một cấu trúc component lồng nhau phức tạp, khiến việc debug và hiểu cấu trúc trở nên khó khăn.
- Không làm việc tốt với Refs: Theo mặc định, HOC không truyền `ref` xuống component được bọc. Cần sử dụng `React.forwardRef` để xử lý điều này, làm tăng độ phức tạp.
- Không dễ dàng áp dụng cho component Functional & Hooks: Mặc dù HOC có thể bọc cả functional component, nhưng cách nó hoạt động (thêm props) không tương thích tự nhiên với cách Hooks chia sẻ logic (sử dụng stateful logic *bên trong* component).
Render Props: Điều Khiển Render Từ Bên Ngoài
Mẫu Render Props sử dụng một cách tiếp cận khác để chia sẻ logic. Thay vì bọc component, một component sử dụng Render Props sẽ **nhận một hàm thông qua một prop** (thường là prop `render`, hoặc có thể là prop `children`, nếu sử dụng kiểu Function as a Child). Component này sẽ gọi hàm đó, truyền cho nó dữ liệu/logic mà nó quản lý, và hàm đó sẽ trả về JSX mà component gốc sẽ render.
Nói cách khác, component không tự quyết định cách hiển thị dữ liệu của nó, mà ủy quyền việc rendering đó cho một hàm được truyền từ bên ngoài.
Cấu Trúc Của Một Component Sử Dụng Render Props Đơn Giản
Component quản lý logic:
import React, { useState } from 'react';
function LogicProvider(props) {
const [value, setValue] = useState('Giá trị ban đầu');
// Logic phức tạp...
const changeValue = () => {
setValue('Giá trị đã thay đổi: ' + Date.now());
};
// Component gọi prop 'render' (hoặc 'children')
// và truyền dữ liệu/logic của nó vào hàm này
return (
<div>
{props.render({
value: value,
changeValue: changeValue
})}
</div>
);
}
Cách sử dụng (truyền hàm vào prop `render`):
function App() {
return (
<div>
<h2>Ví dụ Render Props</h2>
<LogicProvider
render={({ value, changeValue }) => (
<div>
<p>Giá trị từ LogicProvider: <strong>{value}</strong></p>
<button onClick={changeValue}>Thay đổi giá trị</button>
</div>
)}
/>
</div>
);
}
// export default App;
Ở đây, `LogicProvider` không biết cách hiển thị dữ liệu (`value`, `changeValue`). Nó chỉ biết quản lý logic đó. Component `App` (hoặc bất cứ component cha nào) khi sử dụng `LogicProvider` sẽ truyền một hàm vào prop `render`. Hàm này nhận dữ liệu/logic từ `LogicProvider` làm đối số và trả về JSX mà nó muốn hiển thị. `LogicProvider` sau đó chỉ đơn giản là gọi hàm này và render kết quả.
Biến thể phổ biến của Render Props là sử dụng prop `children` (còn gọi là Function as a Child):
function LogicProvider(props) {
// ... (logic tương tự như trên) ...
return (
<div>
{/* Sử dụng children prop thay vì render */}
{typeof props.children === 'function'
? props.children({
value: value,
changeValue: changeValue
})
: null}
</div>
);
}
// Cách sử dụng với children prop
function App() {
return (
<div>
<h2>Ví dụ Render Props (Function as a Child)</h2>
<LogicProvider>
{({ value, changeValue }) => (
<div>
<p>Giá trị từ LogicProvider: <strong>{value}</strong></p>
<button onClick={changeValue}>Thay đổi giá trị</button>
</div>
)}
</LogicProvider>
</div>
);
}
// export default App;
Tại Sao Sử Dụng Render Props?
- Luồng dữ liệu rõ ràng: Bạn thấy rõ ràng dữ liệu/logic được chia sẻ đang được truyền vào hàm render như thế nào. Không có “magic props” xuất hiện bất ngờ.
- Tránh xung đột tên props: Dữ liệu được truyền qua đối số của hàm, không phải qua props của component, loại bỏ nguy cơ xung đột tên prop.
- Kiểm soát UI linh hoạt: Component cha hoàn toàn kiểm soát cách dữ liệu được hiển thị. Component quản lý logic không quan tâm đến UI.
- Kết hợp dễ dàng: Có thể lồng nhiều component Render Props vào nhau để kết hợp logic.
Ví Dụ Thực Tế: Component `MouseTracker`
Hãy tạo một component theo dõi vị trí chuột và sử dụng Render Prop để hiển thị vị trí đó theo cách bạn muốn.
import React, { useState, useEffect } from 'react';
function MouseTracker(props) {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMouseMove = (event) => {
setPosition({
x: event.clientX,
y: event.clientY
});
};
window.addEventListener('mousemove', handleMouseMove);
// Cleanup function: Loại bỏ event listener khi component unmount
// Nhắc lại kiến thức từ Vòng đời Component!
return () => {
window.removeEventListener('mousemove', handleMouseMove);
};
}, []); // Empty dependency array means this effect runs once after the initial render
// Render prop có thể là 'render' hoặc 'children'
const renderProp = props.render || props.children;
// Component gọi hàm render prop và truyền vị trí chuột
return (
<div style={{ border: '1px dashed black', padding: '20px', minHeight: '100px' }}>
{typeof renderProp === 'function' ? renderProp(position) : null}
</div>
);
}
// Cách sử dụng MouseTracker với Render Prop (prop 'render')
function AppRenderProps() {
return (
<div>
<h2>Ví dụ Render Props (MouseTracker)</h2>
<MouseTracker
render={({ x, y }) => (
<h3>Vị trí chuột hiện tại: ({x}, {y})</h3>
)}
/>
</div>
);
}
// Cách sử dụng MouseTracker với Function as a Child (prop 'children')
function AppFunctionAsChild() {
return (
<div>
<h2>Ví dụ Render Props (Function as a Child MouseTracker)</h2>
<MouseTracker>
{({ x, y }) => (
<p>
<strong>X:</strong> {x}, <strong>Y:</strong> {y}
</p>
)}
</MouseTracker>
</div>
);
}
// Export các component ví dụ để sử dụng
// export { AppRenderProps, AppFunctionAsChild };
Trong ví dụ này, `MouseTracker` chỉ lo việc theo dõi vị trí chuột. Nó không quan tâm vị trí đó sẽ được hiển thị dưới dạng `
`, `
`, hay thậm chí là một `
Nhược Điểm Của Render Props
- Callback Hell (JSX Nesting): Khi sử dụng nhiều component Render Props lồng nhau, cấu trúc JSX có thể trở nên sâu và khó đọc, giống như “callback hell” trong JavaScript bất đồng bộ.
- Hiệu năng tiềm ẩn: Nếu hàm được truyền vào prop `render` hoặc `children` được định nghĩa inline trong phương thức `render` (hoặc thân hàm functional component), nó sẽ tạo ra một hàm mới trên mỗi lần render. Điều này có thể ảnh hưởng nhẹ đến hiệu năng trong một số trường hợp (mặc dù React thường tối ưu hóa đủ tốt để điều này hiếm khi là vấn đề lớn trong thực tế). Cần cẩn thận với việc tối ưu hóa bằng `React.memo` hoặc `useCallback` nếu cần.
Render Props vs HOCs: So Sánh Trực Tiếp
Để dễ hình dung sự khác biệt giữa hai mẫu thiết kế này, hãy cùng xem bảng so sánh:
| Đặc điểm | Higher-Order Components (HOCs) | Render Props |
|---|---|---|
| Cơ chế hoạt động | Hàm nhận vào Component, trả về Component mới được bọc. | Component nhận một hàm qua prop (thường là render hoặc children), hàm này trả về JSX. |
| Cách chia sẻ logic/dữ liệu | Truyền dữ liệu/logic qua props cho Component được bọc. | Truyền dữ liệu/logic qua đối số của hàm được truyền vào prop render/children. |
| Luồng dữ liệu | Ít rõ ràng, các props mới xuất hiện “ma thuật” trên component được bọc. | Rõ ràng, bạn thấy dữ liệu được truyền vào hàm render như thế nào. |
| Xung đột tên props | Có nguy cơ xảy ra nếu HOC thêm props trùng tên với props gốc. | Không xảy ra xung đột tên props vì dữ liệu là đối số hàm. |
| Khả năng kiểm soát UI | HOC quyết định render component được bọc hoặc UI thay thế (ví dụ: loading state). Ít kiểm soát cách dữ liệu được hiển thị bên trong component con. | Component cha hoàn toàn kiểm soát cách dữ liệu được hiển thị thông qua hàm render prop. Rất linh hoạt về UI. |
| Kết hợp nhiều logic | Có thể dẫn đến “wrapper hell” (lồng component sâu). | Có thể dẫn đến lồng các hàm/JSX sâu, nhưng luồng dữ liệu vẫn rõ ràng hơn. |
| Refs | Cần xử lý đặc biệt với React.forwardRef để truyền refs. |
Không ảnh hưởng đến refs của component con. |
| Phù hợp với Hooks? | Ít tự nhiên hơn, chủ yếu được dùng cho các thư viện cũ hoặc bọc các Class Component. | Ý tưởng truyền hàm và nhận dữ liệu gần gũi với cách Hooks hoạt động (nhận dữ liệu trả về từ custom hook). Mặc dù Hooks thường thay thế nhu cầu này, Render Props vẫn có thể được sử dụng cùng Hooks. |
| Ví dụ phổ biến | connect (Redux – phiên bản cũ), withRouter (React Router – phiên bản cũ). |
<Route render> (React Router), Consumer (Context API trước Hooks). |
Khi Nào Sử Dụng Mẫu Nào?
- Sử dụng HOCs khi:
- Bạn cần thêm các chức năng phi-UI hoặc các props tái sử dụng cho nhiều component.
- Logic chủ yếu liên quan đến quản lý state, subscription, hoặc fetching data mà không cần can thiệp sâu vào cách component con hiển thị UI.
- Bạn muốn áp dụng cùng một “lớp” hành vi cho một nhóm các component khác nhau.
- Làm việc với các codebase cũ sử dụng HOCs.
- Sử dụng Render Props khi:
- Bạn cần chia sẻ logic stateful nhưng đồng thời muốn component sử dụng logic đó có toàn quyền kiểm soát cách hiển thị UI.
- Bạn muốn luồng dữ liệu rõ ràng và minh bạch hơn.
- Cần tránh xung đột tên props.
- Logic cần chia sẻ có thể được biểu diễn dưới dạng một “nguồn dữ liệu” mà các component khác nhau có thể “kết nối” tới và render theo cách riêng.
Trong thế giới React hiện đại với Hooks, nhu cầu sử dụng trực tiếp HOCs hoặc Render Props để chia sẻ logic stateful đã giảm đi đáng kể. Hooks cung cấp một cách tự nhiên và gọn gàng hơn để tái sử dụng logic stateful (ví dụ: `useState`, `useEffect`, hoặc tạo custom hooks). Tuy nhiên, việc hiểu rõ Render Props và HOCs vẫn là kiến thức nền tảng quý giá, giúp bạn đọc hiểu code cũ, sử dụng thư viện hiện có, và thậm chí là hiểu sâu hơn nguyên lý hoạt động của các patterns chia sẻ logic.
Sự Tiến Hóa: Từ HOCs/Render Props Đến Hooks
Sự ra đời của React Hooks trong phiên bản 16.8 là một bước tiến lớn, giải quyết nhiều nhược điểm của HOCs và Render Props khi chia sẻ logic stateful.
- Hooks cho phép bạn tái sử dụng logic stateful trực tiếp bên trong Functional Component mà không cần bọc component (như HOC) hay truyền hàm render (như Render Props).
- Custom Hooks là cách chuẩn và được khuyến khích hiện nay để chia sẻ logic giữa các component Functional.
- Hooks giúp tránh “wrapper hell” và “callback hell” bằng cách làm phẳng cấu trúc component.
- Luồng dữ liệu với Hooks rõ ràng hơn nhiều so với HOCs ẩn danh.
Ví dụ `MouseTracker` với Hooks sẽ đơn giản hơn nhiều:
import React, { useState, useEffect } from 'react';
// Custom Hook để theo dõi vị trí chuột
function useMousePosition() {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMouseMove = (event) => {
setPosition({ x: event.clientX, y: event.clientY });
};
window.addEventListener('mousemove', handleMouseMove);
return () => {
window.removeEventListener('mousemove', handleMouseMove);
};
}, []);
return position; // Hook trả về dữ liệu/logic
}
// Component sử dụng custom hook
function MouseDisplayWithHooks() {
const { x, y } = useMousePosition(); // Sử dụng custom hook để lấy dữ liệu
return (
<div>
<h3>Vị trí chuột hiện tại (với Hooks): ({x}, {y})</h3>
</div>
);
}
// export default MouseDisplayWithHooks;
Rõ ràng là cách tiếp cận với Hooks cho cùng một bài toán trở nên gọn gàng và dễ đọc hơn đáng kể.
Tuy nhiên, điều này không có nghĩa là HOCs và Render Props đã lỗi thời hoàn toàn. Chúng vẫn là những mẫu thiết kế mạnh mẽ và có chỗ đứng trong một số trường hợp nhất định, đặc biệt khi xây dựng các thư viện UI hoặc làm việc với các API yêu cầu chúng (ví dụ: một số thư viện routing, state management cũ…). Quan trọng nhất là bạn cần hiểu chúng để đọc và làm việc với code React hiệu quả.
Kết Luận
Trong bài viết này, chúng ta đã tìm hiểu sâu về hai mẫu thiết kế quan trọng trong React để chia sẻ logic giữa các component: Render Props và Higher-Order Components (HOCs). Chúng ta đã thấy HOCs bọc component để thêm chức năng, còn Render Props sử dụng một hàm prop để ủy quyền việc render UI, mang lại sự linh hoạt cao hơn.
Mặc dù Hooks đã trở thành cách được ưu tiên để chia sẻ logic stateful trong React hiện đại, việc nắm vững HOCs và Render Props là cần thiết. Chúng giúp bạn hiểu rõ hơn về lịch sử phát triển của React, các kỹ thuật composition, và giúp bạn đọc hiểu các codebase cũ hoặc sử dụng các thư viện phổ biến còn dùng các mẫu này.
Hãy thử tự mình viết các ví dụ HOC và Render Props đơn giản để củng cố kiến thức. Hiểu được khi nào sử dụng mẫu nào (hoặc khi nào Hooks là lựa chọn tốt nhất) là kỹ năng quan trọng trên con đường trở thành một lập trình viên React giỏi.
Cảm ơn các bạn đã đồng hành trong bài viết này của chuỗi React Roadmap. Hẹn gặp lại trong các bài viết tiếp theo, nơi chúng ta sẽ khám phá những khía cạnh thú vị khác của React!



