React Hooks là gì? Tại sao bạn cần học?
Bạn đã biết cách tạo Component trong React, hiểu Props và State cơ bản. Nhưng mỗi khi muốn dùng state trong Function Component, bạn lại thấy code như thế này:
const [count, setCount] = useState(0);
Đó chính là React Hook. Vậy Hook là gì, tại sao nó quan trọng và cách dùng đúng như thế nào? Bài viết này sẽ giải thích toàn bộ.
Trước khi có Hooks — Class Component phức tạp ra sao?
Trước React 16.8, để dùng state bạn buộc phải dùng Class Component:
// Cách cũ: Class Component
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 }; // phải dùng constructor
}
render() {
return (
<div>
<p>{this.state.count}</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
+1
</button>
</div>
);
}
}
Code dài, rườm rà, và this gây nhầm lẫn cho người mới. React Hooks ra đời để giải quyết vấn đề này — cho phép Function Component làm được mọi thứ Class Component làm được, với code ngắn hơn và dễ đọc hơn.
React Hooks là gì?
React Hooks là các hàm đặc biệt (bắt đầu bằng use) cho phép bạn sử dụng các tính năng của React bên trong Function Component.
Các Built-in Hooks quan trọng nhất:
- useState — quản lý state
- useEffect — xử lý side effects (gọi API, timer, event listener...)
- useContext — đọc giá trị từ Context
- useRef — truy cập DOM và lưu biến không trigger re-render
- useMemo / useCallback — tối ưu hiệu năng
Bài viết này tập trung vào useState và useEffect — hai hook bạn sẽ dùng hàng ngày trong mọi dự án React. Để nắm nền tảng, hãy xem lại React là gì trước.
useState — Quản lý state trong Function Component
Cú pháp cơ bản
const [state, setState] = useState(initialValue);
state— giá trị hiện tại của statesetState— hàm dùng để cập nhật state (gọi xong sẽ trigger re-render)initialValue— giá trị khởi tạo (chỉ dùng lần render đầu tiên)
Cú pháp const [a, b] = ... là array destructuring của JavaScript ES6. Bạn có thể đặt tên tùy ý, nhưng quy ước là [tên, setTên].
Ví dụ 1: Counter đơn giản
import { useState } from 'react';
function Counter() {
// Khởi tạo state count với giá trị ban đầu là 0
const [count, setCount] = useState(0);
return (
<div>
<p>Số lần click: <strong>{count}</strong></p>
{/* Gọi setCount để cập nhật state */}
<button onClick={() => setCount(count + 1)}>Tăng (+1)</button>
<button onClick={() => setCount(count - 1)}>Giảm (-1)</button>
<button onClick={() => setCount(0)}>Reset</button>
</div>
);
}
Mỗi khi bạn click nút, setCount được gọi → React re-render component → count hiển thị giá trị mới. Đơn giản như vậy!
Ví dụ 2: Form input
import { useState } from 'react';
function LoginForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
function handleSubmit(e) {
e.preventDefault(); // Ngăn trang reload
console.log('Đăng nhập với:', email, password);
}
return (
<form onSubmit={handleSubmit}>
<div>
<label>Email:</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="nhập email của bạn"
/>
</div>
<div>
<label>Mật khẩu:</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="nhập mật khẩu"
/>
</div>
<button type="submit">Đăng nhập</button>
</form>
);
}
Pattern này gọi là Controlled Component — React kiểm soát giá trị của input thông qua state. Mỗi khi người dùng gõ phím, onChange cập nhật state, React render lại và input hiển thị giá trị mới.
Cập nhật Object và Array — Lỗi thường gặp nhất
Khi state là Object hoặc Array, bạn không được mutate trực tiếp. Phải tạo giá trị mới:
// SAI — React không phát hiện thay đổi!
const [user, setUser] = useState({ name: 'Nguyễn Văn A', tuoi: 22 });
user.tuoi = 23; // Mutate trực tiếp — KHÔNG trigger re-render
// ĐÚNG — Dùng spread operator để tạo object mới
setUser(prev => ({ ...prev, tuoi: 23 }));
Tương tự với Array:
const [todos, setTodos] = useState(['Học React', 'Làm bài tập']);
// Thêm phần tử
setTodos(prev => [...prev, 'Học TypeScript']);
// Xóa phần tử (theo index)
setTodos(prev => prev.filter((_, i) => i !== 0));
// Cập nhật phần tử cụ thể
setTodos(prev => prev.map((item, i) =>
i === 1 ? 'Làm project thực tế' : item
));
Tại sao? React dùng Object.is để so sánh state cũ và mới. Nếu bạn mutate trực tiếp, reference không đổi → React nghĩ không có gì thay đổi → không re-render.
useEffect — Xử lý Side Effects
Side effect là gì?
Side effect là bất kỳ thao tác nào bên ngoài việc tính toán và render UI:
- Gọi API lấy dữ liệu
- Đặt timer (
setTimeout,setInterval) - Đăng ký event listener
- Thay đổi tiêu đề trang (
document.title) - Đọc/ghi localStorage
useEffect là nơi bạn đặt các side effects này — đảm bảo chúng chạy vào đúng thời điểm.
Cú pháp
useEffect(() => {
// Code side effect ở đây
return () => {
// Cleanup (tùy chọn) — chạy trước lần effect tiếp theo hoặc khi unmount
};
}, [dependencies]); // Mảng dependencies
3 Cách dùng dependency array
1. Không có dependency array — chạy sau MỌI lần render:
useEffect(() => {
console.log('Chạy sau mỗi lần render'); // Dùng rất ít, dễ gây bug
});
2. Array rỗng [] — chỉ chạy 1 lần khi component mount:
useEffect(() => {
console.log('Chỉ chạy 1 lần khi component được tạo ra');
}, []); // Tương đương componentDidMount trong Class Component
3. Có dependencies — chạy khi dependency thay đổi:
useEffect(() => {
console.log('userId vừa thay đổi:', userId);
// Fetch dữ liệu mới theo userId
}, [userId]); // Chỉ chạy khi userId thay đổi
Ví dụ: Đồng hồ đếm giây với Cleanup
import { useState, useEffect } from 'react';
function Stopwatch() {
const [seconds, setSeconds] = useState(0);
const [isRunning, setIsRunning] = useState(false);
useEffect(() => {
if (!isRunning) return; // Không chạy nếu đang dừng
// Khởi động timer
const interval = setInterval(() => {
setSeconds(prev => prev + 1); // Functional update tránh stale closure
}, 1000);
// Cleanup: dừng timer khi isRunning thay đổi hoặc component unmount
return () => clearInterval(interval);
}, [isRunning]);
return (
<div>
<p>Thời gian: {seconds} giây</p>
<button onClick={() => setIsRunning(true)}>Bắt đầu</button>
<button onClick={() => setIsRunning(false)}>Dừng</button>
<button onClick={() => { setIsRunning(false); setSeconds(0); }}>Reset</button>
</div>
);
}
Tại sao cần cleanup? Nếu không có return () => clearInterval(interval), khi component bị unmount, timer vẫn tiếp tục chạy ngầm. Điều này gây memory leak và lỗi khi cố gắng cập nhật state của component đã unmount.
Gọi API với useEffect
Đây là use case phổ biến nhất của useEffect. Kết hợp với Async/Await, bạn có thể fetch dữ liệu từ API một cách dễ dàng.
Pattern đầy đủ: Loading, Error, Data
import { useState, useEffect } from 'react';
function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false; // Cờ để tránh race condition
async function fetchUsers() {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/users');
if (!response.ok) {
throw new Error('Không thể tải dữ liệu');
}
const data = await response.json();
// Chỉ cập nhật state nếu component chưa unmount
if (!cancelled) {
setUsers(data);
}
} catch (err) {
if (!cancelled) {
setError(err.message);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
}
fetchUsers();
return () => { cancelled = true; }; // Cleanup
}, []);
if (loading) return <p>Đang tải...</p>;
if (error) return <p style={{ color: 'red' }}>Lỗi: {error}</p>;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name} — {user.email}</li>
))}
</ul>
);
}
Lưu ý: Không dùng async trực tiếp trong useEffect
// SAI — useEffect callback không được return Promise
useEffect(async () => {
const data = await fetch('/api/data'); // Lỗi!
}, []);
// ĐÚNG — Định nghĩa async function bên trong rồi gọi
useEffect(() => {
async function loadData() {
const response = await fetch('/api/data');
const data = await response.json();
setData(data);
}
loadData();
}, []);
Lý do: useEffect phải return một hàm cleanup hoặc undefined. Nếu dùng async, nó sẽ return một Promise — React sẽ báo lỗi.
Các Hooks quan trọng khác
useContext — Chia sẻ dữ liệu toàn cục
useContext cho phép đọc giá trị từ Context mà không cần truyền props qua nhiều cấp:
import { createContext, useContext, useState } from 'react';
// Tạo Context
const ThemeContext = createContext('light');
function App() {
const [theme, setTheme] = useState('dark');
return (
<ThemeContext.Provider value={theme}>
<button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
Đổi theme
</button>
<Header />
</ThemeContext.Provider>
);
}
// Component con sâu bên trong có thể đọc trực tiếp — không cần props drilling
function Header() {
const theme = useContext(ThemeContext);
return <header className={`header-${theme}`}>Header</header>;
}
useRef — Truy cập DOM và lưu biến
import { useRef } from 'react';
function SearchBar() {
const inputRef = useRef(null);
function handleSearch() {
inputRef.current.focus(); // Truy cập trực tiếp DOM element
console.log('Nội dung tìm kiếm:', inputRef.current.value);
}
return (
<div>
<input ref={inputRef} type="text" placeholder="Tìm kiếm..." />
<button onClick={handleSearch}>Tìm</button>
</div>
);
}
useRef khác useState ở điểm này: Thay đổi ref.current không trigger re-render. Dùng useRef khi bạn cần lưu giá trị giữa các render mà không muốn component render lại.
useMemo và useCallback — Tối ưu hiệu năng
// useMemo: cache kết quả tính toán nặng
const filteredList = useMemo(() => {
return bigList.filter(item => item.name.includes(searchText));
}, [bigList, searchText]); // Chỉ lọc lại khi bigList hoặc searchText thay đổi
// useCallback: cache function reference
const handleDelete = useCallback((id) => {
setItems(prev => prev.filter(item => item.id !== id));
}, []); // Function không thay đổi giữa các render
Lưu ý: Không nên lạm dụng useMemo và useCallback. Chỉ dùng khi có vấn đề hiệu năng thực sự, vì bản thân chúng cũng có chi phí bộ nhớ.
Custom Hook — Tái sử dụng Logic
Khi bạn thấy mình viết cùng một đoạn logic (useState + useEffect) ở nhiều component, hãy tách nó ra thành Custom Hook. Quy tắc: tên phải bắt đầu bằng use.
// Custom Hook: useFetch — tái sử dụng được ở bất kỳ đâu
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
fetch(url)
.then(res => {
if (!res.ok) throw new Error('Lỗi khi tải dữ liệu');
return res.json();
})
.then(data => { setData(data); setLoading(false); })
.catch(err => { setError(err.message); setLoading(false); });
}, [url]);
return { data, loading, error };
}
// Dùng trong nhiều component khác nhau
function Posts() {
const { data: posts, loading, error } = useFetch('/api/posts');
if (loading) return <p>Đang tải...</p>;
if (error) return <p>Lỗi: {error}</p>;
return <ul>{posts?.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
}
function UserProfile({ userId }) {
const { data: user, loading } = useFetch(`/api/users/${userId}`);
if (loading) return <p>Đang tải hồ sơ...</p>;
return <div>{user?.name}</div>;
}
Rules of Hooks — Quy tắc bắt buộc
React Hooks có 2 quy tắc tuyệt đối không được vi phạm:
Quy tắc 1: Chỉ gọi Hooks ở top level
// SAI — Hooks trong điều kiện
function MyComponent({ isLoggedIn }) {
if (isLoggedIn) {
const [user, setUser] = useState(null); // Vi phạm quy tắc!
}
}
// ĐÚNG — Luôn gọi ở đầu component, trước mọi điều kiện
function MyComponent({ isLoggedIn }) {
const [user, setUser] = useState(null); // Luôn gọi, bất kể điều kiện
if (!isLoggedIn) return <p>Vui lòng đăng nhập</p>;
return <div>Xin chào, {user?.name}</div>;
}
Quy tắc 2: Chỉ gọi Hooks trong React Function
- Trong Function Component — được phép
- Trong Custom Hook (hàm bắt đầu bằng
use) — được phép - Trong hàm JavaScript thường — không được
- Trong Class Component — không được
Tại sao có quy tắc này? React theo dõi thứ tự gọi Hooks giữa các lần render. Nếu thứ tự thay đổi (do điều kiện), React không biết state nào tương ứng với Hook nào — dẫn đến bug khó tìm.
Lỗi thường gặp và cách sửa
| Lỗi | Nguyên nhân | Cách sửa |
|---|---|---|
| useEffect chạy vô hạn (infinite loop) | Object/Array trong dependency array tạo mới mỗi render | Dùng useMemo để ổn định reference hoặc đặt object ra ngoài component |
| State không cập nhật ngay | setState là bất đồng bộ | Dùng useEffect để theo dõi state, hoặc functional update |
| Object/Array không trigger re-render | Mutate trực tiếp thay vì tạo giá trị mới | Dùng spread operator: {...obj, key: value} |
| useEffect dùng giá trị cũ (stale closure) | Closure capture biến tại thời điểm tạo | Dùng functional update: setCount(prev => prev + 1) |
| Lỗi khi dùng async trong useEffect | useEffect không được return Promise | Định nghĩa async function bên trong rồi gọi |
| Memory leak warning | Cập nhật state sau khi component unmount | Thêm cleanup với cờ cancelled |
useState vs useEffect — Tóm tắt
| useState | useEffect | |
|---|---|---|
| Dùng để | Lưu và cập nhật dữ liệu trong component | Thực hiện tác vụ ngoài render (API, timer...) |
| Trigger re-render | Có — khi setState được gọi | Không tự trigger, chạy SAU render |
| Cleanup | Không cần | Cần với timer, event listener, API request |
| Ví dụ dùng | Form input, counter, toggle, danh sách | Fetch API, setInterval, addEventListener |
Muốn tìm hiểu thêm về React? Xem React là gì và tại sao nên học để có cái nhìn tổng quan, hoặc Component trong React để hiểu nền tảng.
Câu hỏi thường gặp (FAQ)
Hooks có thay thế hoàn toàn Class Component không?
Về mặt kỹ thuật, Hooks cung cấp đầy đủ tính năng của Class Component. React không loại bỏ Class Component nhưng khuyến khích dùng Function Component + Hooks cho code mới. Hầu hết codebase hiện đại tại Việt Nam đã chuyển sang dùng Hooks.
Có thể dùng nhiều useState trong một component không?
Hoàn toàn được. Mỗi useState quản lý một state độc lập. Nên tách các state không liên quan ra riêng để dễ quản lý và debug.
useEffect có chạy trên server (SSR/Next.js) không?
Không. useEffect chỉ chạy trên browser (client-side). Khi dùng Next.js, logic trong useEffect không chạy khi server render — đây là điều quan trọng cần nhớ khi làm việc với React trong môi trường SSR.
Nên học tiếp gì sau useState và useEffect?
Sau khi thành thạo hai hook cơ bản này, hãy học tiếp: useReducer (quản lý state phức tạp), useContext (state toàn cục), React Router (điều hướng), và thư viện như React Query hoặc SWR để thay thế useEffect cho API calls một cách chuyên nghiệp hơn.