Đây là lỗi kinh điển của người mới OWL: viết this.count++ trong một handler, bấm nút, và... số trên màn hình đứng im. Code chạy đúng, biến có tăng, nhưng giao diện không biết. Điều làm OWL "sống" — dữ liệu đổi thì màn hình tự vẽ lại — không tự nhiên có; nó đến từ một thứ: useState. Bài này đặt hai bộ đếm cạnh nhau, một dùng useState một không, rồi bấm thật để thấy khác biệt.

Một reactive, một thường

Trong setup, tôi tạo hai bộ đếm: state.dem (qua useState) và demThuong (thuộc tính thường), cùng hai nút tăng chúng:

Ảnh chụp đoạn mã nền tối. Phần setup this state bằng useState với dem 0 chú thích PROXY phản ứng, this demThuong bằng 0 chú thích thuộc tính thường; method tangPhanUng this state dem cộng cộng chú thích đổi state tự render lại, method tangThuong this demThuong cộng cộng chú thích đổi thường không render. Phần template span t-esc state dem chú thích đọc state cập nhật ngay, span t-esc demThuong chú thích đọc thuộc tính thường, button t-on-click tangPhanUng state dem cộng cộng, button t-on-click tangThuong demThuong cộng cộng. Phần cuối chú thích đo thật bấm demThuong 3 lần UI vẫn 0 không render rồi bấm state dem 1 lần render state dem bằng 1 và demThuong bằng 3 giá trị tích luỹ mới lộ ra chỉ thay đổi state qua useState mới khiến giao diện tự vẽ lại

Hình 1: Hai bộ đếm. this.state = useState({..., dem: 0}) — useState bọc object trong một proxy theo dõi mọi truy cập/thay đổi. this.demThuong = 0 là thuộc tính JavaScript thường, không được theo dõi. tangPhanUng tăng state.dem; tangThuong tăng demThuong. t-on-click gắn handler vào nút.

Đo: bấm nút thường, màn hình đứng im

Bấm nút demThuong++ ba lần. Biến demThuong trong bộ nhớ đã thành 3, nhưng màn hình:

Ảnh chụp giao diện demo useState phản ứng, dòng state dem useState hiện badge xanh số 0, dòng demThuong thường hiện badge xám số 0, hai nút state dem cộng cộng render lại màu xanh và demThuong cộng cộng không render màu xám, đây là ảnh sau khi đã bấm nút demThuong ba lần nhưng cả hai số vẫn hiển thị 0

Hình 2: Sau khi bấm demThuong++ ba lần — cả hai vẫn hiện 0. Biến demThuong đã là 3 trong bộ nhớ, nhưng vì nó không đi qua useState, OWL không biết có gì đổi nên không vẽ lại. Màn hình đứng im dù dữ liệu đã thay đổi. Đây chính là cái bẫy "số không nhúc nhích".

Bấm nút reactive: mọi thứ hiện ra

Giờ bấm state.dem++ một lần. Nó thay đổi state (được theo dõi) → OWL vẽ lại toàn component → và lần vẽ lại này lộ ra luôn giá trị demThuong đã tích luỹ:

Ảnh chụp giao diện demo useState phản ứng sau khi bấm nút state dem một lần, dòng state dem useState hiện badge xanh số 1, dòng demThuong thường hiện badge xám số 3, hai nút giữ nguyên; số 3 của demThuong bây giờ mới lộ ra vì việc render đã bị kích hoạt bởi thay đổi state

Hình 3: Sau một lần bấm state.dem++ — state.dem thành 1, và demThuong bất ngờ nhảy lên 3. Con số 3 đó đã có từ trước (ba lần bấm trước), chỉ là bây giờ mới lộ ra vì việc render bị kích hoạt bởi thay đổi state.dem. Bài học rõ ràng: chỉ thay đổi qua useState mới khiến giao diện tự vẽ lại; thuộc tính thường có đổi cũng phải chờ một render do thứ khác gây ra mới hiện.

Vài lưu ý về reactivity

  • Luôn để dữ liệu-cần-hiển-thị trong state. Nếu một giá trị sẽ đổi và cần cập nhật UI, nó phải nằm trong object useState. Thuộc tính thường chỉ hợp cho thứ không ảnh hưởng giao diện (một tham chiếu service, một bộ đếm nội bộ).
  • Proxy theo dõi sâu. useState reactive cả với object/mảng lồng nhau: state.cards.push(x) hay state.user.name = "..." đều kích hoạt render. Không cần "set" đặc biệt như vài framework khác.
  • Fine-grained: chỉ render khi thứ ĐANG hiển thị đổi. OWL chỉ vẽ lại nếu thuộc tính bị thay đổi có được đọc trong lần render trước. Đổi một field trong state mà template không dùng thì không tốn công vẽ lại.
  • Đừng thay cả object state. Gán this.state = {...} mới là mất proxy. Sửa bên trong: this.state.x = ..., không gán lại chính this.state.

Ba ý mang về

  1. useState(obj) trả về một proxy phản ứng: thay đổi this.state.x khiến OWL tự vẽ lại component; thay đổi this.x (thuộc tính thường) thì không.
  2. Đo thật thấy rõ: bấm nút tăng thuộc tính thường 3 lần, UI vẫn 0; chỉ khi một thay đổi state kích hoạt render, giá trị tích luỹ mới lộ ra.
  3. Để mọi dữ liệu-hiển-thị trong state; proxy theo dõi cả object/mảng lồng; và đừng gán lại cả this.state kẻo mất tính phản ứng.

Một component tự quản state của mình là đủ cho widget nhỏ. Nhưng ứng dụng thật gồm nhiều component lồng nhau, và cha cần đưa dữ liệu xuống con. Phần sau đi vào props — truyền dữ liệu xuống component con: khai báo, kiểu, và vì sao con không nên tự sửa props.