Bạn viết this.myBox.el.focus() trong setup, chạy thử, và nhận TypeError: Cannot read property 'focus' of null. DOM chưa có ở đó. Một component OWL không "sinh ra" tức thì — nó đi qua một vòng đời: khởi tạo, nạp dữ liệu, dựng DOM, gắn vào trang. Mỗi việc phải làm đúng chặng của nó. Bài này đo thật thứ tự các chặng và chỉ ra: nạp dữ liệu ở đâu, chạm DOM ở đâu.

Bốn chặng, hai hook chính

onWillStart để nạp dữ liệu, onMounted để chạm DOM — vì hai việc khả dụng ở hai thời điểm khác nhau:

Ảnh chụp đoạn mã nền tối, import Component useState onWillStart onMounted useRef từ @odoo/owl. Trong setup chú thích 1 setup khởi tạo chưa có DOM, gán this orm useService orm, this hopRef useRef hop chú thích trỏ tới div t-ref hop. Gọi onWillStart async chú thích 2 onWillStart nạp dữ liệu trước lần render đầu await được, gán this state cards await this orm searchRead. Gọi onMounted chú thích 4 onMounted DOM đã gắn vào trang chạm được node thật, gán this hopRef el textContent bằng onMounted đã ghi vào DOM, chú thích nơi khởi tạo thư viện JS ngoài biểu đồ focus ô nhập. Cuối chú thích thứ tự setup rồi onWillStart rồi render rồi onMounted, dọn dẹp khi gỡ onWillUnmount clearInterval

Hình 1: setup() chạy đầu tiên — khởi tạo state, service, useRef, nhưng chưa có DOM. onWillStart(async ...) chạy tiếp, là nơi await nạp dữ liệu trước lần render đầu (component không hiện cho tới khi nó xong). Sau khi OWL render và gắn DOM vào trang, onMounted chạy — lúc này this.hopRef.el mới là node thật, chạm được. Còn onWillUnmount để dọn dẹp (huỷ timer, gỡ listener) khi component bị gỡ.

Đo thật: thứ tự qua Console

Tôi đặt một console.log ở mỗi hook rồi mở dashboard, bắt log bằng trình duyệt tự động. Thứ tự in ra không đổi:

HOOK 1 · setup (khởi tạo, chưa có DOM)
HOOK 2 · onWillStart (nạp dữ liệu, TRƯỚC render)
HOOK 4 · onMounted (DOM đã gắn -> chạm được)

Ảnh sơ đồ dòng thời gian vòng đời một component OWL trên nền kem, bốn bước nối bằng mũi tên. Bước 1 setup khởi tạo state service useRef chưa có DOM. Bước 2 onWillStart nạp dữ liệu await trước render đầu. Bước 3 render OWL dựng DOM từ template. Bước 4 onMounted DOM đã gắn chạm node khởi tạo lib ngoài. Dưới ghi Console in đúng thứ tự HOOK 1 setup rồi HOOK 2 onWillStart rồi HOOK 4 onMounted, về sau onWillUpdateProps khi props đổi onWillUnmount khi gỡ dọn timer listener, quy tắc đọc dữ liệu ở onWillStart đụng vào DOM hoặc thư viện ngoài ở onMounted vì trước đó node chưa tồn tại

Hình 2: Dòng thời gian. setup → onWillStart → render (OWL dựng DOM) → onMounted. Con số trong Console (1, 2, 4) khớp đúng — chặng 3 là render, không phải hook để bạn viết code. Về sau còn onWillUpdateProps (khi props đổi) và onWillUnmount (khi gỡ, để dọn dẹp).

Bằng chứng onMounted chạm được DOM

Trong onMounted, tôi ghi đè nội dung một node qua useRef. Nếu hook này chạy trước khi DOM tồn tại, ref sẽ null và không có gì đổi. Nhưng nó chạy sau render, nên node có thật:

Ảnh chụp một khối trên dashboard nền kem, dòng tiêu đề Demo hook onMounted chạm DOM màu nâu, bên dưới dòng chữ onMounted đã ghi vào DOM sau khi render xong, thay cho văn bản gốc mà template vẽ ra

Hình 3: Template ban đầu vẽ ra một dòng giữ chỗ, nhưng onMounted đã ghi đè nó thành "onMounted đã ghi vào DOM sau khi render xong." Việc này chỉ thành công vì this.hopRef.el trỏ tới một node có thật — bằng chứng onMounted chạy sau khi DOM đã gắn. Thử cùng đoạn code đó trong setup là nhận null.

Quy tắc dùng hook

  • Nạp dữ liệu ở onWillStart. Nó await được và chặn render cho tới khi xong, nên component hiện ra đã có sẵn dữ liệu — không nhấp nháy "trống rồi mới đầy".
  • Chạm DOM / khởi tạo thư viện ngoài ở onMounted. Vẽ biểu đồ (Chart.js), focus ô nhập, đo kích thước element — tất cả cần node thật, chỉ có từ onMounted.
  • Lấy node bằng useRef + t-ref. useRef("hop") trong setup, t-ref="hop" trong template; .el chỉ có giá trị từ onMounted trở đi.
  • Dọn dẹp ở onWillUnmount. Đặt setInterval, thêm listener toàn cục, mở kết nối? Huỷ chúng ở đây, kẻo rò rỉ khi component bị gỡ.

Ba ý mang về

  1. Component OWL có vòng đời: setup (khởi tạo, chưa DOM) → onWillStart (nạp dữ liệu, await, trước render) → render → onMounted (DOM đã gắn) — đo thật thứ tự này qua Console.
  2. Nạp dữ liệu ở onWillStart, chạm DOM ở onMounted — dùng useRef + t-ref, .el chỉ có từ onMounted; chạm DOM trong setup luôn null.
  3. Dọn dẹp ở onWillUnmount (timer, listener, kết nối); onWillUpdateProps chạy khi props đổi.

Hook lo khi nào code chạy. Còn phản ứng với người dùng — bấm, gõ, di chuột — thì gắn thế nào? Phần sau đi vào bắt sự kiện với t-on: t-on-click, truyền tham số cho handler, và những cái bẫy khi lặp trong t-foreach.