Tìm hiểu cách tận dụng React custom hooks để trích xuất và tái sử dụng logic thành phần, cải thiện khả năng bảo trì, kiểm thử mã và kiến trúc ứng dụng tổng thể.
React Custom Hooks: Trích xuất logic thành phần để tái sử dụng
React hooks đã cách mạng hóa cách chúng ta viết các thành phần React, mang lại một cách tiếp cận thanh lịch và hiệu quả hơn để quản lý trạng thái và các tác dụng phụ. Trong số các hook khác nhau có sẵn, custom hooks nổi bật như một công cụ mạnh mẽ để trích xuất và tái sử dụng logic thành phần. Bài viết này cung cấp một hướng dẫn toàn diện để hiểu và triển khai React custom hooks, giúp bạn xây dựng các ứng dụng dễ bảo trì, kiểm thử và mở rộng hơn.
React Custom Hooks là gì?
Về bản chất, một custom hook là một hàm JavaScript có tên bắt đầu bằng "use" và có thể gọi các hook khác. Nó cho phép bạn trích xuất logic thành phần vào các hàm có thể tái sử dụng, từ đó loại bỏ sự trùng lặp mã và thúc đẩy cấu trúc thành phần sạch hơn. Không giống như các thành phần React thông thường, custom hooks không hiển thị bất kỳ giao diện người dùng nào; chúng chỉ đơn thuần đóng gói logic.
Hãy nghĩ chúng là các hàm có thể tái sử dụng có thể truy cập trạng thái React và các tính năng vòng đời. Chúng là một cách tuyệt vời để chia sẻ logic có trạng thái giữa các thành phần khác nhau mà không cần dùng đến các higher-order components (HOCs) hoặc render props, những thứ thường dẫn đến mã khó đọc và bảo trì.
Tại sao nên sử dụng Custom Hooks?
Lợi ích của việc sử dụng custom hooks rất nhiều:
- Khả năng tái sử dụng: Viết logic một lần và tái sử dụng nó trên nhiều thành phần. Điều này làm giảm đáng kể sự trùng lặp mã và làm cho ứng dụng của bạn dễ bảo trì hơn.
- Cải thiện Tổ chức Mã: Trích xuất logic phức tạp vào custom hooks làm cho các thành phần của bạn gọn gàng hơn, dễ đọc và hiểu hơn. Các thành phần trở nên tập trung hơn vào các trách nhiệm hiển thị cốt lõi của chúng.
- Nâng cao khả năng kiểm thử: Custom hooks dễ dàng kiểm thử độc lập. Bạn có thể kiểm thử logic của hook mà không cần hiển thị một thành phần, dẫn đến các bài kiểm thử mạnh mẽ và đáng tin cậy hơn.
- Tăng cường khả năng bảo trì: Khi logic thay đổi, bạn chỉ cần cập nhật nó ở một nơi – custom hook – thay vì trong mọi thành phần nơi nó được sử dụng.
- Giảm Boilerplate: Custom hooks có thể đóng gói các mẫu phổ biến và các tác vụ lặp đi lặp lại, giảm lượng mã boilerplate bạn cần viết trong các thành phần của mình.
Tạo Custom Hook đầu tiên của bạn
Hãy minh họa việc tạo và sử dụng một custom hook với một ví dụ thực tế: tìm nạp dữ liệu từ một API.
Ví dụ: useFetch
- Một Hook tìm nạp dữ liệu
Hãy tưởng tượng bạn thường xuyên cần tìm nạp dữ liệu từ các API khác nhau trong ứng dụng React của mình. Thay vì lặp lại logic tìm nạp trong mỗi thành phần, bạn có thể tạo một hook useFetch
.
import { useState, useEffect } from 'react';
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const abortController = new AbortController();
const signal = abortController.signal;
const fetchData = async () => {
setLoading(true);
try {
const response = await fetch(url, { signal: signal });
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const json = await response.json();
setData(json);
setError(null); // Clear any previous errors
} catch (error) {
if (error.name === 'AbortError') {
console.log('Fetch aborted');
} else {
setError(error);
}
setData(null); // Clear any previous data
} finally {
setLoading(false);
}
};
fetchData();
return () => {
abortController.abort(); // Cleanup function to abort the fetch on unmount or URL change
};
}, [url]); // Re-run effect when the URL changes
return { data, loading, error };
}
export default useFetch;
Giải thích:
- Biến trạng thái: Hook sử dụng
useState
để quản lý dữ liệu, trạng thái loading và trạng thái lỗi. - useEffect: Hook
useEffect
thực hiện việc tìm nạp dữ liệu khi thuộc tínhurl
thay đổi. - Xử lý lỗi: Hook bao gồm xử lý lỗi để bắt các lỗi tiềm ẩn trong quá trình tìm nạp. Mã trạng thái được kiểm tra để đảm bảo phản hồi thành công.
- Trạng thái Loading: Trạng thái
loading
được sử dụng để cho biết liệu dữ liệu vẫn đang được tìm nạp hay không. - AbortController: Sử dụng API AbortController để hủy yêu cầu tìm nạp nếu thành phần bị hủy gắn kết hoặc URL thay đổi. Điều này ngăn ngừa rò rỉ bộ nhớ.
- Giá trị trả về: Hook trả về một đối tượng chứa các trạng thái
data
,loading
vàerror
.
Sử dụng Hook useFetch
trong một Thành phần
Bây giờ, hãy xem cách sử dụng custom hook này trong một thành phần React:
import React from 'react';
import useFetch from './useFetch';
function UserList() {
const { data: users, loading, error } = useFetch('https://jsonplaceholder.typicode.com/users');
if (loading) return <p>Đang tải người dùng...</p>;
if (error) return <p>Lỗi: {error.message}</p>;
if (!users) return <p>Không tìm thấy người dùng nào.</p>;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name} ({user.email})</li>
))}
</ul>
);
}
export default UserList;
Giải thích:
- Thành phần nhập hook
useFetch
. - Nó gọi hook với URL API.
- Nó giải cấu trúc đối tượng trả về để truy cập các trạng thái
data
(đổi tên thànhusers
),loading
vàerror
. - Nó hiển thị nội dung khác nhau một cách có điều kiện dựa trên các trạng thái
loading
vàerror
. - Nếu dữ liệu có sẵn, nó hiển thị danh sách người dùng.
Các mẫu Custom Hook nâng cao
Ngoài việc tìm nạp dữ liệu đơn giản, custom hooks có thể được sử dụng để đóng gói logic phức tạp hơn. Dưới đây là một vài mẫu nâng cao:
1. Quản lý trạng thái với useReducer
Đối với các tình huống quản lý trạng thái phức tạp hơn, bạn có thể kết hợp custom hooks với useReducer
. Điều này cho phép bạn quản lý các chuyển đổi trạng thái một cách dễ đoán và có tổ chức hơn.
import { useReducer } from 'react';
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
throw new Error();
}
}
function useCounter() {
const [state, dispatch] = useReducer(reducer, initialState);
const increment = () => dispatch({ type: 'increment' });
const decrement = () => dispatch({ type: 'decrement' });
return { count: state.count, increment, decrement };
}
export default useCounter;
Cách sử dụng:
import React from 'react';
import useCounter from './useCounter';
function Counter() {
const { count, increment, decrement } = useCounter();
return (
<div>
<p>Số đếm: {count}</p>
<button onClick={increment}>Tăng</button>
<button onClick={decrement}>Giảm</button>
</div>
);
}
export default Counter;
2. Tích hợp Context với useContext
Custom hooks cũng có thể được sử dụng để đơn giản hóa việc truy cập React Context. Thay vì sử dụng useContext
trực tiếp trong các thành phần của bạn, bạn có thể tạo một custom hook đóng gói logic truy cập context.
import { useContext } from 'react';
import { ThemeContext } from './ThemeContext'; // Giả sử bạn có ThemeContext
function useTheme() {
return useContext(ThemeContext);
}
export default useTheme;
Cách sử dụng:
import React from 'react';
import useTheme from './useTheme';
function MyComponent() {
const { theme, toggleTheme } = useTheme();
return (
<div style={{ backgroundColor: theme.background, color: theme.color }}>
<p>Đây là thành phần của tôi.</p>
<button onClick={toggleTheme}>Chuyển đổi chủ đề</button>
</div>
);
}
export default MyComponent;
3. Debouncing và Throttling
Debouncing và throttling là các kỹ thuật được sử dụng để kiểm soát tốc độ thực thi một hàm. Custom hooks có thể được sử dụng để đóng gói logic này, giúp dễ dàng áp dụng các kỹ thuật này cho các trình xử lý sự kiện.
import { useState, useEffect, useRef } from 'react';
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
export default useDebounce;
Cách sử dụng:
import React, { useState } from 'react';
import useDebounce from './useDebounce';
function SearchInput() {
const [searchValue, setSearchValue] = useState('');
const debouncedSearchValue = useDebounce(searchValue, 500); // Debounce trong 500ms
useEffect(() => {
// Thực hiện tìm kiếm với debouncedSearchValue
console.log('Đang tìm kiếm:', debouncedSearchValue);
// Thay thế console.log bằng logic tìm kiếm thực tế của bạn
}, [debouncedSearchValue]);
const handleChange = (event) => {
setSearchValue(event.target.value);
};
return (
<input
type="text"
value={searchValue}
onChange={handleChange}
placeholder="Tìm kiếm..."
/>
);
}
export default SearchInput;
Thực tiễn tốt nhất để viết Custom Hooks
Để đảm bảo custom hooks của bạn hiệu quả và dễ bảo trì, hãy làm theo các thực tiễn tốt nhất sau:
- Bắt đầu bằng "use": Luôn đặt tên custom hooks của bạn với tiền tố "use". Quy ước này báo hiệu cho React rằng hàm tuân thủ các quy tắc của hook và có thể được sử dụng trong các thành phần hàm.
- Giữ sự tập trung: Mỗi custom hook nên có một mục đích rõ ràng và cụ thể. Tránh tạo các hook quá phức tạp xử lý quá nhiều trách nhiệm.
- Trả về các giá trị hữu ích: Trả về một đối tượng chứa tất cả các giá trị và hàm mà thành phần sử dụng hook cần. Điều này làm cho hook linh hoạt và có thể tái sử dụng hơn.
- Xử lý lỗi một cách duyên dáng: Bao gồm xử lý lỗi trong custom hooks của bạn để ngăn chặn hành vi không mong muốn trong các thành phần của bạn.
- Xem xét Cleanup: Sử dụng hàm dọn dẹp trong
useEffect
để ngăn ngừa rò rỉ bộ nhớ và đảm bảo quản lý tài nguyên đúng cách. Điều này đặc biệt quan trọng khi xử lý các subscription, timers hoặc event listeners. - Viết bài kiểm thử: Kiểm thử kỹ lưỡng custom hooks của bạn một cách độc lập để đảm bảo chúng hoạt động như mong đợi.
- Tài liệu hóa Hooks của bạn: Cung cấp tài liệu rõ ràng cho custom hooks của bạn, giải thích mục đích, cách sử dụng và bất kỳ hạn chế tiềm năng nào.
Những cân nhắc toàn cầu
Khi phát triển ứng dụng cho đối tượng toàn cầu, hãy ghi nhớ những điều sau:
- Quốc tế hóa (i18n) và Bản địa hóa (l10n): Nếu custom hook của bạn xử lý văn bản hoặc dữ liệu hướng đến người dùng, hãy xem xét cách nó sẽ được quốc tế hóa và bản địa hóa cho các ngôn ngữ và khu vực khác nhau. Điều này có thể liên quan đến việc sử dụng thư viện như
react-intl
hoặci18next
. - Định dạng Ngày và Giờ: Hãy lưu ý các định dạng ngày và giờ khác nhau được sử dụng trên toàn thế giới. Sử dụng các hàm hoặc thư viện định dạng thích hợp để đảm bảo ngày và giờ được hiển thị chính xác cho mỗi người dùng.
- Định dạng Tiền tệ: Tương tự, xử lý định dạng tiền tệ một cách thích hợp cho các khu vực khác nhau.
- Khả năng tiếp cận (a11y): Đảm bảo custom hooks của bạn không tác động tiêu cực đến khả năng tiếp cận của ứng dụng. Hãy xem xét người dùng khuyết tật và tuân thủ các thực tiễn tốt nhất về khả năng tiếp cận.
- Hiệu suất: Hãy nhận thức về các tác động tiềm ẩn đến hiệu suất của custom hooks, đặc biệt khi xử lý logic phức tạp hoặc tập dữ liệu lớn. Tối ưu hóa mã của bạn để đảm bảo nó hoạt động tốt cho người dùng ở các địa điểm khác nhau với tốc độ mạng thay đổi.
Ví dụ: Định dạng Ngày Quốc tế hóa với Custom Hook
import { useState, useEffect } from 'react';
import { DateTimeFormat } from 'intl';
function useFormattedDate(date, locale) {
const [formattedDate, setFormattedDate] = useState('');
useEffect(() => {
try {
const formatter = new DateTimeFormat(locale, {
year: 'numeric',
month: 'long',
day: 'numeric',
});
setFormattedDate(formatter.format(date));
} catch (error) {
console.error('Lỗi định dạng ngày:', error);
setFormattedDate('Ngày không hợp lệ');
}
}, [date, locale]);
return formattedDate;
}
export default useFormattedDate;
Cách sử dụng:
import React from 'react';
import useFormattedDate from './useFormattedDate';
function MyComponent() {
const today = new Date();
const enDate = useFormattedDate(today, 'en-US');
const frDate = useFormattedDate(today, 'fr-FR');
const deDate = useFormattedDate(today, 'de-DE');
return (
<div>
<p>Ngày US: {enDate}</p>
<p>Ngày Pháp: {frDate}</p>
<p>Ngày Đức: {deDate}</p>
</div>
);
}
export default MyComponent;
Kết luận
React custom hooks là một cơ chế mạnh mẽ để trích xuất và tái sử dụng logic thành phần. Bằng cách tận dụng custom hooks, bạn có thể viết mã sạch hơn, dễ bảo trì và kiểm thử hơn. Khi bạn trở nên thành thạo hơn với React, việc nắm vững custom hooks sẽ cải thiện đáng kể khả năng xây dựng các ứng dụng phức tạp và có thể mở rộng của bạn. Hãy nhớ tuân thủ các thực tiễn tốt nhất và xem xét các yếu tố toàn cầu khi phát triển custom hooks để đảm bảo chúng hiệu quả và dễ tiếp cận cho nhiều đối tượng khác nhau. Hãy nắm bắt sức mạnh của custom hooks và nâng cao kỹ năng phát triển React của bạn!