Skip to content

Commit 8e05fd8

Browse files
authored
Merge pull request #164 from GetStream/channel_and_notification_events
fix: Add missing ngZone reenters
2 parents 35104a9 + d2995ac commit 8e05fd8

File tree

8 files changed

+278
-183
lines changed

8 files changed

+278
-183
lines changed
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
---
2+
id: change-detection
3+
sidebar_position: 4
4+
title: Change detection
5+
---
6+
7+
For performance reasons, the Stream chat WebSocket connection is opened outside of the [Angular change detection zone](https://angular.io/guide/zone). This means that when we react to WebSocket events, Angular won't update the UI in response to these events. Furthermore, if a new component is created reacting to a WebSocket event (for example, if we receive a new message, and a new message component is created to display the new message), the new component will operate outside of the Angular change detection zone. To solve this problem, we need to reenter Angular's change detection zone.
8+
9+
## Reentering Angular's change detection zone
10+
11+
You can reenter Angular's change detection zone with the `run` method of the `NgZone` service. For example if you want to display a notification when a user is added to a channel, you can watch for the `notification.added_to_channel` event and return to the zone when that event is received:
12+
13+
```typescript
14+
import { Component, NgZone, OnInit } from "@angular/core";
15+
import { filter } from "rxjs/operators";
16+
import { ChatClientService, NotificationService } from "stream-chat-angular";
17+
18+
@Component({
19+
selector: "app-root",
20+
templateUrl: "./app.component.html",
21+
styleUrls: ["./app.component.scss"],
22+
})
23+
export class AppComponent implements OnInit {
24+
constructor(
25+
private chatService: ChatClientService,
26+
private notificationService: NotificationService,
27+
private ngZone: NgZone
28+
) {}
29+
30+
ngOnInit(): void {
31+
this.chatService.notification$
32+
.pipe(filter((n) => n.eventType === "notification.added_to_channel"))
33+
.subscribe((notification) => {
34+
// reenter Angular's change detection zone
35+
this.ngZone.run(() => {
36+
this.notificationService.addTemporaryNotification(
37+
`You've been added to the ${notification.event.channel?.name} channel`,
38+
"success"
39+
);
40+
});
41+
});
42+
}
43+
}
44+
```
45+
46+
If you were to display the notification without reentering Angular's zone, the `addTemporaryNotification` would run outside of Angular's change detection zone, and the notification wouldn't disappear after the 5-second timeout.
47+
48+
## When necessary to reenter the zone
49+
50+
You need to reenter Angular's change detection zone when
51+
52+
- you subscribe to events using the [`notification$`](../services/chat-client.mdx/#notification) Observable of the `ChatClientService`
53+
- you subscribe to channel events
54+
55+
For example the [`ChannelPreview`](../components/channel-preview.mdx) component needs to subscribe to the `message.read` channel events to know if the channel has unread messages and reenter Angular's zone when an event is received:
56+
57+
```typescript
58+
this.channel.on("message.read", () =>
59+
this.ngZone.run(() => {
60+
this.isUnread = !!this.channel.countUnread() && this.canSendReadEvents;
61+
})
62+
);
63+
```
64+
65+
## When unnecessary to reenter the zone
66+
67+
You **don't** need to reenter the zone when
68+
69+
- you use the SDK's default components in your UI and don't watch for additional events
70+
- when you [override the default channel list behavior](../services/channel.mdx/#channels)
71+
- when you subscribe to the [`connectionState$`](../services/chat-client.mdx/#connectionstate) Observable of the `ChatClientService`
72+
73+
If you are unsure whether or not you are in Angular's zone, you can use the following function call to check:
74+
75+
```typescript
76+
NgZone.isInAngularZone();
77+
```

docusaurus/docs/Angular/services/channel.mdx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,13 @@ Queries the channels with the given filters, sorts and options. More info about
1414

1515
## channels$
1616

17-
Emits the currently loaded and [watched](https://getstream.io/chat/docs/javascript/watch_channel/?language=javascript) channel list. Apart from pagination, the channel list is also updated on the following events:
17+
Emits the currently loaded and [watched](https://getstream.io/chat/docs/javascript/watch_channel/?language=javascript) channel list.
18+
19+
:::important
20+
If you want to subscribe to channel events, you need to manually reenter Angular's change detection zone, our [Change detection guide](../concepts/change-detection.mdx) explains this in detail.
21+
:::
22+
23+
Apart from pagination, the channel list is also updated on the following events:
1824

1925
| Event type | Default behavior | Custom handler to override |
2026
| ----------------------------------- | ------------------------------------------------------------------ | --------------------------------------------- |
@@ -37,7 +43,7 @@ Our platform documentation covers the topic of [channel events](https://getstrea
3743
Emits the currently active channel.
3844

3945
:::important
40-
Please note that for performance reaasons the client is connected [outside of the NgZone](https://angular.io/guide/zone#ngzone-1), if you want to subscribe to [notification or channel events](https://getstream.io/chat/docs/javascript/event_object/?language=javascript), you will need to [reenter the NgZone](https://angular.io/guide/zone#ngzone-run-and-runoutsideofangular) or call change detection manually (you can use the [`ChangeDetectorRef`](https://angular.io/api/core/ChangeDetectorRef) or the [`ApplicationRef`](https://angular.io/api/core/ApplicationRef) for that).
46+
If you want to subscribe to channel events, you need to manually reenter Angular's change detection zone, our [Change detection guide](../concepts/change-detection.mdx) explains this in detail.
4147
:::
4248

4349
## setAsActiveChannel

docusaurus/docs/Angular/services/chat-client.mdx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,17 @@ The `ChatClient` service connects the user to the Stream chat.
1212

1313
The [StreamChat client](https://github.com/GetStream/stream-chat-js/blob/master/src/client.ts) instance. In general you shouldn't need to access the client, but it's there if you want to use it.
1414

15-
:::important
16-
Please note that for performance reaasons the client is connected [outside of the NgZone](https://angular.io/guide/zone#ngzone-1), if you want to subscribe to [notification or channel events](https://getstream.io/chat/docs/javascript/event_object/?language=javascript), you will need to [reenter the NgZone](https://angular.io/guide/zone#ngzone-run-and-runoutsideofangular) or call change detection manually (you can use the [`ChangeDetectorRef`](https://angular.io/api/core/ChangeDetectorRef) or the [`ApplicationRef`](https://angular.io/api/core/ApplicationRef) for that).
17-
:::
18-
1915
## init
2016

2117
Creates a [`StreamChat`](https://github.com/GetStream/stream-chat-js/blob/668b3e5521339f4e14fc657834531b4c8bf8176b/src/client.ts#L124) instance using the provided `apiKey`, and connects a user with the given `userId` and `userToken`. More info about [connecting users](https://getstream.io/chat/docs/javascript/init_and_users/?language=javascript) can be found in the platform documentation.
2218

2319
## notification$
2420

25-
Emits [`Notification`](https://github.com/GetStream/stream-chat-angular/blob/master/projects/stream-chat-angular/src/lib/chat-client.service.ts) events, the list of [supported events](https://github.com/GetStream/stream-chat-angular/blob/master/projects/stream-chat-angular/src/lib/chat-client.service.ts) can be found on GitHub. The platform documentation covers [events in detail](https://getstream.io/chat/docs/javascript/event_object/?language=javascript).
21+
Emits [`Notification`](https://github.com/GetStream/stream-chat-angular/blob/master/projects/stream-chat-angular/src/lib/chat-client.service.ts) events. The platform documentation covers [the list of client and notification events](https://getstream.io/chat/docs/javascript/event_object/?language=javascript).
22+
23+
:::important
24+
For performance reasons this Observable operates outside of the Angular change detection zone. If you subscribe to it, you need to manually reenter Angular's change detection zone, our [Change detection guide](../concepts/change-detection.mdx) explains this in detail.
25+
:::
2626

2727
## connectionState$
2828

projects/stream-chat-angular/src/lib/channel-preview/channel-preview.component.ts

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
1+
import { Component, Input, NgZone, OnDestroy, OnInit } from '@angular/core';
22
import { Subscription } from 'rxjs';
33
import {
44
Channel,
@@ -21,7 +21,7 @@ export class ChannelPreviewComponent implements OnInit, OnDestroy {
2121
private subscriptions: (Subscription | { unsubscribe: () => void })[] = [];
2222
private canSendReadEvents = true;
2323

24-
constructor(private channelService: ChannelService) {}
24+
constructor(private channelService: ChannelService, private ngZone: NgZone) {}
2525

2626
ngOnInit(): void {
2727
this.subscriptions.push(
@@ -51,11 +51,11 @@ export class ChannelPreviewComponent implements OnInit, OnDestroy {
5151
this.channel!.on('channel.truncated', this.handleMessageEvent.bind(this))
5252
);
5353
this.subscriptions.push(
54-
this.channel!.on(
55-
'message.read',
56-
() =>
57-
(this.isUnread =
58-
!!this.channel!.countUnread() && this.canSendReadEvents)
54+
this.channel!.on('message.read', () =>
55+
this.ngZone.run(() => {
56+
this.isUnread =
57+
!!this.channel!.countUnread() && this.canSendReadEvents;
58+
})
5959
)
6060
);
6161
}
@@ -81,19 +81,21 @@ export class ChannelPreviewComponent implements OnInit, OnDestroy {
8181
}
8282

8383
private handleMessageEvent(event: Event) {
84-
if (this.channel?.state.messages.length === 0) {
85-
this.latestMessage = 'Nothing yet...';
86-
return;
87-
}
88-
if (
89-
!event.message ||
90-
this.channel?.state.messages[this.channel?.state.messages.length - 1]
91-
.id !== event.message.id
92-
) {
93-
return;
94-
}
95-
this.setLatestMessage(event.message);
96-
this.isUnread = !!this.channel.countUnread() && this.canSendReadEvents;
84+
this.ngZone.run(() => {
85+
if (this.channel?.state.messages.length === 0) {
86+
this.latestMessage = 'Nothing yet...';
87+
return;
88+
}
89+
if (
90+
!event.message ||
91+
this.channel?.state.messages[this.channel?.state.messages.length - 1]
92+
.id !== event.message.id
93+
) {
94+
return;
95+
}
96+
this.setLatestMessage(event.message);
97+
this.isUnread = !!this.channel.countUnread() && this.canSendReadEvents;
98+
});
9799
}
98100

99101
private setLatestMessage(message?: FormatMessageResponse | MessageResponse) {

0 commit comments

Comments
 (0)