I am trying to make a simple TODO app in angular using RXJS. I have a JSON server mock database with my TODO tasks.
So I ended up with this TasksService:
@Injectable({
providedIn: 'root'
})
export class TasksService
{
private _tasks : ITask[] = [];
private _tasks$: BehaviorSubject<ITask[]> = new BehaviorSubject<ITask[]>([]);
constructor (private _http: HttpClient) { }
public getTasks()
{
this.getTasksObservableFromDb().pipe(
tap(
(tasks) =>
{
this._tasks = tasks;
this._tasks$.next(tasks);
}
)
).subscribe();
return this._tasks$;
}
public addTask(task: ITask)
{
this._tasks.push(task);
this._tasks$.next(this._tasks);
}
private getTasksObservableFromDb(): Observable<any>
{
return this._http.get<any>('http://127.0.0.1:3000/tasks');
}
When I add task I don't want to post them to the server right away. So when I get my tasks from the server, I save them to the _tasks property and then pass them to the next() method for my _tasks$: BehaviorSubject. Because later I want to post my tasks to the server in bulk and now I just want them to display properly in Angular.
In my AppComponent I get my tasks and assign them to my tasks property.
export class AppComponent implements OnInit
{
public tasks!:BehaviorSubject<ITask[]>;
constructor (private _tasksService: TasksService)
{}
ngOnInit(): void
{
console.log('OnInit');
this.tasks = this._tasksService.getTasks();
}
public addTask()
{
this._tasksService.addTask(
{
id: crypto.randomUUID(),
isImportant: true,
text: 'Added task'
}
);
}
}
In my HTML template I use an async pipe for my tasks property and display my tasks:
<ng-container *ngFor="let task of tasks | async">
{{task.text}}
{{task.id}}
</ng-container>
<button type="button" (click)="addTask()">Add Task</button>
But later I accidentally deleted this line in my TaskService:
this._tasks$.next(this._tasks);
So my method now looks like this:
public addTask(task: ITask)
{
this._tasks.push(task);
}
But adding tasks still works! Angular displays newly added tasks even though I don't pass new task array for my BehaviorSubject.
So I decided to log values from my tasks! : BehaviorSubject<ITask[]> property in my AppComponent class:
public addTask()
{
this._tasksService.addTask(
{
id: crypto.randomUUID(),
isImportant: true,
text: 'Added task'
}
);
this.tasks.pipe(tap((value) => console.log(value)
)).subscribe();
}
And task is added as expected - every time a get an array with one more task:
Array(3) [ {…}, {…}, {…} ] <- Add task button is clicked
Array(4) [ {…}, {…}, {…}, {…} ] <- Add task button is clicked
Array(5) [ {…}, {…}, {…}, {…}, {…} ] <- Add task button is clicked
But when I return this line to my addTask method in TaskService:
this._tasks$.next(this._tasks);
I get these logs:
Array(3) [ {…}, {…}, {…} ] <- Add task button is clicked -> one task is added
Array(4) [ {…}, {…}, {…}, {…} ] <- Add task button is clicked -> one task is added
Array(4) [ {…}, {…}, {…}, {…} ] <- I get the same array
Array(5) [ {…}, {…}, {…}, {…}, {…} ] <- Add task button is clicked -> one task is added
Array(5) [ {…}, {…}, {…}, {…}, {…} ] <- I get the same array
Array(5) [ {…}, {…}, {…}, {…}, {…} ] <- I get the same array once again
So I am kinda lost why the observable behaves like this... Maybe I don't fully understand the next() method?
As I understand, you have two issues with this code. First, you don't know why you have duplicated console logs. Second, you don't know why your view is updated even though you didn't call ".next()" on your behaviour subject.
Let's start with the first one.
You need to understand how rxjs observables and behaviourSubjects work. Normal observable, when you subscribe to it, will wait for some value to be emitted, and then every time it happens, it will invoke an action that you attached to it. For example:
exampleSubject = new Subject<string>();
ngOnInit(): void {
this.exampleSubject.pipe(
tap(console.log)
).subscribe();
}
emitValue() {
this.exampleSubject.next("text");
}
Now notice that in this code, we subscribed only once in ngOnInit. Despite this, console.log will be invoked every time you call emitValue() method (for example from a button). This is because subscriptions lasts until they're unsubscribed. This means, that action will be invoked every time there's next() called on a subject.
So what happens when you subscribe multiple times? Let's try it:
exampleSubject = new Subject<string>();
ngOnInit(): void {
subscribeToSubject(); // 1st
subscribeToSubject(); // 2nd
subscribeToSubject(); // 3rd
this.emitValue();
}
emitValue() {
this.exampleSubject.next("text");
}
subscribeToSubject() {
this.exampleSubject.pipe(
tap(console.log)
).subscribe();
}
We subsribed to a subject 3 times, and now whenever value is emitted, console log will be called 3 times. It's called for every subscription you created.
Now when we have a look on your example, you add subscription every time you click on addTask button. That's why every time you add task, there's one more console log. But hey, in your first example, when you removed .next(), you had some console logs, even though you didn't emit any values, why is that? And here we come to BehaviourSubject. This is a special kind of subject that holds its value and will emit it the moment it's subscribed to. AND it also emits every time you call .next(), but you don't do it, so that's why it calls 1 console log every time you subscribe.
What you should have done was to call
this.tasks.pipe(tap((value) => console.log(value)
)).subscribe();
only once, for example in ngOnInit()
Alright, now let's get to the second issue
This is quite simple, but requires some knowledge on how references work.
In your service you put in BehaviourSubject the whole array of tasks. In fact, this subject holds reference to this array. This means, that whenever you push new value in array, BehaviourSubject will also have it. And that's what happens, whenever you call addTask method, tou push new value to tasks array, and your BehaviourSubject also has it.