Bạn đã bao giờ tự hỏi làm thế nào các ứng dụng React có thể phản ứng linh hoạt với hành động của người dùng, thay đổi dữ liệu và cập nhật giao diện mà không cần tải lại trang chưa? Bí mật nằm ở việc quản lý trạng thái (state).
Trong thế giới React hiện đại, useState
là một trong những React Hooks quan trọng nhất, cho phép chúng ta thêm trạng thái vào các component hàm (functional components) một cách dễ dàng và hiệu quả. Trước đây, việc này chủ yếu dành cho các component lớp (class components), nhưng với sự ra đời của Hooks, useState
đã thay đổi hoàn toàn cách chúng ta xây dựng UI động và tương tác.
Bài viết này sẽ đưa bạn đi sâu vào useState
, từ cú pháp cơ bản đến các ví dụ thực tế và những lưu ý quan trọng khi sử dụng.
Bối Cảnh: State trong React là gì?
Trong React, state là một đối tượng JavaScript đơn giản được sử dụng để lưu trữ dữ liệu hoặc thông tin mà có thể thay đổi trong suốt vòng đời của một component. Khi state của một component thay đổi, React sẽ tự động re-render (kết xuất lại) component đó và các component con của nó để cập nhật giao diện người dùng, phản ánh những thay đổi mới nhất.
Hãy tưởng tượng một bộ đếm số, một form nhập liệu, hay một danh sách công việc. Dữ liệu như số đếm hiện tại, giá trị nhập vào, hoặc trạng thái hoàn thành của một công việc đều là các phần của state. Điều này khác với props (properties), vốn là dữ liệu được truyền từ component cha xuống component con và không thể thay đổi bên trong component nhận.
Sự Ra Đời của React Hooks và useState
Trước React 16.8, chỉ có các component lớp mới có thể có state và sử dụng các phương thức vòng đời (lifecycle methods) như componentDidMount
hay componentDidUpdate
. Tuy nhiên, class components thường đi kèm với một số thách thức:
- Sử dụng
this
khó hiểu và dễ gây nhầm lẫn, đặc biệt với những người mới học JavaScript. - Khó tái sử dụng logic có trạng thái giữa các component mà không cần các mẫu thiết kế phức tạp như Higher-Order Components (HOCs) hoặc Render Props.
- Các phương thức vòng đời có thể trở nên phức tạp và khó quản lý khi logic liên quan đến nhiều tác vụ khác nhau (ví dụ: fetching data và setting up event listeners) bị phân tán trong nhiều phương thức.
React Hooks được giới thiệu để giải quyết những vấn đề này, cho phép chúng ta sử dụng state và các tính năng khác của React mà không cần viết một class. useState
là Hook đầu tiên và cơ bản nhất, mở ra cánh cửa cho việc quản lý trạng thái trong các functional components.
useState Hoạt Động Như Thế Nào?
Cú Pháp Cơ Bản
Để sử dụng useState
, bạn cần import nó từ thư viện react
và gọi nó bên trong component hàm của bạn.
import React, { useState } from 'react';
function MyComponent() {
// Khai báo một biến trạng thái 'count' và hàm cập nhật 'setCount'
const [count, setCount] = useState(0);
// ... phần còn lại của component
}
Hãy phân tích cú pháp này:
useState(0)
: Đây là lời gọi Hook. Nó nhận một đối số duy nhất là giá trị khởi tạo (initial state) cho biến trạng thái của bạn. Trong ví dụ này,0
là giá trị khởi tạo chocount
.const [count, setCount]
: Đây là cú pháp phân tách mảng (array destructuring).useState
trả về một mảng gồm hai phần tử:count
: Biến trạng thái hiện tại. Bạn có thể đặt tên bất kỳ cho nó (ví dụ:name
,isVisible
).setCount
: Một hàm dùng để cập nhật biến trạng thái đó. Theo quy ước, tên hàm này thường làset
+ Tênbiếntrạng_thái (ví dụ:setName
,setIsVisible
). Khi bạn gọi hàm này với một giá trị mới, React sẽ re-render component với giá trị trạng thái mới.
Ví Dụ Thực Tế Đầu Tiên: Bộ Đếm Đơn Giản
Hãy xây dựng một bộ đếm số đơn giản để thấy useState
hoạt động như thế nào.
import React, { useState } from 'react';
function Counter() {
// Khai báo biến trạng thái 'count' với giá trị khởi tạo là 0
const [count, setCount] = useState(0);
// Hàm xử lý khi nút "Tăng" được nhấn
const increment = () => {
setCount(count + 1); // Cập nhật state: tăng giá trị của 'count' lên 1
};
// Hàm xử lý khi nút "Giảm" được nhấn
const decrement = () => {
setCount(count - 1); // Cập nhật state: giảm giá trị của 'count' xuống 1
};
return (
<div>
<h1>Bộ Đếm: {count}</h1>
<button onClick={increment}>Tăng</button>
<button onClick={decrement}>Giảm</button>
</div>
);
}
export default Counter;
Trong ví dụ trên:
- Chúng ta khai báo
count
với giá trị ban đầu là0
. - Khi nút “Tăng” được nhấn, hàm
increment
gọisetCount(count + 1)
. Điều này yêu cầu React cập nhậtcount
lên một giá trị mới. - React nhận thấy
count
đã thay đổi, nó sẽ re-render componentCounter
. - Giao diện người dùng sẽ hiển thị giá trị
count
mới nhất.
Các Điểm Quan Trọng Cần Nhớ Khi Sử Dụng useState
Giá Trị Khởi Tạo (Initial State)
Mọi Kiểu Dữ Liệu: Giá trị khởi tạo có thể là bất kỳ kiểu dữ liệu nào: số, chuỗi, boolean, đối tượng, hoặc mảng.
javascript
const [name, setName] = useState('Alice'); // Chuỗi
const [age, setAge] = useState(30); // Số
const [isActive, setIsActive] = useState(false); // Boolean
const [user, setUser] = useState({ id: 1, name: 'Bob' }); // Đối tượng
const [items, setItems] = useState([]); // MảngHàm Khởi Tạo “Lười” (Lazy Initial State): Nếu giá trị khởi tạo của bạn yêu cầu một phép tính phức tạp hoặc tốn kém về tài nguyên, bạn có thể truyền một hàm vào
useState
. Hàm này chỉ được gọi một lần duy nhất trong lần render đầu tiên của component, giúp tối ưu hiệu suất.function calculateInitialValue() {
console.log('Chỉ chạy một lần!'); // Hàm này chỉ được gọi một lần duy nhất
return Math.random() * 100;
}
function MyComponentWithLazyState() {
const [value, setValue] = useState(calculateInitialValue); // Truyền hàm, không phải kết quả của hàm
return <div>Giá trị: {value}</div>;
}
Cập Nhật State Bất Đồng Bộ (Asynchronous State Updates)
Một điều cực kỳ quan trọng cần nhớ là các hàm set
(như setCount
) thường cập nhật state một cách bất đồng bộ. Điều này có nghĩa là React có thể nhóm nhiều cập nhật state lại với nhau để tối ưu hiệu suất.
Do đó, nếu bạn cần cập nhật state dựa trên giá trị state trước đó, bạn không nên dựa vào giá trị hiện tại của biến state trực tiếp (ví dụ: count + 1
) vì nó có thể không phải là giá trị mới nhất nếu có nhiều cập nhật đang chờ xử lý.
Thay vào đó, hãy truyền một hàm callback vào hàm set
. Hàm callback này sẽ nhận giá trị state trước đó làm đối số đầu tiên và trả về giá trị state mới.
function CounterAsync() {
const [count, setCount] = useState(0);
const handleClick = () => {
// Sai lầm phổ biến: có thể không cho kết quả mong muốn nếu gọi nhiều lần liên tiếp
// setCount(count + 1);
// setCount(count + 1); // Sẽ chỉ tăng 1 đơn vị, không phải 2 (do count vẫn là giá trị cũ trong cùng một render cycle)
// Cách đúng: Sử dụng functional update để đảm bảo lấy giá trị state mới nhất
setCount(prevCount => prevCount + 1);
setCount(prevCount => prevCount + 1); // Sẽ tăng 2 đơn vị
};
return (
<div>
<h1>Bộ Đếm Bất Đồng Bộ: {count}</h1>
<button onClick={handleClick}>Tăng 2</button>
</div>
);
}
Cập Nhật Đối Tượng và Mảng
Khi state của bạn là một đối tượng hoặc mảng, bạn không thể trực tiếp thay đổi nó (mutation). React yêu cầu bạn phải tạo một bản sao mới của đối tượng hoặc mảng đó và sau đó cập nhật bản sao mới này.
Nếu bạn trực tiếp thay đổi state, React sẽ không nhận ra sự thay đổi và component sẽ không re-render.
Cập Nhật Đối Tượng
Sử dụng toán tử trải rộng (spread operator ...
) để tạo một bản sao của đối tượng hiện có và sau đó ghi đè (hoặc thêm) các thuộc tính bạn muốn thay đổi.
function UserProfile() {
const [user, setUser] = useState({ name: 'John Doe', age: 30, city: 'New York' });
const updateCity = () => {
setUser({
...user, // Sao chép tất cả các thuộc tính hiện có từ đối tượng 'user' cũ
city: 'London' // Ghi đè thuộc tính 'city' với giá trị mới
});
};
const updateAge = () => {
setUser(prevUser => ({
...prevUser, // Đảm bảo dùng prevUser để lấy giá trị mới nhất của đối tượng
age: prevUser.age + 1
}));
};
return (
<div>
<h2>Hồ Sơ Người Dùng</h2>
<p>Tên: {user.name}</p>
<p>Tuổi: {user.age}</p>
<p>Thành phố: {user.city}</p>
<button onClick={updateCity}>Chuyển đến London</button>
<button onClick={updateAge}>Ăn mừng sinh nhật</button>
</div>
);
}
Cập Nhật Mảng
Tương tự, khi cập nhật mảng, bạn cũng cần tạo một bản sao mới của mảng. Các phương thức như map
, filter
, slice
, hoặc toán tử trải rộng ...
rất hữu ích.
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: 'Học React Hooks', completed: false },
{ id: 2, text: 'Viết Blog về useState', completed: false },
]);
const addTodo = (text) => {
setTodos([
...todos, // Sao chép tất cả các todo hiện có
{ id: todos.length + 1, text, completed: false } // Thêm todo mới vào cuối mảng mới
]);
};
const toggleTodo = (id) => {
setTodos(
todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo // Tạo bản sao mới của todo cần thay đổi
)
);
};
return (
<div>
<h2>Danh Sách Công Việc</h2>
<ul>
{todos.map(todo => (
<li key={todo.id}
style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
onClick={() => toggleTodo(todo.id)}>
{todo.text}
</li>
))}
</ul>
<button onClick={() => addTodo('Tập thể dục')}>Thêm "Tập thể dục"</button>
</div>
);
}
Nhiều useState trong một Component
Bạn hoàn toàn có thể sử dụng nhiều lời gọi useState
trong một component để quản lý các phần trạng thái riêng biệt. Đây là một thực hành tốt vì nó giúp logic của bạn rõ ràng hơn và dễ quản lý hơn.
function UserForm() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [email, setEmail] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const handleSubmit = (e) => {
e.preventDefault();
setIsSubmitting(true);
// Xử lý gửi form (ví dụ: gửi dữ liệu lên API)
console.log({ firstName, lastName, email });
setTimeout(() => { // Giả lập quá trình gửi form
setIsSubmitting(false);
alert('Form đã được gửi!');
setFirstName('');
setLastName('');
setEmail('');
}, 1500);
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={firstName}
onChange={(e) => setFirstName(e.target.value)}
placeholder="Tên"
/>
<input
type="text"
value={lastName}
onChange={(e) => setLastName(e.target.value)}
placeholder="Họ"
/>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
/>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Đang gửi...' : 'Gửi'}
</button>
</form>
);
}
Việc sử dụng nhiều useState
cho các giá trị độc lập là cách làm được khuyến nghị, thay vì gộp tất cả vào một đối tượng lớn, giúp React tối ưu hóa việc re-render và dễ dàng hơn trong việc tách biệt các logic.
Một Số Sai Lầm Thường Gặp
- Thay đổi trực tiếp state (mutation): Ví dụ
user.age = 31; setUser(user);
hoặctodos.push(newTodo); setTodos(todos);
. Luôn luôn tạo bản sao mới của đối tượng/mảng khi cập nhật! - Quên import
useState
:import React, { useState } from 'react';
là bắt buộc. - Gọi
useState
bên trong điều kiện, vòng lặp hoặc hàm lồng nhau: React Hooks phải được gọi ở cấp cao nhất của component hàm. Đây là một quy tắc quan trọng của Hooks. - Không sử dụng functional update khi state mới phụ thuộc vào state cũ: Dẫn đến lỗi không mong muốn trong các trường hợp cập nhật nhanh hoặc nhiều lần liên tiếp.
Kết Luận
useState
là một trong những React Hooks quan trọng nhất, đã cách mạng hóa cách chúng ta quản lý trạng thái trong các component hàm. Với cú pháp đơn giản nhưng mạnh mẽ, nó cho phép bạn thêm tính tương tác và động lực vào ứng dụng React của mình một cách hiệu quả.
Hãy nhớ các quy tắc cơ bản:
- Khai báo với
const [state, setState] = useState(initialState);
. - Cập nhật state bằng hàm
setState
. - Không thay đổi trực tiếp các đối tượng hoặc mảng, hãy tạo bản sao mới.
- Sử dụng functional update khi state mới phụ thuộc vào state cũ.
- Gọi Hooks ở cấp cao nhất của component, không bên trong điều kiện hay vòng lặp.
Nắm vững useState
là bước đầu tiên và quan trọng nhất để trở thành một nhà phát triển React thành thạo. Hãy thực hành thường xuyên để làm quen với cách nó hoạt động và bạn sẽ thấy việc xây dựng các ứng dụng React phức tạp trở nên dễ dàng hơn rất nhiều.
Chúc bạn mã hóa vui vẻ!