useState là một Hook tích hợp trong React cho phép các component chức năng quản lý trạng thái cục bộ mà không cần chuyển đổi thành component lớp. React 16.8 đã giới thiệu Hooks, và useState trở thành Hook được sử dụng rộng rãi nhất trong số đó, thay thế this.state và this.setState từ cách tiếp cận dựa trên lớp. Theo Tài liệu React (2025), hơn 80% component trong các ứng dụng React hiện đại sử dụng useState để quản lý dữ liệu biểu mẫu, cờ giao diện và bộ đếm. Hook trả về một bộ gồm giá trị hiện tại và một hàm setter để cập nhật trạng thái và kích hoạt việc kết xuất lại component.
Những điểm chính
useState là một Hook cơ bản của React, được thêm vào phiên bản 16.8, cho phép các component chức năng lưu trữ và thay đổi trạng thái cục bộ. Trước khi Hooks được giới thiệu, trạng thái chỉ có thể được sử dụng trong các component lớp thông qua this.state và this.setState, khiến các component chức năng chỉ mang tính trình bày. useState đã loại bỏ hạn chế này, cho phép viết toàn bộ ứng dụng bằng các component chức năng.
Hook nhận một đối số — giá trị trạng thái ban đầu — và trả về một mảng gồm hai phần tử. Phần tử đầu tiên là giá trị trạng thái hiện tại và phần tử thứ hai là hàm để cập nhật nó. React đảm bảo rằng hàm setter ổn định và không thay đổi giữa các lần kết xuất, cho phép bạn truyền nó một cách an toàn cho các component con và sử dụng nó trong các bao đóng.
Theo Khảo sát Sử dụng React DevTools (2024), useState được sử dụng trong 96% ứng dụng React, trở thành Hook phổ biến nhất trong hệ sinh thái. Ngay cả trong các ứng dụng sử dụng trạng thái toàn cục qua Redux hoặc Zustand, trạng thái cục bộ qua useState vẫn là cơ chế chính để quản lý trạng thái giao diện người dùng — modal mở, giá trị trường nhập liệu, tab đang hoạt động.
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}
Cơ chế bên trong của useState dựa trên hệ thống nút fiber của React. Mỗi component trong React được đại diện bởi một nút fiber lưu trữ một danh sách liên kết các Hooks liên quan đến nó. Khi một component gọi useState, React tạo một nút mới trong danh sách này và lưu trữ giá trị trạng thái hiện tại cùng với tham chiếu đến hàng đợi cập nhật trong đó.
Khi hàm setter được gọi, React không cập nhật trạng thái ngay lập tức. Thay vào đó, nó đặt bản cập nhật vào hàng đợi trạng thái đang chờ xử lý, lên lịch kết xuất lại component và chỉ trong lần kết xuất tiếp theo mới tính toán trạng thái mới dựa trên trạng thái cũ và các bản cập nhật đã được áp dụng. Điều này đảm bảo việc gộp lô — nếu bạn gọi setter ba lần trong một trình xử lý sự kiện, React sẽ gộp chúng thành một lần kết xuất lại duy nhất.
Theo Nhóm React — Nhóm Công tác React 18 (2024), tính năng gộp lô tự động đã được mở rộng trong React 18: các bản cập nhật trạng thái hiện được gộp không chỉ trong trình xử lý sự kiện mà còn trong setTimeout, callback Promise và trình xử lý sự kiện gốc. Điều này mang lại hiệu suất tăng lên đến 30% trong các tình huống có nhiều bản cập nhật trạng thái.
Khi trạng thái mới phụ thuộc vào trạng thái trước đó, hãy sử dụng dạng hàm của setter: setCount(prev => prev + 1). React truyền giá trị trạng thái thực tế tại thời điểm cập nhật vào hàm, đảm bảo tính chính xác ngay cả với các bản cập nhật được gộp lô. Nếu không có dạng hàm, setCount(count + 1) có thể sử dụng giá trị cũ nếu được gọi nhiều lần liên tiếp.
// Khởi tạo lười biếng — chỉ chạy một lần
setCount(prev => prev + 1);
setCount(prev => prev + 1);
setCount(prev => prev + 1);
// Result: count increased by 3
// NOT batch-safe: stale closure
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
// Result: count increased by 1 (stale closure)
Cú pháp cơ bản của useState cực kỳ ngắn gọn: const [state, setState] = useState(initialValue). Giá trị ban đầu chỉ được sử dụng trong lần kết xuất đầu tiên; trong các lần kết xuất tiếp theo, React bỏ qua nó và trả về giá trị đã lưu hiện tại. Nếu tính toán ban đầu yêu cầu các thao tác tốn kém, hãy truyền một hàm khởi tạo: useState(() => computeExpensiveInitial()).
Khởi tạo lười biếng đặc biệt quan trọng khi giá trị ban đầu đến từ localStorage, biến đổi dữ liệu phức tạp hoặc phân tích tham số URL. React chỉ gọi hàm khởi tạo một lần — khi component được gắn kết, giúp tiết kiệm tài nguyên trong các lần kết xuất tiếp theo. Nếu không có khởi tạo lười biếng, biểu thức tốn kém sẽ được đánh giá mỗi lần kết xuất, ngay cả khi kết quả của nó bị bỏ qua.
| Dạng | Ví dụ | Khi nào sử dụng |
|---|---|---|
| Trực tiếp | useState(0) | Giá trị ban đầu đơn giản |
| Lười biếng | useState(() => compute()) | Tính toán ban đầu tốn kém |
| Setter hàm | setState(prev => prev + 1) | Cập nhật dựa trên giá trị trước đó |
| Setter trực tiếp | setState(newValue) | Giá trị mới không phụ thuộc vào giá trị cũ |
// Trạng thái đối tượng — tạo tham chiếu mới
const [user, setUser] = useState(() => {
const saved = localStorage.getItem('user');
return saved ? JSON.parse(saved) : null;
});
useState hoạt động tốt như nhau với các kiểu nguyên thủy (số, chuỗi, boolean) và kiểu tham chiếu (mảng, đối tượng). Tuy nhiên, có một sự khác biệt quan trọng: đối với kiểu nguyên thủy, React phát hiện thay đổi theo giá trị; đối với đối tượng và mảng, theo tham chiếu. Biến đổi một đối tượng hiện có mà không tạo đối tượng mới sẽ không kích hoạt kết xuất lại vì tham chiếu vẫn giữ nguyên.
Khi làm việc với đối tượng và mảng, hãy luôn tạo một bản sao mới với dữ liệu đã được sửa đổi. Đối với đối tượng, sử dụng cú pháp spread: setUser(prev => ({...prev, name: newName})). Đối với mảng, sử dụng các phương thức trả về mảng mới: filter, map, concat, hoặc cú pháp spread để thêm phần tử. Các phương thức biến đổi như push, pop, splice sẽ không hoạt động — React sẽ bỏ qua chúng vì tham chiếu mảng không thay đổi.
// ✅ Đúng: hooks ở cấp cao nhất
const [form, setForm] = useState({ name: '', email: '' });
const updateField = (field, value) =>
setForm(prev => ({ ...prev, [field]: value }));
// ...
const [items, setItems] = useState([]);
const addItem = item =>
setItems(prev => [item, ...prev]);
Giống như tất cả các Hooks React, useState tuân theo hai quy tắc. Thứ nhất: chỉ gọi Hooks ở cấp cao nhất của component — không đặt chúng bên trong các điều kiện, vòng lặp hoặc hàm lồng nhau. Điều này đảm bảo rằng Hooks được gọi theo cùng một thứ tự trong mỗi lần kết xuất, điều này rất quan trọng đối với danh sách liên kết nội bộ của React.
Quy tắc thứ hai: chỉ gọi Hooks từ các component chức năng React hoặc Hooks tùy chỉnh. Không gọi useState từ các hàm thông thường, callback hoặc component lớp. Vi phạm các quy tắc này dẫn đến lỗi Invalid hook call mà React phát hiện tại thời điểm chạy.
// ❌ Vấn đề bao đóng cũ
function GoodComponent() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
// ✅ Đã sửa bằng cập nhật hàm
}
// count đã cũ!
function BadComponent() {
if (isLogged) {
const [user, setUser] = useState(null);
}
}
Lỗi phổ biến nhất là biến đổi trực tiếp đối tượng và mảng thay vì tạo tham chiếu mới. Các nhà phát triển đã quen với phong cách có thể biến đổi từ các component lớp, nơi this.state.user.name = ‘Tên mới’ hoạt động (mặc dù nó cũng không được khuyến nghị). Trong các component chức năng, sự biến đổi như vậy đơn giản bị bỏ qua: React không thấy sự thay đổi tham chiếu và không kết xuất lại component.
Vấn đề bao đóng cũ xảy ra khi một callback được truyền cho useEffect hoặc trình xử lý sự kiện captures một giá trị trạng thái lỗi thời từ bao đóng tại thời điểm callback được tạo. Giải pháp là sử dụng dạng hàm của setter hoặc bao gồm các giá trị cần thiết trong mảng phụ thuộc của useEffect. Theo Tài liệu React — Câu hỏi Thường gặp về Hooks (2025), bao đóng cũ là nguyên nhân phổ biến thứ hai gây ra lỗi trong Hooks sau mảng phụ thuộc không chính xác.
// ❌ Stale closure problem
useEffect(() => {
const timer = setInterval(() => {
setCount(count + 1); // count is stale!
}, 1000);
return () => clearInterval(timer);
}, []);
// ✅ Fixed with functional update
useEffect(() => {
const timer = setInterval(() => {
setCount(prev => prev + 1);
}, 1000);
return () => clearInterval(timer);
}, []);
Các câu hỏi thường gặp
Có, nhưng cần thận trọng. Đối với các đối tượng lồng nhau sâu, mỗi cấp đều yêu cầu sao chép khi cập nhật: setState(prev => ({...prev, nested: {...prev.nested, key: newValue}})). Nếu bạn có các đối tượng với ba cấp lồng nhau trở lên, hãy cân nhắc Immer (một thư viện cho các bản cập nhật bất biến) hoặc chia trạng thái thành nhiều lần gọi useState cho các cấp khác nhau.
useState là bất đồng bộ theo nghĩa là gọi setter không cập nhật giá trị ngay lập tức. React đặt bản cập nhật vào hàng đợi và thực thi nó trong lần kết xuất tiếp theo. Sau setCount(newValue), biến count trong hàm hiện tại vẫn chứa giá trị cũ. Giá trị mới sẽ chỉ khả dụng ở lần gọi component tiếp theo. Cơ chế gộp lô đảm bảo rằng nhiều lần gọi được gộp vào một lần kết xuất duy nhất.
React sẽ đưa ra lỗi Invalid hook call. Hooks chỉ có thể được gọi bên trong các component chức năng React hoặc Hooks tùy chỉnh (các hàm có tên bắt đầu bằng use). Hạn chế này được xây dựng trong cách triển khai Hook: chúng sử dụng cây fiber của component để lưu trữ trạng thái và bên ngoài component, cây này không có sẵn.
useState phù hợp cho các trạng thái độc lập đơn giản (bộ đếm, trường nhập liệu, cờ). useReducer tốt hơn khi trạng thái là một đối tượng phức tạp với nhiều trường, logic cập nhật phụ thuộc vào loại hành động hoặc trạng thái tiếp theo yêu cầu biến đổi phức tạp. Một quy tắc thực tế: nếu bạn có nhiều hơn ba trường trạng thái liên quan đến nhau, hãy sử dụng useReducer.
Điều này được thực hiện vì lý do hiệu suất. Nếu useState cập nhật trạng thái đồng bộ, mỗi lần gọi setter sẽ kích hoạt kết xuất lại ngay lập tức, dẫn đến nhiều lần vẽ lại trong một trình xử lý sự kiện duy nhất. Gộp lô, nơi React gộp các bản cập nhật và thực hiện một lần kết xuất lại duy nhất, là một tối ưu hóa được giới thiệu trong React 16 và được cải thiện đáng kể trong React 18 với tính năng gộp lô tự động.
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