Bất biến ở đây nghĩa là không sửa object hay array đang nằm trong state, mà tạo bản mới rồi đưa vào setter. Lý do rất cụ thể: React quyết định có render lại hay không bằng Object.is trên giá trị cũ và mới — tức so tham chiếu, không so nội dung.
// Sai: cùng một mảng, React thấy không có gì đổi → không render lại
items.push(newItem)
setItems(items)
// Đúng: mảng mới, tham chiếu mới
setItems([...items, newItem])Điều nhiều người bỏ qua: đoạn sai ở trên vẫn đổi dữ liệu thật trong bộ nhớ. Màn hình chưa cập nhật, nhưng items đã có phần tử mới. Đến lần render sau vì lý do khác, phần tử đó bỗng xuất hiện — nên bug trông như "UI cập nhật trễ một nhịp", rất khó lần ra.
Với object lồng nhau, chỉ cần sao chép các cấp trên đường đi tới chỗ sửa:
setUser((prev) => ({ ...prev, address: { ...prev.address, city } }))Cùng nguyên tắc đó khiến mấy hàm mảng sau không dùng trực tiếp trên state: push, pop, splice, sort, reverse đều sửa tại chỗ. Muốn sắp xếp thì sao chép trước: [...items].sort(...).
Lợi ích thứ hai của bất biến là React.memo và useMemo hoạt động được: chúng cũng chỉ so tham chiếu. Sửa tại chỗ khiến nhánh con không render lại dù dữ liệu đã đổi.
Lưu ý: Props cũng vậy — đừng sửa object nhận từ cha. Nó thuộc về cha, và sửa lén ở con tạo ra bug mà nhìn vào component cha không thấy được nguyên nhân.