useState: nó là gì, hook trạng thái và cú pháp trong React

Tác giả: IT Sectr Đã đăng: 2026-07-04 Thời gian đọc: 9 phút

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 — một Hook để quản lý trạng thái cục bộ trong các component chức năng React.
  • Bộ — trả về một cặp [giá trị, setter], trong đó setter là hàm cập nhật trạng thái.
  • Kết xuất lại — gọi setter luôn kích hoạt việc kết xuất lại component với giá trị mới.
  • Kiểu dữ liệu — useState hoạt động với số, chuỗi, mảng, đối tượng và giá trị boolean.
  • Tính bất biến — trạng thái không thể bị biến đổi trực tiếp, chỉ thông qua hàm setter.

useState trong React là gì

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.

jsx
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 useState hoạt động

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.

Cập nhật Trạng thái Hàm

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.

jsx
// 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 useState: Khai báo và Cập nhật

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ạngVí dụKhi nào sử dụng
Trực tiếpuseState(0)Giá trị ban đầu đơn giản
Lười biếnguseState(() => compute())Tính toán ban đầu tốn kém
Setter hàmsetState(prev => prev + 1)Cập nhật dựa trên giá trị trước đó
Setter trực tiếpsetState(newValue)Giá trị mới không phụ thuộc vào giá trị cũ
jsx
// 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 với các Kiểu dữ liệu Khác nhau

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.

jsx
// ✅ Đú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]);

Quy tắc Sử dụng Hooks với useState

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.

  • Không trong điều kiện — if (flag) { useState(0) } vi phạm thứ tự gọi Hook.
  • Không trong vòng lặp — for (...) { useState(0) } làm cho thứ tự không thể đoán trước.
  • Không trong callback — useEffect(() => { useState(0) }, []) phá vỡ danh sách liên kết.
jsx
// ❌ 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 Thường gặp với useState

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.

  • Biến đổi đối tượng — user.name = ‘Mới’ thay vì setUser({...user, name: ‘Mới’}).
  • Bao đóng cũ — sử dụng giá trị lỗi thời trong setter mà không có dạng hàm.
  • Kết xuất lại thừa — gọi setter với cùng giá trị (React 18 vẫn thực hiện kết xuất cho kiểu nguyên thủy).
  • Đặt lại trạng thái — vô tình cập nhật prop key của component, khiến nó được gắn kết lại.

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.

jsx
// ❌ 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ó thể sử dụng useState cho các đối tượng lồng nhau phức tạp không?

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ộ hay đồng bộ?

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.

Điều gì xảy ra nếu useState được gọi bên ngoài component?

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.

Khi nào sử dụng useState và khi nào sử dụng useReducer?

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.

Tại sao useState không cập nhật giá trị ngay lập tức?

Đ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

  • useState — một Hook cơ bản của React để quản lý trạng thái cục bộ trong các component chức năng, trả về [state, setter].
  • Tính bất biến — trạng thái không thể bị biến đổi trực tiếp; luôn truyền giá trị mới hoặc sử dụng dạng hàm của setter.
  • Khởi tạo lười biếng — cho các tính toán ban đầu tốn kém, hãy truyền một hàm: useState(() => compute()).
  • Setter hàm — setState(prev => prev + 1) đảm bảo tính chính xác với các bản cập nhật được gộp lô.
  • Gộp lô — React gộp nhiều lần gọi setter thành một lần kết xuất lại duy nhất để tối ưu hóa.
  • Quy tắc Hook — chỉ gọi useState ở cấp cao nhất của component, không trong điều kiện hoặc vòng lặp.
  • Kiểu dữ liệu — đối với đối tượng và mảng, luôn tạo tham chiếu mới khi cập nhậ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.

Thảo luận dự án

Đọc thêm