Cả loạt OWL này ta luôn để framework lo DOM: đổi state, template render lại, xong. Đó là cách đúng cho 99% trường hợp. Nhưng 1% còn lại cần chạm thẳng vào node DOM thật: tự đặt con trỏ vào một ô khi mở trang, đo chiều rộng thật để tính toán, vẽ lên <canvas>, hay cắm một thư viện JS bên ngoài vào một <div>. Cho những lúc đó, OWL có useRef.

useRef + t-ref: cầu nối tới node thật

Cơ chế hai mảnh: gắn t-ref="tên" lên một phần tử trong template, và useRef("tên") trong setup() trả về một đối tượng ref. Thuộc tính .el của nó là node DOM thật. Nhưng có một điều sống còn về thời điểm: lúc setup() chạy, DOM chưa được dựng, nên .el là null. Node chỉ tồn tại sau khi component mount — tức trong onMounted trở đi:

Ảnh chụp đoạn mã OWL nền tối. Phần setup import useRef và onMounted từ owl, tạo this.oTim bằng useRef với tên oTim kèm chú thích ref trỏ tới node có t-ref oTim nhưng chưa có el lúc này, trong onMounted chú thích DOM đã dựng xong nên el là node DOM thật, gọi this.oTim.el.focus để đặt con trỏ vào ô và gán this.state.doRong bằng this.oTim.el.offsetWidth để đo px thật. Method chonHet gọi this.oTim.el.select để bôi đen toàn bộ chữ. Phần template gắn input t-ref oTim class form-control value VIP-0003 kèm chú thích el bằng null trước onMounted đừng chạm DOM trong setup

Hình 1: useRef("oTim") gắn với <input t-ref="oTim"/>. Trong setup() ref đã tạo nhưng .el còn null — chạm DOM ở đây sẽ nổ. Đợi tới onMounted (DOM đã dựng) mới gọi this.oTim.el.focus() và đọc offsetWidth. Nút "Chọn hết chữ" gọi .el.select() — lúc người dùng bấm thì DOM chắc chắn đã có.

Chạy thật: tự focus và đo px

Mình thêm một ô tìm kiếm vào dashboard: khi mở trang nó tự nhận con trỏ, và mình đo bề rộng thật của nó bằng offsetWidth:

Ảnh chụp thật bảng điều khiển Thẻ thành viên trên Odoo 19 với ba thẻ số liệu 4 385 và 1, phía dưới là một ô nhập có viền focus màu tím chứa chữ VIP-0003 đang được bôi đen nền xanh nhạt, bên cạnh là nút Chọn hết chữ, và một dòng chữ Ô tìm kiếm đã tự focus khi mở trang Bề rộng đo được useRef mũi tên offsetWidth in đậm 420 px

Hình 2: Thật cả. Ô tìm kiếm có viền focus (con trỏ tự vào khi mở trang, nhờ onMounted gọi .el.focus()). Chữ "VIP-0003" đang bôi đen vì mình đã bấm "Chọn hết chữ" (.el.select()). Và dòng cuối: "Bề rộng đo được ... 420 px" — con số offsetWidth đọc trực tiếp từ node DOM, không phải bịa. Đây là những việc state/props không làm được: chúng là đo và điều khiển DOM, không phải dữ liệu.

Khi nào dùng — và khi nào KHÔNG

Đây là chỗ dễ lạm dụng nhất của người mới. Quy tắc:

  • Dùng useRef khi bạn cần một thao tác DOM thô mà OWL không có API khai báo: focus(), select(), scrollIntoView(), đo offsetWidth/getBoundingClientRect(), vẽ canvas, khởi tạo thư viện ngoài (chart, map...).
  • ĐỪNG dùng useRef để đọc/ghi dữ liệu. Lấy giá trị ô input thì dùng state + t-model hay sự kiện, không đọc ref.el.value. Đổi màu, ẩn/hiện thì đổi state để template render lại, không sờ ref.el.style. Chạm DOM để đổi thứ mà state quản lý là đi ngược mô hình, và OWL sẽ ghi đè thay đổi của bạn ở lần render sau.

Nói ngắn: ref là để đo và điều khiển DOM, state/props là để quản lý dữ liệu. Lẫn hai thứ là nguồn của những bug "sửa rồi lại mất".

Vài lưu ý

  • .el là null trước onMounted và null sau onWillUnmount. Kiểm if (this.oTim.el) nếu có khả năng gọi lúc chưa mount.
  • t-ref nhận tên động: t-ref="item_#{id}" để trỏ tới phần tử trong vòng lặp — hữu ích khi cần scroll tới đúng dòng.
  • Ref không làm component render lại. Đổi qua .el không kích hoạt render; nếu cần UI phản ứng theo, đó là việc của state.

Ba ý mang về

  1. useRef("tên") + t-ref="tên" cho bạn node DOM thật qua .el — để làm những thao tác DOM thô mà OWL không có API: focus, select, đo offsetWidth, vẽ canvas, cắm thư viện ngoài.
  2. .el chỉ có sau khi mount — dùng trong onMounted trở đi, không phải trong setup() (lúc đó .el là null). Sau onWillUnmount nó lại về null.
  3. Ref để đo/điều khiển DOM, state/props để quản lý dữ liệu. Đừng dùng ref đọc giá trị input hay đổi style — đó là việc của state; lẫn lộn sinh bug "sửa rồi mất".

Trong dashboard ta tính tong, diem, vang ngay trong onWillStart rồi nhét vào state. Nhưng nếu một giá trị suy ra từ state khác thì sao — có nên lưu riêng không? Phần sau nói về getter thay cho "computed": cách OWL tính giá trị dẫn xuất gọn gàng mà không cần cơ chế computed riêng như Vue.