useState là hook giúp component ghi nhớ một thứ gì đó giữa các lần render: một con số đếm, đoạn chữ trong ô nhập, menu đang mở hay đóng. Mỗi lần bạn gọi setter của nó, React render lại component với giá trị mới.
Click nút, rồi đổi useState(0) thành useState(10) trong trình soạn thảo. Bản xem trước dựng lại và con số bắt đầu từ 10.
Cú pháp
const [value, setValue] = useState(initialValue);
initialValuechỉ được dùng ở lần render đầu tiên. Ở mọi lần render sau, React bỏ qua nó và trả lại giá trị đã lưu.valuelà giá trị hiện tại của lần render này.setValuethay thế giá trị và lên lịch một lần render mới.
Tên do bạn đặt. useState trả về một mảng hai phần tử và cặp ngoặc vuông tách nó ra, nên [isOpen, setIsOpen] và [email, setEmail] là cùng một mẫu.
Gọi useState ở cấp cao nhất của component, không bao giờ đặt trong vòng lặp, câu lệnh điều kiện hay hàm lồng nhau. React ghép mỗi lời gọi với giá trị đã lưu của nó theo thứ tự các lời gọi, nên thứ tự đó phải giống nhau ở mọi lần render (xem quy tắc của hook).
Nhiều phần state
Một component có thể gọi useState bao nhiêu lần tùy cần. Mỗi lời gọi là một giá trị độc lập.
State là một ảnh chụp
Setter không thay đổi biến bạn đang cầm trong tay. Nó cho React biết giá trị nên là gì ở lần render tiếp theo. Cho đến khi lần render đó diễn ra, lần render hiện tại vẫn thấy giá trị cũ.
Mở console bên dưới bản xem trước và click một lần. Handler ghi 0, giá trị cũ, rồi component ghi một lần render mới với 1. Không có gì sai cả: count là một hằng số trong suốt lần render đó, và lần render tiếp theo nhận một giá trị mới.
Hàm updater
Vì state là ảnh chụp, gọi setter ba lần với count + 1 chỉ cộng thêm một, không phải ba: lần gọi nào cũng tính 0 + 1. Hãy truyền một hàm thay vào đó, và React đưa cho mỗi lời gọi kết quả của lời gọi trước.
Dùng dạng updater mỗi khi giá trị tiếp theo phụ thuộc vào giá trị trước và cập nhật có thể không chạy thẳng từ lần render mới nhất: nhiều lần set trong một handler, một callback của setInterval, hoặc một effect.
Object và mảng trong state
React quyết định có render lại hay không bằng cách so sánh giá trị cũ và mới bằng Object.is. Nếu bạn sửa trực tiếp một object rồi truyền lại chính object đó, phép so sánh cho rằng không có gì thay đổi và màn hình giữ nguyên. Luôn truyền một object hay mảng mới.
{ ...user, age: user.age + 1 } sao chép mọi field và ghi đè một field. [...tags, 'code'] thêm một phần tử, còn filter xóa một phần tử, cả hai đều trả về mảng mới. Trang cập nhật mảng và object trình bày object lồng nhau, sửa một phần tử trong danh sách và chèn vào giữa.
State ban đầu lười
Đối số truyền cho useState được tính ở mỗi lần render, dù React chỉ dùng nó lần đầu tiên. Khi việc tính toán tốn kém (đọc localStorage, phân tích một chuỗi lớn), hãy truyền một hàm. React gọi nó một lần, ở lần render đầu tiên.
// Runs readSavedNotes() on every render, then throws the result away
const [notes, setNotes] = useState(readSavedNotes());
// Runs it once
const [notes, setNotes] = useState(readSavedNotes);
const [notes, setNotes] = useState(() => readSavedNotes());
Đặt lại state bằng key
State thuộc về vị trí của component trong cây. Nếu bạn render một key khác ở cùng chỗ, React coi đó là một component mới và state của nó bắt đầu lại từ đầu. Đây là cách gọn gàng nhất để đặt lại một form khi thứ nó đang sửa thay đổi.
Gõ một tin nhắn, rồi đổi người nhận: bản nháp bị xóa. Xóa key={to} rồi thử lại, đoạn chữ vẫn còn, vì giờ React thấy cùng một Draft ở cùng một chỗ.
Lỗi thường gặp
Ghi log state ngay sau khi set. Bạn sẽ thấy giá trị cũ, như ở trên. Hãy ghi log trong thân component, hoặc tính giá trị tiếp theo vào một biến trước rồi dùng biến đó.
Sửa trực tiếp state. user.age++; setUser(user) truyền vào cùng một object, nên không có gì được render. Hãy sao chép trước.
Gọi setter trong lúc render. <button onClick={setCount(count + 1)}> gọi setCount trong lúc render, việc này gây render lại, rồi lại gọi nó lần nữa. React chặn vòng lặp này bằng lỗi "Too many re-renders". Hãy truyền một hàm: onClick={() => setCount(count + 1)}.
Lưu những giá trị tính được. Nếu fullName luôn là first + ' ' + last, hãy tính nó trong lúc render thay vì giữ một phần state thứ ba có thể bị lệch.
Câu hỏi thường gặp
useState trả về gì?
Một mảng có đúng hai phần tử: giá trị hiện tại và một hàm để thay thế nó. Bạn tự đặt tên cho chúng bằng destructuring mảng, theo quy ước là [thing, setThing].
Vì sao state không cập nhật ngay lập tức?
Gọi setter không thay đổi biến bạn đang có. Nó yêu cầu React render lại component, và giá trị mới xuất hiện ở lần render đó. Bên trong event handler hiện tại, giá trị cũ vẫn nằm trong phạm vi, nên console.log(count) ngay sau setCount(count + 1) in ra con số cũ.
Khi nào nên truyền một hàm cho setter?
Khi giá trị mới phụ thuộc vào giá trị cũ và bạn set nó nhiều lần, hoặc set từ bên trong timer hay effect. setCount(c => c + 1) luôn bắt đầu từ giá trị mới nhất, còn setCount(count + 1) bắt đầu từ giá trị mà lần render này nhìn thấy.
useState có lưu được object hay mảng không?
Có, nhưng hãy coi nó là chỉ đọc. Tạo một object hay mảng mới rồi truyền nó cho setter (setUser({ ...user, name }), setItems([...items, item])). Sửa trực tiếp cái đang có sẽ không kích hoạt render.
useState và useReducer khác nhau thế nào?
Cả hai đều lưu state. useState hợp với vài giá trị độc lập. useReducer chuyển logic cập nhật vào một hàm nhận action, dễ đọc hơn khi nhiều sự kiện thay đổi cùng một state theo những cách liên quan đến nhau.