Angular Learning
Reactivity & State

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

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);
  }
}
direction: right

n0: "setTimeout hoàn tất" {
  width: 176
  height: 58
}

n1: "Zone.js thấy callback" {
  width: 192
  height: 58
}

n2: "Angular chạy\nchange detection" {
  width: 152
  height: 58
}

n3: "Đọc {{ message }}\n<p> thành Đã tải xong" {
  width: 192
  height: 58
}

n0 -> n1
n1 -> n2
n2 -> n3
setTimeout hoàn tấtZone.js thấy callbackAngular chạychange detectionĐọc {{ message }}<p> thành Đã tải xong
setTimeout hoàn tấtZone.js thấy callbackAngular chạychange detectionĐọc {{ message }}<p> thành Đã tải xong
Zone.js giúp Angular biết callback vừa chạy

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);
direction: right

n0: "Template đọc message()" {
  width: 200
  height: 58
}

n1: "Ghi dependency\nStatusComponent dùng\nmessage" {
  width: 184
  height: 79
}

n2: "message.set\nĐã tải xong" {
  width: 130
  height: 58
}

n3: "Đánh dấu component\nđể cập nhật" {
  width: 168
  height: 58
}

n0 -> n1
n1 -> n2
n2 -> n3
Template đọc message()Ghi dependencyStatusComponent dùngmessagemessage.setĐã tải xongĐánh dấu componentđể cập nhật
Template đọc message()Ghi dependencyStatusComponent dùngmessagemessage.setĐã tải xongĐánh dấu componentđể cập nhật
Signal nối lần đọc template với lần cập nhật (1/2)
direction: right

n3: "Đánh dấu component\nđể cập nhật" {
  width: 168
  height: 58
}

n4: "{{ message() }}\nĐổi text DOM" {
  width: 144
  height: 58
}

n3 -> n4
Đánh dấu componentđể cập nhật{{ message() }}Đổi text DOM
Đánh dấu componentđể cập nhật{{ message() }}Đổi text DOM
Signal nối lần đọc template với lần cập nhật (2/2)

Đâ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áoThô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ốcquery, products, isLoadingLưu và cập nhật tại một nguồn sự thật.
Dữ liệu dẫn xuấtfilteredProducts, total, isEmptyTính từ state gốc; không lưu bản sao nếu không cần.
Side effectgọi API, ghi localStorage, cập nhật tiêu đề trangChạ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:

direction: right

n0: "Input angular / HTTP\ntrả" {
  width: 184
  height: 58
}

n1: "State gốc\nquery, products,\nisLoading, error" {
  width: 152
  height: 79
}

n2: "Dữ liệu dẫn xuất\nfilteredProducts,\nresultCount" {
  width: 160
  height: 79
}

n3: "Template đọc + hiển\nthị" {
  width: 176
  height: 58
}

n0 -> n1
n1 -> n2
n2 -> n3
Input angular / HTTPtrảState gốcquery, products,isLoading, errorDữ liệu dẫn xuấtfilteredProducts,resultCountTemplate đọc + hiểnthị
Input angular / HTTPtrảState gốcquery, products,isLoading, errorDữ liệu dẫn xuấtfilteredProducts,resultCountTemplate đọc + hiểnthị
State gốc, dữ liệu dẫn xuất và side effect (1/2)
direction: right

n3: "Template đọc + hiển\nthị" {
  width: 176
  height: 58
}

n4: "Side effect khi cần\nURL, lưu lựa chọn,\nanalytics" {
  width: 176
  height: 79
}

n3 -> n4
Template đọc + hiểnthịSide effect khi cầnURL, lưu lựa chọn,analytics
Template đọc + hiểnthịSide effect khi cầnURL, lưu lựa chọn,analytics
State gốc, dữ liệu dẫn xuất và side effect (2/2)

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ọnLý do
Toggle mở/đóng, tab đang chọn, bộ lọc, state của một màn hìnhSignalGiá 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áccomputedDiễ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 đổieffect 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, retryObservable/RxJSDữ 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 chungService có Signals hoặc ObservableChia 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 chungNgRxStore 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.

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.

  1. State UI bằng Signals. Tạo tasks, query và selectedFilter. Hiển thị chúng trong template, sau đó thêm/xóa một công việc bằng event handler.
  2. Dẫn xuất thay vì sao chép. Tạo visibleTasks và remainingCount từ 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.
  3. 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.
  4. 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.
  5. 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.
  6. Đá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

On this page