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
log
helper thatconsole.log
s emitted values without causing side-effects. - Use the
log
helper 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:
randomNumbers
to emit random numbers.floats0to100
to emit the random numbers multiplied by 100.ints0to100
to 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 thenumber
observable.
NOTE 2: The solution will log
cardNumber
twice. 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 }}
-cardError
derives 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>