People often complain that it can be hard to use RxJS in the right way. There is a feeling that memory leaks are always around the corner. With the recent introduction of Signals in Angular the RxJS situation is even more unsure. However, RxJS is here to stay! To avoid RxJS pitfalls, understanding the concepts is crucial. Besides that there is also some help we can get in through RxJS eslint. In this post, I show various rules.
Setup of RxJS with eslint
There is an eslint-plugin-rxjs available. To install the package you first need to install the ESLint TypeScript parser followed by the package itself:
npm install @typescript-eslint/parser --save-dev
npm install eslint-plugin-rxjs --save-dev
In the .eslintrc.js
file in your project you can configure the plugin (code taken from https://github.com/cartant/eslint-plugin-rxjs):
const { join } = require("path");
module.exports = {
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaVersion: 2019,
project: join(__dirname, "./tsconfig.json"),
sourceType: "module"
},
plugins: ["rxjs"],
extends: [],
rules: {
"rxjs/no-async-subscribe": "error",
"rxjs/no-ignored-observable": "error",
"rxjs/no-ignored-subscription": "error",
"rxjs/no-nested-subscribe": "error",
"rxjs/no-unbound-methods": "error",
"rxjs/throw-error": "error"
}
};
Alternatively, the recommended configuration can be used. This enables some of the rules that are likely to be relevant for most usages.
const { join } = require("path");
module.exports = {
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaVersion: 2019,
project: join(__dirname, "./tsconfig.json"),
sourceType: "module"
},
extends: ["plugin:rxjs/recommended"],
};
For Angular projects, there is another plugin (eslint-plugin-rxjs-angular) available.
ESLint in action
We are creating a task in the package.json to execute eslint for our project. Here we are only interested in a certain directory.
"lint": "npx eslint src/rxjs_eslint/**/*.ts"
This allows us to run the lint functionality and see the violations. If we would have a file like the following:
import { of } from 'rxjs';
// finnish notation violation
const obs = of(1);
// correct example
const finnish$ = of(1);
The rule "rxjs/finnish": "error"
sees an error and shows us the file and line where we violate the rule:
Deep dive into some rules
A full list of rules is available on the official GitHub page of the plugin. Nevertheless, I want to show some interesting ones in detail and explain which situations we can avoid thanks to them.
No nested subscribe
A subscribe within a subscribe is bad practice. It breaks the stream and can lead to memory leaks. It is also kind of imperative instead of the declarative way that RxJS nicely provides for us. Luckily eslint-plugin-rxjs offers a rule:
"rxjs/no-nested-subscribe": "error"
Like that, we would receive a reminder if a nested subscription appears in our code:
of(1).subscribe({
next: value => {
of(2).pipe(
map(inner => inner + value)
).subscribe(console.log)
}
});
Usually, we can handle such a situation by just pulling the operators up and flattening the higher-order Observable:
of(1).pipe(
concatMap(value => of(2)
.pipe(
map(inner => inner + value))
)
).subscribe(console.log);
Of course, also other higher-order Observable mapping operators than concatMap can be used.
No ignored subscription
When we subscribe in RxJS, we should also unsubscribe to stop the async operation or clean up. To unsubscribe we need at least to assign the subscription somewhere or use an operator like take that unsubscribes.
RxJS eslint offers the “no-ignored-subscription” rule that fires if a subscription is neither assigned to a variable or property nor passed to a function. The following code would violate the rule:
interval(1e3).pipe().subscribe(
(value) => console.log(value)
);
However, also using a take operator will still fire the rule. This is kind of okay because we don’t know whether the Observable will emit any value and ever reach the take operator the required times.
interval(1e3).pipe(take(1)).subscribe(
(value) => console.log(value)
);
Hence, it is better in my opinion to use the “no-ignored-subscription” just as a warning and not fail the linting completely. This can be configured in the .eslintrc.js file:
"rxjs/no-ignored-subscription": "warn"
No internal
The “no-internal” rule is again more obvious and straightforward. We do not want to rely on internals:
import { of } from 'rxjs/internal/observable/of';
import { of } from 'rxjs';
This results in an error because of the import on line 1.
1:20 error RxJS imports from internal are forbidden rxjs/no-internal
Preferable is the import on line 3.
Conclusion
Especially in larger code bases, linting is going to help ensure consistency. As always with rules a discussion should happen about which ones to use. Explaining which problems a certain rule is preventing, can help to get it in.
This post is part of the RxJS mastery series. As always you can find the code examples on GitHub.