Giới thiệu về Custom Hook
Trong React, Custom Hook là một cơ chế cho phép bạn đóng gói và tái sử dụng các logic quản lý trạng thái (state logic) giữa các component. Thay vì viết lại cùng một logic trong nhiều component, bạn có thể trích xuất nó vào một hàm có tiền tố use. Điều này giúp tách biệt logic khỏi giao diện người dùng (UI) và làm cho mã nguồn của bạn gọn gàng, dễ bảo trì hơn.
Bản chất của trạng thái (State) là dữ liệu nội bộ của một component, và việc thay đổi trạng thái sẽ kích hoạt component đó render lại, đảm bảo giao diện luôn đồng bộ với dữ liệu.
Cách triển khai Custom Hook
Quá trình tạo một custom hook thường bao gồm các bước sau:
- Xác định logic cần tái sử dụng: Tìm kiếm các đoạn mã lặp lại trong các component, chẳng hạn như quản lý trạng thái, xử lý side effect, hoặc lấy dữ liệu.
- Đóng gói logic: Tạo một hàm bắt đầu bằng
use, bên trong hàm này, bạn có thể gọi các hook có sẵn của React nhưuseState,useEffect,useReducer, v.v. - Cung cấp giao diện: Xác định những gì component sẽ cần từ hook và trả về nó, có thể là giá trị trạng thái, hàm xử lý, hoặc một đối tượng chứa nhiều giá trị.
Ví dụ: Tạo một hook tìm kiếm có debounce
Giả sử chúng ta có một component tìm kiếm cần trì hoãn việc gọi hàm tìm kiếm sau khi người dùng ngừng gõ trong một khoảng thời gian nhất định (debounce). Thay vì viết logic này trong component, chúng ta sẽ tạo một hook.
Component sử dụng hook
import React from 'react';
import { useDebouncedSearch } from './hooks';
const SearchComponent = () => {
const { searchTerm, setSearchTerm, inputProps } = useDebouncedSearch({
onSearch: (term) => {
console.log('Tìm kiếm với từ khóa:', term);
// Gọi API ở đây
},
delay: 300,
});
return (
<div>
<input {...inputProps} placeholder="Tìm kiếm..." />
<p>Từ khóa hiện tại: {searchTerm}</p>
</div>
);
};
export default SearchComponent;
Triển khai hook useDebouncedSearch
import { useState, useEffect, useRef } from 'react';
/**
* Một hook để thực hiện tìm kiếm với cơ chế debounce.
* @param {Object} config - Đối tượng cấu hình.
* @param {Function} config.onSearch - Hàm được gọi khi tìm kiếm.
* @param {number} [config.delay=500] - Thời gian debounce (ms).
* @returns {Object} - Đối tượng chứa trạng thái và thuộc tính cho input.
*/
export function useDebouncedSearch(config) {
const { onSearch, delay = 500 } = config;
const [searchTerm, setSearchTerm] = useState('');
const searchHandlerRef = useRef(onSearch);
const delayRef = useRef(delay);
// Cập nhật ref để luôn có giá trị mới nhất
useEffect(() => {
searchHandlerRef.current = onSearch;
}, [onSearch]);
useEffect(() => {
if (!searchTerm.trim()) return;
const timerId = setTimeout(() => {
searchHandlerRef.current(searchTerm);
}, delayRef.current);
return () => clearTimeout(timerId);
}, [searchTerm]);
const handleInputChange = (event) => {
setSearchTerm(event.target.value);
};
return {
searchTerm,
setSearchTerm,
inputProps: {
value: searchTerm,
onChange: handleInputChange,
},
};
}
Các loại giá trị trả về của Custom Hook
Custom hook có thể trả về nhiều loại giá trị khác nhau tùy thuộc vào nhu cầu:
1. Trả về trạng thái (State)
Đơn giản nhất là trả về một giá trị trạng thái duy nhất.
function useToggle(initialValue = false) {
const [isToggled, setIsToggled] = useState(initialValue);
return isToggled;
}
2. Trả về các hàm xử lý (Actions)
Bên cạnh trạng thái, bạn có thể trả về các hàm để component có thể tương tác với trạng thái đó.
function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
const increase = () => setCount(prev => prev + 1);
const decrease = () => setCount(prev => prev - 1);
return { count, increase, decrease };
}
// Sử dụng
const { count, increase } = useCounter(10);
<button onClick={increase}>Tăng</button>
3. Trả về đối tượng hoặc mảng
Trả về một đối tượng cho phép bạn nhóm nhiều giá trị lại với nhau, giúp việc sử dụng trong component trở nên rõ ràng hơn.
function useTextInput(defaultValue = '') {
const [inputValue, setInputValue] = useState(defaultValue);
const handleChange = (event) => setInputValue(event.target.value);
return { inputValue, handleChange };
}
// Sử dụng
const { inputValue, handleChange } = useTextInput('Giá trị mặc định');
<input value={inputValue} onChange={handleChange} />
4. Trả về Promise hoặc kết quả bất đồng bộ
Đối với các logic liên quan đến thao tác bất đồng bộ (ví dụ: gọi API), hook có thể trả về một Promise hoặc một đối tượng chứa trạng thái của thao tác đó.
function useApiData(url) {
const [responseData, setResponseData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(url);
const data = await response.json();
setResponseData(data);
} catch (err) {
setError(err);
} finally {
setIsLoading(false);
}
};
fetchData();
}, [url]);
return { responseData, isLoading, error };
}
Các loại Custom Hook phổ biến
| Loại | Mô tả | Ví dụ |
|---|---|---|
| Lấy dữ liệu | Đóng gói logic gọi API | useApiData(url) |
| Quản lý form | Xử lý trạng thái và xác thực form | useFormValidation() |
| Quản lý side effect | Nghe các sự kiện, vòng đời component | useEventListener('click') |
| Chia sẻ trạng thái | Chia sẻ trạng thái giữa nhiều component | useTheme(), useAuth() |
| Tích hợp bên thứ ba | Kết nối với thư viện bên ngoài | useWebSocket(url) |
Nguyên tắc thiết kế Custom Hook
| Nguyên tắc | Giải thích |
|---|---|
| Chỉ gọi Hook của React | Không gọi các hook như useState trong các hàm thông thường. |
| Giá trị trả về rõ ràng | Trả về trạng thái, hàm hoặc đối tượng để component dễ dàng sử dụng. |
| Tham số linh hoạt | Cho phép truyền vào các tùy chọn để tăng tính tái sử dụng. |
| Dọn dẹp side effect | Sử dụng hàm trả về trong useEffect để giải phóng tài nguyên. |
| Đặt tên thống nhất | Sử dụng tiền tố useXxx để dễ nhận biết và nhất quán. |
Ví dụ phức tạp: Hook quản lý modal form
Dưới đây là một ví dụ về một hook phức tạp hơn để quản lý một modal hiển thị form.
Triển khai hook useModalForm
import { useState, useReducer } from 'react';
import { Form, message } from 'antd';
export const useModalForm = (initialConfig = []) => {
const [form] = Form.useForm();
const [modalState, setModalState] = useState({
isOpen: false,
title: '',
fields: initialConfig,
});
const [submitHandler, setSubmitHandler] = useState(() => () => {});
const [callback, setCallback] = useState(() => () => {});
const [state, setState] = useReducer((prevState, params) => {
return { ...prevState, ...params };
}, {
isOpen: false,
title: '',
fields: [],
submitHandler: () => {},
callback: () => {},
defaultValues: {},
});
const openModal = (config = {}) => {
const {
title: modalTitle = 'Form',
defaultValues = {},
fields = initialConfig,
submitHandler: configHandler = () => {},
callback: configCallback = () => {},
} = config;
form.setFieldsValue(defaultValues);
setState({
isOpen: true,
title: modalTitle,
fields,
submitHandler: configHandler,
callback: configCallback,
});
};
const closeModal = () => {
setState({ isOpen: false });
form.resetFields();
if (callback) callback();
};
const handleSubmit = async () => {
try {
const values = await form.validateFields();
await submitHandler(values);
closeModal();
} catch (errorInfo) {
message.error('Vui lòng điền đầy đủ thông tin');
}
};
const renderFormItems = () => {
if (!state.fields.length) return null;
return state.fields.map(item => {
if (item?.hidden) return null;
return (
{item.type}
);
});
};
return {
isOpen: state.isOpen,
title: state.title,
form,
renderFormItems,
openModal,
closeModal,
handleSubmit,
setSubmitHandler,
setCallback,
setState,
};
};
Component sử dụng hook
import React from 'react';
import { Button, Modal } from 'antd';
import { useModalForm } from '../hooks/useModalForm';
const UserFormModal = () => {
const formConfig = [
{
label: 'Tên người dùng',
name: 'username',
require: true,
message: 'Vui lòng nhập tên người dùng',
type: <Input placeholder="Nhập tên" />,
},
{
label: 'Email',
name: 'email',
require: true,
message: 'Vui lòng nhập email',
type: <Input placeholder="Nhập email" />,
},
];
const {
isOpen,
title,
form,
renderFormItems,
openModal,
closeModal,
handleSubmit,
} = useModalForm(formConfig);
return (
<div>
<Button onClick={() => openModal({ title: 'Thêm người dùng' })}>
Mở form
</Button>
<Modal title={title} open={isOpen} onCancel={closeModal} onOk={handleSubmit}>
<form>
{renderFormItems()}
</form>
</Modal>
</div>
);
};
export default UserFormModal;