Thành thạo Higher-Order Components trong React: Hướng dẫn chi tiết
Gần đây mình đã đào sâu vào các mẫu thiết kế trong React và ghi chép lại trong Notion, và Higher-Order Components (HOCs) thật sự là một bước ngoặt lớn! Chúng giúp bạn tái sử dụng mã và giữ cho ứng dụng sạch sẽ, gọn gàng.
Trong bài viết này, mình sẽ giải thích HOC là gì, chia sẻ các ví dụ đơn giản và khám phá các tình huống sử dụng thực tế — tất cả theo cách dễ hiểu nhất. Dù bạn mới bắt đầu với React hay đang muốn nâng cao trình độ, bạn sẽ thấy HOC rất đáng để học.
Cùng bắt đầu nhé!
HOC trong React là gì?
Higher-Order Component (HOC) là một hàm nhận vào một component và trả về một component mới với nhiều tính năng bổ sung. Hãy tưởng tượng bạn nâng cấp một chiếc xe đạp thường thành xe đạp điện — vẫn là xe đạp, nhưng có thể làm được nhiều hơn! 🚴♂️⚡
Ý tưởng cơ bản:
- Bắt đầu với một component, như nút bấm hoặc form.
- HOC sẽ “bao bọc” component đó, thêm các tính năng như lấy dữ liệu hoặc xác thực người dùng.
- Bạn sẽ có một component mới mạnh mẽ hơn mà không cần chỉnh sửa cái gốc.
Vì sao nên dùng HOC?
- Tiết kiệm thời gian: Không phải lặp lại cùng một đoạn mã ở nhiều nơi.
- Mã sạch: Giữ component của bạn tập trung và gọn gàng.
- Chia sẻ logic: Tái sử dụng logic ở nhiều nơi trong ứng dụng.
Hãy cùng xem các ví dụ thực tế để hiểu rõ hơn nhé.
Ví dụ 1: Gỡ lỗi props bằng HOC
Khi mới học React, mình rất hay nhầm lẫn và mất thời gian vì lỗi đánh máy trong props. Vì vậy, mình tạo một HOC để log các props ra console — một cách đơn giản để debug mà không làm rối mã.
Code: checkProps.js
// HOC to log props for debugging
export const checkProps = (Component) => {
// Returns a new component
return (props) => {
// Show props in console
console.log("Props received:", props);
// Pass all props to the original component
return <Component {...props} />;
};
};
Sử dụng: App.js
import { checkProps } from "./checkProps"; // Import HOC
import { UserInfo } from "./UserInfo"; // Import component
// Wrap UserInfo with HOC
const UserInfoWrapper = checkProps(UserInfo);
function App() {
return (
<div>
{/* Use wrapped component with props */}
<UserInfoWrapper name="John" age={23} />
</div>
);
}
export default App;
Component: UserInfo.js
// Simple component to show user info
export const UserInfo = ({ name, age }) => (
<div>
<h2>{name}</h2>
<p>Age: {age}</p>
</div>
);
Hoạt động thế nào?
checkProps
nhận một component và trả về một component mới.- Component mới này log các props ra console rồi truyền lại cho component gốc.
- Trong
App.js
, chúng ta dùngUserInfoWrapper
thay vìUserInfo
. - Mở console, bạn sẽ thấy props được in ra.
Lợi ích?
- Không cần thêm
console.log
thủ công ở mọi nơi. - Giúp phát hiện lỗi đánh máy nhanh chóng.
Ví dụ 2: Lấy dữ liệu bằng HOC
HOC rất tuyệt vời để xử lý dữ liệu, như lấy thông tin người dùng từ máy chủ. Ví dụ này cho thấy HOC lấy thông tin chi tiết của người dùng và cho phép bạn chỉnh sửa chúng trong biểu mẫu — thực tế và có thể tái sử dụng!
Code: includeUpdatableUser.js
import { useEffect, useState } from "react";
import axios from "axios"; // For server requests
// HOC to fetch and edit user data
export const includeUpdatableUser = (Component, userId) => {
return (props) => {
// Store original user data
const [user, setUser] = useState(null);
// Store editable user data
const [updatableUser, setUpdatableUser] = useState(null);
// Fetch data when component loads
useEffect(() => {
(async () => {
const response = await axios.get(`/users/${userId}`);
setUser(response.data); // Save original
setUpdatableUser(response.data); // Save editable
})();
}, []); // Run once
// Update data when user types
const userChangeHandler = (updates) => {
setUpdatableUser({ ...updatableUser, ...updates });
};
// Save changes to server
const userPostHandler = async () => {
const response = await axios.post(`/users/${userId}`, {
user: updatableUser,
});
setUser(response.data); // Update original
setUpdatableUser(response.data); // Update editable
};
// Reset to original data
const resetUserHandler = () => {
setUpdatableUser(user);
};
// Pass data and functions to component
return (
<Component
{...props}
updatableUser={updatableUser}
changeHandler={userChangeHandler}
userPostHandler={userPostHandler}
resetUserHandler={resetUserHandler}
/>
);
};
};
Cách sử dụng: UserInfoForm.js
import { includeUpdatableUser } from "./includeUpdatableUser";
// Wrap form with HOC
export const UserInfoForm = includeUpdatableUser(
({ updatableUser, changeHandler, userPostHandler, resetUserHandler }) => {
// Get name and age, or empty if null
const { name, age } = updatableUser || {};
// Show form or loading
return updatableUser ? (
<div>
<label>
Name:
<input
value={name}
onChange={(e) => changeHandler({ name: e.target.value })}
/>
</label>
<br />
<label>
Age:
<input
value={age}
onChange={(e) => changeHandler({ age: Number(e.target.value) })}
/>
</label>
<br />
<button onClick={resetUserHandler}>Reset</button>
<button onClick={userPostHandler}>Save</button>
</div>
) : (
<h3>Loading...</h3>
);
},
"3" // User ID
);
function App() {
return <UserInfoForm />;
}
export default App;
Nó hoạt động thế nào?
includeUpdatableUser
nhận một component và userId
.
- Nó lấy dữ liệu (ví dụ:
{ name: "John", age: 23 }
) từ/users/${userId}
. - Nó sử dụng hai state:
user
: Dữ liệu gốc.updatableUser
: Bản sao có thể chỉnh sửa.
Nó truyền 4 prop vào component:
updatableUser
: Dữ liệu hiển thị trong form.changeHandler
: Hàm cập nhật dữ liệu khi người dùng nhập liệu.userPostHandler
: Hàm lưu thay đổi lên server.resetUserHandler
: Hàm khôi phục lại dữ liệu gốc ban đầu.
UserInfoForm
là một form cho phép chỉnh sửa tên và tuổi người dùng, với các nút lưu và khôi phục.
Tại sao nó hữu ích?
HOC này xử lý toàn bộ các tác vụ liên quan đến dữ liệu — từ việc lấy, chỉnh sửa đến lưu — nên form của bạn chỉ cần tập trung vào phần giao diện UI.
Bạn có thể tái sử dụng nó cho bất kỳ người dùng nào chỉ bằng cách thay userId
.
Ứng dụng của tôi từng bị crash vì không xử lý trạng thái tải dữ liệu, nhưng chỉ cần thêm <h3>Loading...</h3>
là khắc phục được. Luôn đảm bảo xử lý tình huống đang tải!
Ví dụ 3: HOC có thể tái sử dụng cho bất kỳ tài nguyên nào
Người dùng HOC rất tuyệt, nhưng tôi muốn thứ gì đó linh hoạt hơn — cho sản phẩm, bài đăng hoặc bất kỳ dữ liệu nào. HOC chung này cắt giảm mã lặp lại và hoạt động với bất kỳ tài nguyên nào!
Code: includeUpdatableResource.js
import { useEffect, useState } from "react";
import axios from "axios";
// Capitalize names (e.g., "product" -> "Product")
const toCapital = (str) => str.charAt(0).toUpperCase() + str.slice(1);
// HOC for any resource
export const includeUpdatableResource = (Component, resourceUrl, resourceName) => {
return (props) => {
// Original data
const [data, setData] = useState(null);
// Editable data
const [updatableData, setUpdatableData] = useState(null);
// Fetch data on load
useEffect(() => {
(async () => {
const response = await axios.get(resourceUrl);
setData(response.data);
setUpdatableData(response.data);
})();
}, []); // Run once
// Update editable data
const changeHandler = (updates) => {
setUpdatableData({ ...updatableData, ...updates });
};
// Save to server
const dataPostHandler = async () => {
const response = await axios.post(resourceUrl, {
[resourceName]: updatableData,
});
setData(response.data);
setUpdatableData(response.data);
};
// Reset to original
const resetHandler = () => {
setUpdatableData(data);
};
// Dynamic props (e.g., product, onChangeProduct)
const resourceProps = {
[resourceName]: updatableData,
[`onChange${toCapital(resourceName)}`]: changeHandler,
[`onSave${toCapital(resourceName)}`]: dataPostHandler,
[`onReset${toCapital(resourceName)}`]: resetHandler,
};
// Pass props to component
return <Component {...props} {...resourceProps} />;
};
};
Cách sử dụng: ProductForm.js
import { includeUpdatableResource } from "./includeUpdatableResource";
// Wrap product form with HOC
export const ProductForm = includeUpdatableResource(
({ product, onChangeProduct, onSaveProduct, onResetProduct }) => {
const { name, price } = product || {};
return product ? (
<div>
<label>
Product Name:
<input
value={name}
onChange={(e) => onChangeProduct({ name: e.target.value })}
/>
</label>
<br />
<label>
Price:
<input
value={price}
onChange={(e) => onChangeProduct({ price: Number(e.target.value) })}
/>
</label>
<br />
<button onClick={onResetProduct}>Reset</button>
<button onClick={onSaveProduct}>Save</button>
</div>
) : (
<h3>Loading...</h3>
);
},
"/products/1", // Product URL
"product" // Resource name
);
function App() {
return <ProductForm />;
}
export default App;
Cách hoạt động
includeUpdatableResource
nhận vào:
Component
: Component cần được bọc.resourceUrl
: Đường dẫn dữ liệu (ví dụ: /products/1).resourceName
: Tên tài nguyên, ví dụ như "product".
Nó sẽ:
-
Lấy dữ liệu từ URL và lưu vào:
data
(dữ liệu gốc)updatableData
(dữ liệu có thể chỉnh sửa)
-
Tạo ra các prop động như:
- product
- onChangeProduct
- onSaveProduct
- onResetProduct
ProductForm
sử dụng các prop này để chỉnh sửa tên và giá của sản phẩm.
Tại sao nó hữu ích?
HOC này có thể hoạt động với bất kỳ loại dữ liệu nào — người dùng, sản phẩm, bài viết, v.v... Chỉ cần thay đổi URL và tên tài nguyên là được.
Nó giống như một công cụ đa năng “một size dùng cho tất cả” để lấy và chỉnh sửa dữ liệu.
Phần tạo prop động với toCapital
giúp mọi thứ trở nên mượt mà, chuyên nghiệp hơn. Hãy thử nghiệm với nhiều loại tài nguyên khác nhau để thấy sức mạnh của nó!
Những trường hợp sử dụng HOC trong thực tế
HOC không chỉ dùng để lấy dữ liệu mà còn giải quyết mọi loại vấn đề thực tế. Sau đây là ba trường hợp sử dụng phổ biến kèm theo ví dụ để truyền cảm hứng cho bạn!
1. Thêm xác thực người dùng với HOC
Giả sử bạn có một số trang chỉ dành cho người dùng đã đăng nhập. Bạn có thể sử dụng HOC để kiểm tra xem người dùng có được phép truy cập hay không.
Code: withAuth.js
import { useAuth } from "./useAuth"; // Get user info
import Redirect from "./Redirect"; // Redirect component
import AccessDenied from "./AccessDenied"; // Error component
// HOC to check login and role
export const withAuth = (requiredRole) => (Component) => (props) => {
const { user } = useAuth();
// No user? Go to login
if (!user) {
return <Redirect to="/login" />;
}
// Wrong role? Show error
if (requiredRole && user.role !== requiredRole) {
return <AccessDenied />;
}
// All clear? Show component
return <Component {...props} user={user} />;
};
Sử dụng: App.js
import { withAuth } from "./withAuth";
// Admin-only dashboard
const AdminDashboard = withAuth("admin")(({ user }) => (
<div>
<h2>Welcome, {user.name}!</h2>
<p>Admin Dashboard</p>
</div>
));
function App() {
return <AdminDashboard />;
}
export default App;
Nó hoạt động ra sao?
withAuth
kiểm tra propisAuthenticated
.- Nếu là
false
, nó ngăn truy cập và hiển thị thông báo. - Nếu là
true
, nó hiển thị component được bảo vệ.
2. Theo dõi lượt truy cập trang
Bạn cần theo dõi thời điểm người dùng truy cập một trang? HOC có thể tự động ghi lại.
Code: withTracking.js
import { useEffect } from "react";
import analytics from "./analytics"; // Fake analytics tool
// HOC to track page views
export const withTracking = (eventName) => (Component) => (props) => {
// Track load/unload
useEffect(() => {
analytics.trackPageView(eventName); // Log visit
return () => {
analytics.trackPageExit(eventName); // Log exit
};
}, []); // Run once
// Show component
return <Component {...props} />;
};
Cách sử dụng: App.js
import { withTracking } from "./withTracking";
// Track checkout page
const TrackedCheckout = withTracking("checkout_page")(() => (
<div>
<h2>Checkout</h2>
<input placeholder="Card number" />
<button>Pay</button>
</div>
));
function App() {
return <TrackedCheckout />;
}
export default App;
Tại sao nó hữu ích?
HOC này bổ sung tính năng theo dõi mà không cần chạm vào mã thành phần của bạn. Hoàn hảo để hiểu hành vi của người dùng!
3. Thêm chủ đề vào thành phần
Bạn muốn thêm chế độ sáng hoặc tối vào ứng dụng của mình? HOC có thể truyền kiểu chủ đề cho các thành phần.
Code: withTheme.js
import { useTheme } from "./useTheme"; // Get theme info
// HOC to add theme
export const withTheme = (Component) => (props) => {
const theme = useTheme(); // E.g., { color: "black" }
// Pass theme to component
return <Component {...props} theme={theme} />;
};
Cách sử dụng: App.js
import { withTheme } from "./withTheme";
// Themed button
const ThemedButton = withTheme(({ theme }) => (
<button style={{ background: theme.color, color: "white" }}>
Click Me
</button>
));
function App() {
return <ThemedButton />;
}
export default App;
Tại sao nó hữu ích?
HOC này giữ cho các thành phần được định dạng nhất quán. Sử dụng nó cho các nút, thẻ hoặc bất kỳ thứ gì cần có chủ đề!
Tại sao nên sử dụng HOC?
HOC giải quyết các vấn đề thực tế và giúp việc lập trình dễ dàng hơn. Đây là lý do tại sao tôi thích chúng:
- Phân tách các mối quan tâm : Giữ logic (như tìm nạp) tách biệt với UI (như biểu mẫu).
- Tái sử dụng mã : Viết logic một lần, sử dụng trong nhiều thành phần.
- Khả năng kiểm thử : Chỉ kiểm thử logic HOC, không kết hợp với UI.
- Làm sạch các thành phần : Tránh nhồi nhét các thành phần có móc hoặc tác dụng phụ.
HOC giúp tôi có cảm giác code được sắp xếp ngăn nắp, giống như sắp xếp lại bàn làm việc của mình vậy — rất đáng công sức!
Khi nào nên sử dụng HOC so với các mẫu khác?
HOC không phải lúc nào cũng là câu trả lời. Sau đây là hướng dẫn nhanh để chọn đúng công cụ:
- Tái sử dụng logic trên nhiều thành phần : Sử dụng HOC hoặc hook tùy chỉnh.
- Cần logic vòng đời + kết xuất : HOC là giải pháp phù hợp.
- Tái sử dụng logic nhỏ (trạng thái, hiệu ứng) : Sử dụng hook tùy chỉnh.
- Kiểm soát hoàn toàn bên trong JSX : Tránh sử dụng HOC ở đây.
React hiện đại thường thiên về các hook tùy chỉnh cho các trường hợp đơn giản, nhưng HOC lại nổi trội hơn trong việc đóng gói hành vi—như ghi nhật ký, xác thực, phân tích hoặc kết xuất có điều kiện. Lúc đầu, tôi đã thử hook cho mọi thứ, nhưng HOC tốt hơn cho logic lớn như xác thực. Biết cả hai để lựa chọn một cách khôn ngoan!
Mẹo cuối cùng: Làm cho HOC có thể kết hợp được
HOC giống như các khối xây dựng — bạn có thể xếp chồng chúng lên nhau để thêm nhiều tính năng hơn!
Ví dụ:
import { withAuth } from "./withAuth";
import { withTracking } from "./withTracking";
const MyComponent = () => <div>My App</div>;
// Chain HOCs
export default withAuth("admin")(withTracking("home_page")(MyComponent));
Hoặc sử dụng thư viện như Redux để tạo chuỗi sạch hơn:
import { compose } from "redux";
export default compose(withAuth("admin"), withTracking("home_page"))(MyComponent);
Tại sao nó hữu ích?
Khả năng kết hợp cho phép bạn kết hợp và ghép các HOC, như thêm xác thực và theo dõi vào một thành phần. Việc kết nối các HOC lúc đầu khá khó khăn, nhưng nó cực kỳ mạnh mẽ. Bắt đầu với hai HOC và tăng dần!
Kết luận
Higher-Order Components là một cách thông minh để tái sử dụng logic trong React. Chúng bao bọc các thành phần để thêm các tính năng như gỡ lỗi, tìm nạp dữ liệu hoặc bảo mật, giúp mã của bạn sạch sẽ và có tổ chức. Ghi chú Notion của tôi dạy tôi rằng chúng không khó - chỉ là những công cụ thông minh để xây dựng các ứng dụng tốt hơn.
Cảm ơn các bạn đã theo dõi!
All rights reserved