Reactivity & State
Lộ trình học cách mô hình hóa dữ liệu thay đổi, chọn Signals hoặc RxJS, và tổ chức state Angular từ cục bộ đến NgRx.
Bạn sẽ học gì ở nhóm này?
Bạn sẽ đi từ một component có vài giá trị thay đổi đến cách thiết kế luồng dữ liệu cho một feature. Mục tiêu không phải dùng nhiều công cụ, mà là biết dữ liệu nào là state, giá trị nào chỉ là dữ liệu dẫn xuất, và thay đổi nào là side effect.
Mục lục
- Mục tiêu của nhóm bài
- Kiến thức đầu vào
- Trước Signals: Angular biết khi nào cập nhật UI bằng cách nào?
- Signals được thêm vào để làm gì?
- Mô hình tinh thần: reactivity và state
- Bản đồ chọn công cụ
- Thứ tự học các bài con
- Lộ trình thực hành
- Dấu hiệu bạn đã sẵn sàng
- Bắt đầu học
Mục tiêu của nhóm bài
Reactivity là khả năng để giao diện và các phần phụ thuộc phản ứng lại khi dữ liệu thay đổi. State là dữ liệu hiện tại mà ứng dụng cần ghi nhớ để quyết định phải hiển thị hay xử lý điều gì. Ví dụ, nội dung ô tìm kiếm, danh sách sản phẩm đã tải và trạng thái loading đều là state.
Sau nhóm bài này, bạn có thể:
- Nhận diện state gốc, dữ liệu dẫn xuất và side effect thay vì đưa mọi thứ vào một biến mutable.
- Dùng Signals cho state đồng bộ, cục bộ và các giá trị UI phụ thuộc lẫn nhau.
- Dùng RxJS khi làm việc với luồng bất đồng bộ hoặc nhiều sự kiện theo thời gian, như HTTP, nhập liệu, route và WebSocket.
- Kết hợp Signals với Observable mà không biến đổi qua lại một cách tùy tiện.
- Chọn phạm vi state phù hợp: component, feature/service, hoặc store tập trung với NgRx.
- Giảm các lỗi quen thuộc: state trùng lặp,
subscribe()lồng nhau, effect ghi ngược vào state và state toàn cục quá sớm.
Nguyên tắc xuyên suốt
Hãy giữ một nguồn sự thật cho mỗi dữ liệu. Nếu một giá trị có thể tính từ state khác, hãy dẫn xuất nó thay vì lưu thêm một bản sao.
Kiến thức đầu vào
Bạn nên nắm các phần Angular cơ bản trước khi bắt đầu:
- Component, template, binding và event binding như
(click)hoặc(input). - Dependency injection và service; bạn cần biết vì state feature thường được đặt trong service.
- Lifecycle cơ bản và HTTP client; không cần thuộc mọi hook.
- TypeScript: kiểu dữ liệu,
interface, hàm, mảng và object bất biến ở mức cơ bản.
Bạn không cần biết NgRx hoặc RxJS trước. Bài RxJS căn bản xây nền tảng Observable trước khi bạn dùng chúng trong state thực tế.
Đừng nhảy thẳng vào NgRx
Store tập trung giải quyết một số vấn đề phối hợp state ở quy mô lớn, nhưng thêm khái niệm và quy ước. Hãy thành thạo state cục bộ trước để biết lúc nào một store thật sự cần thiết.
Trước Signals: Angular biết khi nào cập nhật UI bằng cách nào?
Trước Angular v16, Angular không có Signals tích hợp sẵn. Điều đó không có nghĩa giao diện không reactive. Angular vẫn có template binding, event binding, RxJS và change detection. Câu hỏi quan trọng là: sau khi JavaScript đổi một biến, làm sao Angular biết phải chạy change detection để DOM hiển thị giá trị mới?
Bài toán: field đổi nhưng DOM không tự đổi
Template dưới đây hiển thị field message của component:
@Component({
template: `<p>{{ message }}</p>`,
})
export class StatusComponent {
message = 'Đang chờ';
}Nếu đoạn JavaScript nào đó chạy this.message = 'Đã tải xong', bản thân JavaScript chỉ đổi giá trị trong bộ nhớ. Nó không tự biết thẻ <p> nào cần đổi text. Angular phải chạy một lượt change detection: đọc lại binding {{ message }}, thấy giá trị khác và cập nhật DOM.
Vấn đề là callback bất đồng bộ có thể đến từ rất nhiều nơi: người dùng click, setTimeout, Promise, HTTP, WebSocket hoặc thư viện bên thứ ba. Trước Signals, Angular cần một cách phổ quát để được báo: “có callback vừa chạy; có thể state đã đổi.”
Zone.js: “có async work xong, kiểm tra lại đi”
Zone.js là lớp bọc (patch) quanh nhiều API bất đồng bộ của trình duyệt, như event listener, timer và Promise. Nó để ý thời điểm callback chạy trong Angular zone. Khi công việc đó xong, Angular lên lịch change detection.
Vì vậy, trong một ứng dụng Angular truyền thống có Zone.js, ví dụ này thường tự cập nhật UI:
export class StatusComponent implements OnInit {
message = 'Đang chờ';
ngOnInit(): void {
setTimeout(() => {
this.message = 'Đã tải xong';
}, 1_000);
}
}Zone.js làm được một việc rất hữu ích: đánh thức Angular sau các việc async phổ biến. Nhờ đó, bạn có thể đổi một field thường trong callback và giao diện vẫn được kiểm tra lại.
Giới hạn của cách làm này
Zone.js chỉ biết “có việc async vừa xong”, không biết callback đó có thực sự đổi state hay không, càng không biết chính xác view nào đã đọc state đó. Vì an toàn, Angular phải kiểm tra các view phù hợp. Với ứng dụng lớn và nhiều callback, đây có thể là công việc dư.
Ví dụ, callback sau không đổi bất kỳ dữ liệu nào hiển thị, nhưng Zone.js vẫn chỉ có thể báo Angular kiểm tra:
setTimeout(() => {
console.log('Chỉ ghi log, UI không đổi');
}, 1_000);OnPush giúp Angular bỏ qua nhiều subtree không liên quan, nhưng nó không thay đổi bản chất thông báo của Zone.js: “có lẽ có gì đó đã đổi, hãy kiểm tra.” Ngoài ra, callback chạy ngoài Angular zone hoặc API không được Zone.js theo dõi có thể khiến UI không tự cập nhật như mong đợi.
Signals được thêm vào để làm gì?
Signals được giới thiệu dưới dạng developer preview ở Angular v16 để Angular có một thông tin trực tiếp hơn về state và nơi state được dùng. Signals không thay change detection; chúng cải thiện cách Angular biết khi nào và view nào cần được kiểm tra.
Signal ghi nhớ view nào đang dùng state
Khi template gọi một signal, Angular ghi nhớ quan hệ phụ thuộc đó:
@Component({
template: `<p>{{ message() }}</p>`,
})
export class StatusComponent {
readonly message = signal('Đang chờ');
}Ở lần render đầu, Angular biết template này đã đọc message(). Lúc cập nhật signal, Angular biết chính signal đó vừa đổi và view trên là consumer của nó:
setTimeout(() => {
this.message.set('Đã tải xong');
}, 1_000);Đây là lý do Signals hữu ích cho state giao diện: state tự mang theo khả năng thông báo, còn Angular biết consumer nào đang dùng state đó.
Thông báo chính xác thay vì phỏng đoán
So sánh hai cơ chế theo câu hỏi Angular nhận được:
| Cách Angular được báo | Thông tin Angular có | Ý nghĩa |
|---|---|---|
| Zone.js sau timer/Promise/event | “Một callback vừa chạy; state có thể đã đổi.” | Angular cần kiểm tra để tìm thay đổi. |
message.set(...) | “Signal message vừa đổi; view nào đã đọc nó đã được biết.” | Angular có thể đánh dấu đúng consumer phụ thuộc. |
Vì vậy, lý do thêm Signals không phải vì Zone.js làm UI cũ bị hỏng. Zone.js làm tốt việc đánh thức Angular một cách tiện lợi. Signals cho Angular một mô hình state rõ ràng và notification chính xác hơn, từ đó hỗ trợ render hiệu quả, OnPush tự nhiên hơn và ứng dụng không cần dựa vào việc vá toàn cục các API async.
Cách nhớ ngắn gọn
Zone.js nhìn vào việc đang xảy ra — timer, Promise, event đã xong. Signal nhìn vào dữ liệu — state nào đổi và ai đang dùng nó. Change detection là bước cuối cùng đọc template và đổi DOM.
Signals và Zone.js từng cùng tồn tại
Thêm Signals vào ứng dụng Angular v16–v20 không tự gỡ Zone.js. Ở các phiên bản này, Zone.js vẫn có thể đánh thức Angular cho code cũ; đồng thời, việc cập nhật signal tạo notification rõ ràng cho view đã đọc nó.
Từ Angular v21, zoneless là mặc định cho ứng dụng mới. Khi không dùng Zone.js, Angular vẫn nhận notification từ signal, AsyncPipe, event listener trong template và ChangeDetectorRef.markForCheck(). Điều đó cho thấy Signals là một phần quan trọng của hướng đi zoneless, nhưng không phải điều kiện duy nhất.
Đọc Signals và Zone.js nếu bạn muốn đi sâu vào change detection, OnPush và migration sang zoneless.
Mô hình tinh thần: reactivity và state
Đừng xem reactivity là một thư viện hay một cú pháp đặc biệt. Hãy xem nó như một đồ thị phụ thuộc: khi dữ liệu nguồn đổi, Angular biết các giá trị và phần giao diện nào cần được cập nhật.
Ba vai trò của dữ liệu
Một feature danh sách sản phẩm thường có ba vai trò dữ liệu khác nhau:
| Vai trò | Ví dụ | Cách xử lý phù hợp |
|---|---|---|
| State gốc | query, products, isLoading | Lưu và cập nhật tại một nguồn sự thật. |
| Dữ liệu dẫn xuất | filteredProducts, total, isEmpty | Tính từ state gốc; không lưu bản sao nếu không cần. |
| Side effect | gọi API, ghi localStorage, cập nhật tiêu đề trang | Chạy để tương tác với bên ngoài, không phải để thay thế phép tính dữ liệu. |
Ví dụ, filteredProducts được tính từ products và query. Nếu vừa lưu filteredProducts vừa lưu hai giá trị nguồn, bạn phải tự đồng bộ ba biến. Khi người dùng đổi query, bản sao rất dễ cũ. Một computed hoặc phép biến đổi Observable cho phép giá trị dẫn xuất tự thay đổi theo nguồn.
Từ thay đổi đến giao diện
Luồng lành mạnh bắt đầu từ một sự kiện hoặc nguồn dữ liệu, đi qua state gốc, rồi mới đến giá trị dẫn xuất và template:
Mũi tên nên chủ yếu đi theo một chiều. Template đọc state và phát sự kiện; method hoặc service xử lý sự kiện rồi cập nhật state. Cách này giúp bạn trả lời nhanh câu hỏi: “giá trị trên màn hình này được sinh ra từ đâu?”
Phân biệt derived state và effect
Không dùng effect để đặt total từ items. total là dữ liệu dẫn xuất, nên dùng computed. Chỉ dùng effect khi cần chạm vào thế giới bên ngoài, chẳng hạn đồng bộ một lựa chọn vào localStorage.
Bản đồ chọn công cụ
Signals và RxJS không loại trừ nhau. Chúng giải quyết hai dạng bài toán gần nhau nhưng khác trọng tâm.
| Khi bạn gặp tình huống | Điểm khởi đầu nên chọn | Lý do |
|---|---|---|
| Toggle mở/đóng, tab đang chọn, bộ lọc, state của một màn hình | Signal | Giá trị hiện tại được đọc đồng bộ và template dùng trực tiếp. |
| Tính tổng, lọc danh sách, bật/tắt nút từ state khác | computed | Diễn tả rõ quan hệ dẫn xuất, tránh state trùng lặp. |
| Ghi log, đồng bộ trình duyệt hoặc gọi API theo thay đổi | effect hoặc pipeline RxJS | Đây là tác động ra bên ngoài luồng tính toán. |
| HTTP, debounce input, route params, WebSocket, retry | Observable/RxJS | Dữ liệu đến theo thời gian và cần hủy, ghép hoặc biến đổi luồng. |
| State được nhiều component trong một feature dùng chung | Service có Signals hoặc Observable | Chia sẻ vừa đủ trong phạm vi feature. |
| State dùng rộng rãi, có workflow phức tạp, cần DevTools/quy ước chung | NgRx | Store tập trung tạo tính nhất quán khi chi phí vận hành là xứng đáng. |
Điểm quyết định không phải “công cụ nào mới hơn”, mà là dữ liệu đến như một giá trị hiện tại hay một chuỗi sự kiện theo thời gian. Một kết quả HTTP có thể được xử lý bằng RxJS rồi được đưa thành Signal để template đọc thuận tiện.
Thứ tự học các bài con
Học theo thứ tự dưới đây. Mỗi bài giải quyết một câu hỏi, và bài sau dựa trên mô hình của bài trước.
- Signals — Bắt đầu với
signal, cách đọc trong template và các cách cập nhật state. Đây là nền tảng để mô hình hóa giá trị hiện tại của UI. - Computed và effect — Phân tách giá trị được tính lại (
computed) với tác động bên ngoài (effect). Sau bài này, bạn có thể tránh phần lớn derived state dư thừa. - RxJS căn bản — Học Observable, operator và vòng đời subscription. Đây là ngôn ngữ để xử lý dữ liệu bất đồng bộ theo thời gian.
- Signals và RxJS — Chọn đúng công cụ cho từng đoạn của luồng, đồng thời chuyển đổi giữa chúng có chủ đích.
- State cục bộ — Đặt state vào component hoặc service của feature, thiết kế API đọc/cập nhật rõ ràng mà chưa cần global store.
- NgRx — Đánh giá nhu cầu store tập trung và nắm các khái niệm cốt lõi khi ứng dụng cần một quy ước state chung.
Cách đọc hiệu quả
Mỗi bài hãy tự viết lại một ví dụ nhỏ. Đọc xong nhưng không thay đổi một state thật hoặc không quan sát luồng dữ liệu chạy sẽ khó hình thành trực giác.
Lộ trình thực hành
Dùng một feature duy nhất để thấy các quyết định state tiến triển tự nhiên: danh sách công việc có tìm kiếm. Không cần tạo ứng dụng lớn; một component và một service là đủ để bắt đầu.
- State UI bằng Signals. Tạo
tasks,queryvàselectedFilter. Hiển thị chúng trong template, sau đó thêm/xóa một công việc bằng event handler. - Dẫn xuất thay vì sao chép. Tạo
visibleTasksvàremainingCounttừ các state trên. Thử đổi bộ lọc và kiểm tra bạn không phải cập nhật các giá trị này bằng tay. - Thêm dữ liệu bất đồng bộ. Lấy công việc từ API giả lập. Dùng Observable để biểu diễn lúc tải, lỗi và kết quả; thử debounce ô tìm kiếm.
- Nối hai mô hình. Đưa kết quả phù hợp về Signal cho template hoặc giữ Observable khi bạn cần tiếp tục ghép luồng. Giải thích được lý do chọn của mình.
- Tách state theo feature. Di chuyển logic ra service. Component chỉ gửi ý định của người dùng và đọc view model mà service cung cấp.
- Đánh giá NgRx, không mặc định áp dụng. Chỉ chuyển sang NgRx nếu nhiều khu vực độc lập cùng cập nhật/đọc state, workflow phức tạp hoặc nhóm cần truy vết thay đổi nhất quán.
Khi hoàn thành, hãy tự kiểm tra bằng một thay đổi cụ thể: nhập từ khóa, đổi filter, tải lại dữ liệu và xóa một phần tử. Với mỗi thay đổi, bạn phải chỉ được state gốc nào đổi, giá trị nào dẫn xuất lại và side effect nào chạy.
Dấu hiệu bạn đã sẵn sàng
Bạn đã nắm được nền tảng của nhóm này khi có thể trả lời các câu hỏi sau mà không dựa vào công thức có sẵn:
- Giá trị này có phải state gốc không, hay có thể tính từ dữ liệu khác?
- Khi dùng
computed, dependency nào khiến nó tính lại? - Luồng Observable này bắt đầu, kết thúc và bị hủy ở đâu?
- Tại sao phần này nên ở component, service feature hay store tập trung?
- Side effect có đang ghi ngược vào state dẫn xuất, tạo vòng lặp hoặc làm khó kiểm thử không?
Nếu chưa trả lời được, quay về ví dụ nhỏ nhất và vẽ lại mũi tên dữ liệu. Mô hình đơn giản, một chiều thường dễ sửa hơn việc thêm state mới.
Bắt đầu học
1. Signals
Tạo và cập nhật state đồng bộ cho giao diện Angular.
2. Computed và effect
Phân biệt dữ liệu dẫn xuất với side effect.
3. RxJS căn bản
Nắm Observable và các thao tác với luồng bất đồng bộ.
4. Signals và RxJS
Kết hợp hai mô hình phản ứng một cách có chủ đích.
5. State cục bộ
Tổ chức state theo component và feature.
6. NgRx
Chọn store tập trung đúng thời điểm.