React Hooks là các hàm cho phép bạn sử dụng state, effect, context và các tính năng React khác trong component hàm, mà không cần viết class. Được giới thiệu trong React 16.8, hook đã thay đổi hoàn toàn cách tiếp cận phát triển, thay thế các pattern phức tạp như HOC và render props. Theo React, 2024, có 15 hook tích hợp sẵn bao phủ mọi tình huống quản lý component.
Điểm chính
React Hooks là một tập hợp các hàm được tích hợp trong React cho phép bạn “kết nối” vào các cơ chế nội bộ của framework mà không cần tạo class. Trước React 16.8, component hàm chỉ có thể nhận props và trả về JSX — bất kỳ logic phức tạp nào cũng yêu cầu một component class.
Vấn đề với component class là sự phân chia logic liên quan. Mã liên quan đến một thực thể (ví dụ, đăng ký WebSocket) bị chia cắt giữa componentDidMount, componentDidUpdate và componentWillUnmount. Hook giải quyết điều này: tất cả logic được nhóm trong một useEffect duy nhất.
Vấn đề thứ hai là tái sử dụng logic. Để tái sử dụng logic state giữa các component class, bạn phải áp dụng HOC (Component cấp cao) hoặc render props — cả hai pattern đều tạo ra sự lồng ghép thừa trong cây component. Hook tùy chỉnh giải quyết điều này mà không cần lồng ghép.
useState là hook cơ bản nhất. Nó nhận một giá trị khởi tạo và trả về một tuple: giá trị hiện tại và một hàm để cập nhật nó. Không giống this.setState trong class, useState không hợp nhất các đối tượng — state mới thay thế hoàn toàn state trước đó cho một lần gọi useState cụ thể.
import React, { useState } from 'react';
function Form() {
const [text, setText] = useState('');
const [submitting, setSubmitting] = useState(false);
async function handleSubmit() {
setSubmitting(true);
await fetch('/api/submit', {
method: 'POST',
body: JSON.stringify({ text })
});
setSubmitting(false);
}
return (
<div>
<input
value={text}
onChange={e => setText(e.target.value)}
/>
<button onClick={handleSubmit}
disabled={submitting}>
{submitting ? 'Saving...' : 'Submit'}
</button>
</div>
);
}
Setter của useState có thể nhận một hàm nhận giá trị trước đó và trả về giá trị mới. Dạng hàm là bắt buộc khi state mới được tính từ state trước đó và các lần gọi setState theo lô có thể xảy ra — mỗi lần gọi sẽ nhận được giá trị trước hiện tại.
useEffect là hook để thực hiện side effect trong component hàm. Nó nhận hai đối số: một hàm effect và một mảng phụ thuộc. React chạy effect sau mỗi lần render, nhưng khi có phụ thuộc được chỉ định — chỉ khi một trong số chúng thay đổi.
Một mảng phụ thuộc rỗng [] có nghĩa là effect chạy một lần sau lần render đầu tiên (tương tự componentDidMount). Trả về một hàm từ effect là một hàm dọn dẹp chạy khi unmount (tương tự componentWillUnmount) và trước khi chạy lại effect.
import React, { useState, useEffect } from 'react';
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const socket = new WebSocket(`wss://chat/${roomId}`);
socket.onmessage = (event) => {
setMessages(prev => [...prev, JSON.parse(event.data)]);
};
// Dọn dẹp - đóng socket
return () => socket.close();
}, [roomId]);
return <ul>
{messages.map((msg, i) =>
<li key={i}>{msg.text}</li>
)}
</ul>;
}
Các phụ thuộc của useEffect là kết nối duy nhất giữa effect và thế giới bên ngoài. Nếu effect sử dụng một biến từ props hoặc state, nó phải có trong mảng phụ thuộc. Bỏ qua quy tắc này là nguồn gốc của lỗi với stale closure.
useContext là hook để đọc giá trị context. Nó nhận một đối tượng context được tạo bởi React.createContext và trả về giá trị hiện tại. Nếu context thay đổi, component tự động render lại với giá trị mới.
Trước useContext, truy cập context trong component hàm yêu cầu bọc trong Context.Consumer, tạo ra lồng ghép. useContext loại bỏ hoàn toàn Consumer — chỉ cần gọi hook ở bất kỳ đâu trong component hàm.
import React, { createContext, useContext } from 'react';
const ThemeContext = createContext('sáng');
function ThemedButton() {
const theme = useContext(ThemeContext);
return <button
className={`btn btn-${theme}`}
>
{theme === 'dark' ? 'moon' : '☀️'}
</button>;
}
useContext không phải là sự thay thế cho Redux hay state toàn cục. React khuyến nghị sử dụng context cho các giá trị hiếm khi thay đổi — chủ đề, ngôn ngữ, trạng thái xác thực. Đối với dữ liệu thường xuyên cập nhật, mỗi lần render lại sẽ tính toán lại tất cả consumer context, điều này có thể làm giảm hiệu suất.
useReducer là một thay thế cho useState dành cho state có logic cập nhật phức tạp. Nó nhận một hàm reducer và state ban đầu, trả về state hiện tại và một hàm dispatch. Reducer nhận state hiện tại và action, trả về state mới — một hàm thuần túy không có side effect.
useReducer đặc biệt hữu ích khi nhiều action (set, reset, increment, addItem) quản lý một state duy nhất. Thay vì nhiều useState với các setter khác nhau — một reducer duy nhất với các loại action rõ ràng. Điều này làm cho logic có thể dự đoán được và dễ kiểm thử.
import React, { useReducer } from 'react';
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { ...state, count: state.count + 1 };
case 'setStep':
return { ...state, step: action.step };
case 'reset':
return { count: 0, step: 1 };
default:
throw new Error('Unknown action: ' + action.type);
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, {
count: 0,
step: 1
});
return (
<>
<p>Count: {state.count}</p>
<button
onClick={() => dispatch({ type: 'increment' })}>+</button>
<button
onClick={() => dispatch({ type: 'reset' })}>Reset</button>
</>
);
}
useRef tạo một đối tượng có thể thay đổi tồn tại giữa các lần render. Thay đổi current không kích hoạt re-render. Công dụng chính là truy cập phần tử DOM và lưu trữ các giá trị có thể thay đổi (timer, giá trị trước đó, instance thư viện).
useMemo ghi nhớ kết quả của một hàm. Nó chỉ tính toán lại giá trị khi một phụ thuộc thay đổi. Sử dụng nó cho các tính toán tốn kém — lọc mảng lớn, sắp xếp, định dạng dữ liệu. useCallback là một trường hợp đặc biệt của useMemo để ghi nhớ hàm.
import React, { useRef, useMemo, useCallback } from 'react';
function SearchPage({ items, query, onItemSelect }) {
const inputRef = useRef(null);
// Tự động lấy nét khi mount
useEffect(() => {
inputRef.current?.focus();
}, []);
// Ghi nhớ kết quả đã lọc
const filtered = useMemo(() => {
return items.filter(item =>
item.name.toLowerCase()
.includes(query.toLowerCase())
);
}, [items, query]);
// Ghi nhớ callback
const handleSelect = useCallback((id) => {
onItemSelect(id);
}, [onItemSelect]);
return (
<div>
<input ref={inputRef}
value={query}
onChange={e => setQuery(e.target.value)}
/>
<ul>
{filtered.map(item =>
<li key={item.id}>
<button
onClick={() => handleSelect(item.id)}>
{item.name}
</button>
</li>
)}
</ul>
</div>
);
}
Một quy tắc quan trọng: useRef, useMemo và useCallback là công cụ tối ưu, không phải cấu trúc bắt buộc. Đừng bọc mọi thứ trong useMemo — hãy đo hiệu suất trước. Tối ưu sớm làm phức tạp mã. React render component nhanh; useMemo chỉ cần thiết khi các tính toán thực sự tốn kém (O(n²) trở lên).
React đã giới thiệu hai quy tắc nghiêm ngặt cho hook. Thứ nhất: chỉ gọi hook ở cấp cao nhất — không gọi trong điều kiện, vòng lặp hoặc hàm lồng nhau. Thứ hai: chỉ gọi hook từ component hàm React hoặc hook tùy chỉnh. Plugin ESLint eslint-plugin-react-hooks tự động kiểm tra cả hai quy tắc.
Lỗi phổ biến nhất là vi phạm thứ tự gọi hook. React dựa vào thứ tự gọi hook giữa các lần render. Nếu một hook nằm trong điều kiện và không chạy ở một lần render, tất cả các hook tiếp theo sẽ bị dịch chuyển, gây ra lỗi.
| Lỗi | Chuyện gì xảy ra | Giải pháp |
|---|---|---|
| Hook trong điều kiện | Thứ tự hook bị phá vỡ | Di chuyển logic điều kiện vào trong hook |
| Stale closure | Effect sử dụng giá trị cũ | Thêm tất cả phụ thuộc vào mảng |
| Vòng lặp vô hạn | useEffect không phụ thuộc thay đổi state | Chỉ định phụ thuộc hoặc dùng useReducer |
| Thay đổi ref trong render | Side effect trong thân hàm | Di chuyển mutation vào useEffect |
Lỗi phổ biến thứ hai là stale closure trong useEffect. Nếu effect sử dụng một biến không được chỉ định trong phụ thuộc, effect sẽ “bắt giữ” giá trị của nó tại thời điểm tạo closure. Giải pháp là luôn bao gồm tất cả các biến được sử dụng trong mảng phụ thuộc. Nếu có quá nhiều phụ thuộc, hãy cân nhắc chia effect thành nhiều phần.
Câu hỏi thường gặp
React dựa vào thứ tự gọi hook giữa các lần render. Nếu một hook được gọi trong điều kiện và không chạy ở một lần render, tất cả các hook tiếp theo bị dịch chuyển — state của chúng bị xáo trộn. Quy tắc ESLint hooks/exhaustive-deps cảnh báo về điều này.
useLayoutEffect đồng bộ và chạy trước khi trình duyệt vẽ các thay đổi. useEffect bất đồng bộ — sau khi vẽ. Chỉ sử dụng useLayoutEffect khi bạn cần đo hoặc thay đổi DOM trước khi người dùng thấy thay đổi (vị trí popover, khôi phục cuộn).
Sử dụng AbortController trong useEffect. Tạo một controller, truyền signal vào fetch, và gọi controller.abort() trong hàm dọn dẹp. Đối với trình duyệt cũ, sử dụng cờ cancelled: return () => { cancelled = true; } với kiểm tra trước setState.
Hook tùy chỉnh là một hàm JavaScript với tiền tố use sử dụng các hook tích hợp sẵn. Tạo nó khi cùng một logic với hook lặp lại trong hai hoặc nhiều component. Ví dụ: useDebounce, useLocalStorage, useMediaQuery. Hook tùy chỉnh thay thế HOC và render props.
Trong Strict Mode, React cố ý mount và unmount component hai lần trong môi trường phát triển để tìm lỗi với các effect không được dọn dẹp. Nếu hàm dọn dẹp được viết đúng, việc gọi hai lần không gây ra vấn đề. Điều này không xảy ra trong chế độ production.
Tổng kết
Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay
IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.
Đọc thêm