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?
2
Answers
You get the same values in console because you add a new subscription each time when you click button. Move the subscription to a constructor or OnInit.
When you fix it you will ask why you get the new message to screen but there is not in console. This is because when you this._tasks$.next(tasks) in getTasksObservableFromDb you send memory link to the same tasks array and this is enough to display the messages on the screen when the array is changed. but it’s not enough to the behaviorsubject know that the object was changed. To avoid it you should point a new array with help this._tasks$.next([…tasks]) (in getTasksObservableFromDb).
After this you code will not work without the next.
Full example https://stackblitz.com/edit/angular-vsrfhx?file=src/tasks.service.ts
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:
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:
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
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.