Skip to content

Commit 7902e27

Browse files
committed
Initial guide and examples
1 parent 56e338c commit 7902e27

File tree

64 files changed

+14714
-2
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

64 files changed

+14714
-2
lines changed

.gitignore

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# IDEs and editors
2+
/.idea
3+
.project
4+
.classpath
5+
.c9/
6+
*.launch
7+
.settings/
8+
*.sublime-workspace
9+
10+
# IDE - VSCode
11+
.vscode/*
12+
!.vscode/settings.json
13+
!.vscode/tasks.json
14+
!.vscode/launch.json
15+
!.vscode/extensions.json
16+
.history/*

README.md

Lines changed: 329 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,329 @@
1-
# angular-rxjs-unsubscribe
2-
When you should unsubscribe in your Angular app
1+
# When should I unsubscribe from RxJS observables in Angular?
2+
There's a lot of confusion in the community about the question if you have to unsubscribe
3+
from RxJS observables in Angular.
4+
5+
Although there are some quite nice StackOverflow posts about this questions, there is no clear guidance
6+
that contains all relevant information explained by examples.
7+
8+
This repository provides the necessary Angular project to examine the problems by yourself and a guide for common situations.
9+
10+
### Some resources:
11+
##### Angular/RxJs When should I unsubscribe from Subscription:
12+
https://stackoverflow.com/questions/38008334/angular-rxjs-when-should-i-unsubscribe-from-subscription
13+
14+
##### Is it necessary to unsubscribe from observables created by Http methods?
15+
https://stackoverflow.com/questions/35042929/is-it-necessary-to-unsubscribe-from-observables-created-by-http-methods
16+
17+
18+
## How to run the examples by yourself
19+
The project was set up using the latest Angular CLI. Therefore you can just clone the repository and run
20+
``
21+
npm start
22+
``
23+
in the
24+
``
25+
angular-app
26+
``
27+
folder.
28+
29+
## The questions and the study method
30+
We want to answer the following questions in each case study:
31+
### 1. Can we run into the problem of unwanted side effects?
32+
To investigate this issue, we use the realistic scenario, that we want to set the document title
33+
dynamically on behalf of a observable. To this end, we use the Angular title service (https://angular.io/guide/set-document-title).
34+
35+
We set the title in the observables callback, navigate to the ``EmptyComponent``
36+
(which does nothing else than setting its own title), and observe if the title still gets updated.
37+
38+
### 2. Can we run into the problem of memory leaks?
39+
We again route from our component of case study to the ``EmptyComponent``. Under normal circumstances,
40+
the component we came from should be garbage collected.
41+
However, it can happen that the component cannot be garbage collected due to references used in observable callbacks.
42+
43+
We will use Google Chrome's memory snapshot tool to see if the components get garbage collected or not.
44+
45+
46+
## Observables that don't complete
47+
The first and most trivial case is that you have an observable, that does not complete.
48+
49+
Say you have a timer observable, like in our case study component ``RxjsTimerComponent``
50+
```
51+
this.subscription = timer(0, 1000)
52+
.subscribe(() => {
53+
this.counter++;
54+
this.titleService.setTitle('Counter ' + this.counter);
55+
});
56+
```
57+
The is an infinite timer observable, that emits each second. On each emit, we increase a counter and set
58+
the document title with the title service.
59+
60+
### Outcomes
61+
#### Side effects
62+
As expected, after routing to the ``EmptyComponent``, the document title gets updated each second.
63+
So the observable still runs after the ``RxjsTimerComponent`` was destroyed.
64+
So we have an unwanted side effect in our example.
65+
66+
#### Memory leaks
67+
As expected, after routing to the ``EmptyComponent``, the ``RxjsTimerComponent`` is still in memory and doesn't
68+
get garbage collected. This makes sense, since there is still a reference to ``this.counter``
69+
and ``this.titleService`` in the observables callback method.
70+
71+
In fact, by navigating back and forth between ``EmptyComponent`` and ``RxjsTimerComponent`` we can create many
72+
``RxjsTimerComponent`` objects that doesn't get cleaned up. Thus we indeed created a memory leak issue in our example.
73+
74+
NOTE: If you don't use any references (like member variables) from the component in the callback, the component gets garbage collected
75+
and there is no memory leak.
76+
77+
### Countermeasures
78+
One countermeasure (among others at the end of this readme) is to manually unsubscribe in ``ngOnDestroy``.
79+
```
80+
ngOnDestroy() {
81+
// Avoid side effects and memory leak by unsubscribing:
82+
this.subscription.unsubscribe();
83+
}
84+
```
85+
With this statement, the side effects and the memory leak are avoided.
86+
87+
### Summary
88+
__ALWAYS unsubscribe if the observable does not complete or if you are not sure if it completes,
89+
when using 1. code with side effects or 2. accessing member variables in the callback.__
90+
91+
92+
## Observables that eventually complete
93+
We created the component ``RxjsTimerComplete``. It contains the following observable:
94+
```
95+
this.subscription = timer(0, 1000)
96+
.pipe(take(5))
97+
.subscribe(() => {
98+
this.counter++;
99+
this.titleService.setTitle('Counter ' + this.counter);
100+
});
101+
```
102+
We used a pipe with ``take(5)``, thus the observable we subscribe to takes only
103+
the first 5 values from the source and then completes.
104+
105+
Again, we increase a counter on each emit and set
106+
the document title with the title service.
107+
108+
### Outcomes
109+
#### Side effects
110+
If we navigate, before the last value is emitted (i.e. within the first 5 seconds), we can observe
111+
that the document title is still updating. Thus we have an unwanted side effect as in the first case study,
112+
as long as the observable is not complete yet.
113+
114+
Of course there is _no_ unwanted side effect _in our example_, if we wait and navigate after the first 5 values were emitted and
115+
the observable completed.
116+
117+
#### Memory leak
118+
No matter if we navigate before the observable completes or afterwards -
119+
the component ``RxjsTimerComplete`` always eventually gets garbage collected after it completed.
120+
So there is no memory leak.
121+
122+
### Countermeasures
123+
One countermeasure to avoid possible side effects (among others at the end of this readme) is to manually unsubscribe in ``ngOnDestroy``.
124+
```
125+
ngOnDestroy() {
126+
// Avoid side effects by unsubscribing:
127+
this.subscription.unsubscribe();
128+
}
129+
```
130+
With this statement, the side effects and the memory leak are avoided.
131+
132+
### Summary
133+
__ALWAYS unsubscribe if you execute code with side effects in your callback.__
134+
135+
## Observables from the Angular HttpClient
136+
Let's have a look at the angular source code to understand how an observable created from the HttpClient works.
137+
```
138+
if (ok) {
139+
observer.next(new HttpResponse({
140+
// ... code omitted ...
141+
}));
142+
143+
observer.complete();
144+
} else {
145+
observer.error(new HttpErrorResponse({
146+
// ... code omitted ...
147+
}));
148+
}
149+
```
150+
The important part is ``observer.complete()`` in successful call and ``observer.error(...)`` in error case,
151+
which means the observable has finished and does not emit any more values.
152+
153+
This means that the same findings from the case study [Observables that eventually complete](#Observables that eventually complete) apply.
154+
155+
Why? Since network calls can be arbitrarily delayed, the observable can complete when you already navigated to another component.
156+
157+
## Case study
158+
We added a little server written in Go under the folder ``go-server`` to the project,
159+
such that the effect of delayed network calls can be demonstrated.
160+
161+
Furthermore we added the component ``HttpclientComponent`` which issues a GET call to the API exposed by the Go Server.
162+
The GET call delays the response by 5 seconds and then returns an object with a title property.
163+
The component sets the document title with the received data:
164+
165+
```
166+
this.subscription = this.httpClient.get<ApiResponse>('/api').subscribe((result) => {
167+
this.title = result.title;
168+
this.titleService.setTitle(result.title);
169+
});
170+
```
171+
172+
### Outcomes
173+
As already mentioned, we observe the same outcome as with an observable that eventually completes.
174+
175+
#### Side effects
176+
Are possible due to network latencies. The callback still gets executed, even if the user already navigated to another component.
177+
178+
#### Memory leak
179+
After the call completes the component gets garbage collected (even if navigated to another component).
180+
181+
### Countermeasures
182+
One countermeasure to avoid possible side effects (amongst others at the end of this readme) is to manually unsubscribe in ``ngOnDestroy``.
183+
```
184+
ngOnDestroy() {
185+
// Avoid side effects by unsubscribing:
186+
this.subscription.unsubscribe();
187+
}
188+
```
189+
With this statement, the side effects and the memory leak are avoided.
190+
191+
### Summary
192+
__ALWAYS unsubscribe if you execute code with side effects in your callback.__
193+
Many get that wrong, since they have almost no latency in their network calls when testing their application.
194+
If you do not execute methods with unwanted side effects, you do not have to unsubscribe.
195+
Memory leaks are avoided by the HttpClient as the observable completes itself.
196+
197+
## Recommended ways to unsubscribe
198+
The obvious way of unsubscribing is how it is done in our examples: Assign the subscription to a class
199+
property and unsubscribe in the ``ngOnDestroy()`` method.
200+
201+
However, its not feasible for large applications with many subscriptions,
202+
as its cumbersome to write and introduces a lot of obfuscating code.
203+
204+
Another way of handling it, would be to collect all subscriptions and unsubscribe them at once:
205+
```
206+
private readonly subscription = new Subscription();
207+
208+
everySecond = timer(0, 1000);
209+
everyThirdSecond = timer(0, 3000);
210+
211+
constructor() {
212+
}
213+
214+
ngOnInit() {
215+
this.subscription.add(this.everySecond.subscribe(() => {
216+
// some logic here
217+
}));
218+
this.subscription.add(this.everyThirdSecond.subscribe(() => {
219+
// some logic here
220+
}));
221+
}
222+
223+
ngOnDestroy() {
224+
this.subscription.unsubscribe();
225+
}
226+
```
227+
However, same drawbacks here: Cumbersome and obfuscating.
228+
229+
### Unsubscribe with ``takeUntil``
230+
A cleaner way of unsubscribing is using ``takeUntil``.
231+
232+
Official docs for ``takeUntil``: ``takeUntil(notifier: Observable<any>)`` — Emits the values emitted by the source Observable until a notifier Observable emits a value.
233+
234+
Example code:
235+
```
236+
private readonly ngDestroy = new Subject();
237+
238+
everySecond = timer(0, 1000);
239+
everyThirdSecond = timer(0, 3000);
240+
241+
constructor() {
242+
}
243+
244+
ngOnInit() {
245+
this.everySecond.pipe(takeUntil(this.ngDestroy))
246+
.subscribe(() => {
247+
// some logic here
248+
});
249+
250+
this.everyThirdSecond.pipe(takeUntil(this.ngDestroy))
251+
.subscribe(() => {
252+
// some logic here
253+
});
254+
}
255+
256+
ngOnDestroy() {
257+
this.ngDestroy.next();
258+
this.ngDestroy.complete();
259+
}
260+
```
261+
When the component gets destroyed, the observable ``ngDestroy`` gets completed, causing the subscriptions to complete.
262+
Thus memory leaks and side effects are avoided.
263+
264+
Drawbacks: As with the other methods, it's still quite verbose and error-prone.
265+
266+
NOTE: takeUntil operator should always come last (https://blog.angularindepth.com/rxjs-avoiding-takeuntil-leaks-fb5182d047ef)
267+
268+
### Using ``untilDestroyed``
269+
There is an npm package called ``@ngneat/until-destroy`` (https://github.com/ngneat/until-destroy).
270+
271+
You can install it (for Angular versions using Ivy) with
272+
```
273+
npm install --save @ngneat/until-destroy
274+
```
275+
276+
Or for previous Angular versions with:
277+
```
278+
npm install --save ngx-take-until-destroy
279+
```
280+
281+
It simplifies the unsubscription handling a lot. It internally creates a ``Subject`` and uses ``takeUntil``, hooked into ``ngOnDestroy``.
282+
The above code simplifies to:
283+
```
284+
everySecond = timer(0, 1000);
285+
everyThirdSecond = timer(0, 3000);
286+
287+
constructor() {
288+
}
289+
290+
ngOnInit() {
291+
this.everySecond.pipe(untilDestroyed(this))
292+
.subscribe(() => {
293+
// some logic here
294+
});
295+
296+
this.everyThirdSecond.pipe(untilDestroyed(this))
297+
.subscribe(() => {
298+
// some logic here
299+
});
300+
}
301+
302+
ngOnDestroy() {
303+
// needed for untilDestroyed
304+
}
305+
```
306+
Only drawback: We have to implement ``ngOnDestroy`` everywhere we want to use ``untilDestroyed``.
307+
However it should be easy to write a linting rule to enforce this.
308+
309+
## Conclusion
310+
Whether you have to unsubscribe or not heavily depends on the callback logic you are using.
311+
312+
If the callback executes code with side effects you should always unsubscribe.
313+
314+
If the callback uses member variables from the component class there can be a memory leak when using observables that don't complete,
315+
therefore you should unsubscribe in that case.
316+
317+
| | Side effects | Memory leaks |
318+
|----------------------------------------|-----------------|--------------|
319+
| _Observables that don't complete_ | Possible(1) | Possible(2) |
320+
| _Observables that eventually complete_ | Possible(1) | No |
321+
| _Angular HttpClient_ | Possible(1) | No |
322+
323+
Possible(1): If you execute methods with side effects in the callback.
324+
325+
Possible(2): If you use member variables from the component in the callback.
326+
327+
## TODO
328+
- Explain effect on component tree
329+
- Explain side effects

angular-app/.editorconfig

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Editor configuration, see https://editorconfig.org
2+
root = true
3+
4+
[*]
5+
charset = utf-8
6+
indent_style = space
7+
indent_size = 2
8+
insert_final_newline = true
9+
trim_trailing_whitespace = true
10+
11+
[*.md]
12+
max_line_length = off
13+
trim_trailing_whitespace = false

angular-app/.gitignore

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# See http://help.github.com/ignore-files/ for more about ignoring files.
2+
3+
# compiled output
4+
/dist
5+
/tmp
6+
/out-tsc
7+
# Only exists if Bazel was run
8+
/bazel-out
9+
10+
# dependencies
11+
/node_modules
12+
13+
# profiling files
14+
chrome-profiler-events*.json
15+
speed-measure-plugin*.json
16+
17+
# misc
18+
/.sass-cache
19+
/connect.lock
20+
/coverage
21+
/libpeerconnection.log
22+
npm-debug.log
23+
yarn-error.log
24+
testem.log
25+
/typings
26+
27+
# System Files
28+
.DS_Store
29+
Thumbs.db

0 commit comments

Comments
 (0)