Debugging page
Learn how to debug RxJS with the tap operator.
Video
Who has time to read? This video covers the content on this page. Watch fullscreen.
The problem
In this section, we will:
- Learn how to debug RxJS observables.
- Log the value of the
this.cardNumber$observable everytime it changes.
How to solve this problem
- Create a
loghelper thatconsole.logs emitted values without causing side-effects. - Use the
loghelper to log values emitted bythis.cardNumber$.
What you need to know
RxJS can be tricky to debug. It seems like you should simply be able to subscribe to an observable and output its values.
The problem with this is that:
- Subscribing to an observable changes the state of the observable.
- Observables (in contrast to Subjects) run their initialization code every time there is a new subscriber.
Many times you want to subscribe to an intermediate observable to see its value.
The following example creates:
randomNumbersto emit random numbers.floats0to100to emit the random numbers multiplied by 100.ints0to100to emit the multiplied numbers rounded to the nearest integer.
If you subscribe to floats0to100 to see its values, you will notice
that the float values do not match the int values!!
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/7.4.0/rxjs.umd.min.js"></script>
<script type="typescript">
const { Observable } = rxjs;
const { map } = rxjs.operators;
const randomNumbers = new Observable((observer) => {
observer.next(Math.random());
observer.next(Math.random());
observer.next(Math.random());
});
// Operators
const toTimes100 = map((value) => value * 100);
const toRound = map(Math.round);
const floats0to100 = randomNumbers.pipe(toTimes100);
//floats0to100.subscribe((value) => {
// console.log("float", value);
//});
const ints0to100 = floats0to100.pipe(toRound);
ints0to100.subscribe((value) => {
console.log("int", value);
});
</script>
The tap operator allows you to perform a side-effect (such as logging) on every emission on a source observable.
The following uses tap to log floats0to100 values so they match:
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/7.4.0/rxjs.umd.min.js"></script>
<script type="typescript">
const { Observable } = rxjs;
const { map, tap } = rxjs.operators;
const randomNumbers = new Observable((observer) => {
observer.next(Math.random());
observer.next(Math.random());
observer.next(Math.random());
});
// Operators
const toTimes100 = map((value) => value * 100);
const toRound = map(Math.round);
const logFloats = tap((value) => console.log("float", value));
const floats0to100 = randomNumbers.pipe(toTimes100).pipe(logFloats);
const ints0to100 = floats0to100.pipe(toRound);
ints0to100.subscribe((value) => {
console.log("int", value);
});
</script>
We can generalize this pattern with a log operator like:
const log = (name) => {
return tap((value) => console.log(name, value));
};
log can be used as follows:
const number = source.pipe(mapToNumber).pipe(log('number'));
NOTE 1: Notice that to log
number, we call.pipe(log(...))on what would be thenumberobservable.
NOTE 2: The solution will log
cardNumbertwice. That’s expected because there are two subscriptions oncardNumber:
- one directly from
cardNumber$in the template -{{ cardNumber$ | async }}- the other from
cardError$in the template -{{ cardError$ | async }}-cardErrorderives fromcardNumber.
The solution
Click to see the solution
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/7.4.0/rxjs.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/core-js/3.19.0/core.js"></script>
<script src="https://unpkg.com/@angular/core@12.2.16/bundles/core.umd.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/zone.js/0.11.4/zone.min.js"></script>
<script src="https://unpkg.com/@angular/common@12.2.16/bundles/common.umd.js"></script>
<script src="https://unpkg.com/@angular/compiler@12.2.16/bundles/compiler.umd.js"></script>
<script src="https://unpkg.com/@angular/platform-browser@12.2.16/bundles/platform-browser.umd.js"></script>
<script src="https://unpkg.com/@angular/platform-browser-dynamic@12.2.16/bundles/platform-browser-dynamic.umd.js"></script>
<my-app></my-app>
<script type="typescript">
// app.js
const { Component, VERSION } = ng.core;
const { BehaviorSubject } = rxjs;
const { map, tap } = rxjs.operators;
const cleanCardNumber = map((card) => {
if (card) {
return card.replace(/[\s-]/g, "");
}
});
const validateCard = map((card) => {
if (!card) {
return "There is no card";
}
if (card.length !== 16) {
return "There should be 16 characters in a card";
}
});
const log = (name) => {
return tap((value) => console.log(name, value));
};
@Component({
selector: 'my-app',
template: `
<form>
<div class="message">{{ cardError$ | async }}</div>
<input
type="text"
name="cardNumber"
placeholder="Card Number"
(input)="userCardNumber$.next($event.target.value)"
/>
<input type="text" name="expiry" placeholder="MM-YY" />
<input type="text" name="cvc" placeholder="CVC" />
<button>
PAY
</button>
</form>
UserCardNumber: {{ userCardNumber$ | async }} <br />
CardNumber: {{ cardNumber$ | async }} <br />
`
})
class AppComponent {
userCardNumber$ = new BehaviorSubject<string>();
constructor() {
this.cardNumber$ = this.userCardNumber$
.pipe(cleanCardNumber)
.pipe(log("cardNumber"));
this.cardError$ = this.cardNumber$.pipe(validateCard);
}
}
// main.js
const { BrowserModule } = ng.platformBrowser;
const { NgModule } = ng.core;
const { CommonModule } = ng.common;
@NgModule({
imports: [
BrowserModule,
CommonModule,
],
declarations: [AppComponent],
bootstrap: [AppComponent],
providers: []
})
class AppModule {}
const { platformBrowserDynamic } = ng.platformBrowserDynamic;
platformBrowserDynamic()
.bootstrapModule(AppModule)
.catch(err => console.error(err));
</script>
<style>
@import url('https://fonts.googleapis.com/css?family=Raleway:400,500');
body {
background-color: rgba(8, 211, 67, 0.3);
padding: 2%;
font-family: 'Raleway', sans-serif;
font-size: 1em;
}
input {
display: block;
width: 100%;
box-sizing: border-box;
font-size: 1em;
font-family: 'Raleway', sans-serif;
font-weight: 500;
padding: 12px;
border: 1px solid #ccc;
outline-color: white;
transition: background-color 0.5s ease;
transition: outline-color 0.5s ease;
}
input[name='cardNumber'] {
border-bottom: 0;
}
input[name='expiry'],
input[name='cvc'] {
width: 50%;
}
input[name='expiry'] {
float: left;
border-right: 0;
}
input::placeholder {
color: #999;
font-weight: 400;
}
input:focus {
background-color: rgba(130, 245, 249, 0.1);
outline-color: #82f5f9;
}
input.is-error {
background-color: rgba(250, 55, 55, 0.1);
}
input.is-error:focus {
outline-color: #ffbdbd;
}
button {
font-size: 1em;
font-family: 'Raleway', sans-serif;
background-color: #08d343;
border: 0;
box-shadow: 0px 1px 3px 1px rgba(51, 51, 51, 0.16);
color: white;
font-weight: 500;
letter-spacing: 1px;
margin-top: 30px;
padding: 12px;
text-transform: uppercase;
width: 100%;
}
button:disabled {
opacity: 0.4;
background-color: #999999;
}
form {
background-color: white;
box-shadow: 0px 17px 22px 1px rgba(51, 51, 51, 0.16);
padding: 40px;
margin: 0 auto;
max-width: 500px;
}
.message {
margin-bottom: 20px;
color: #fa3737;
}
</style>