Search code examples
angularrxjsbehaviorsubject

AddTodo method in service not working when called from component


I have a todo application that is using the Subject as a Service approach to state management.

My todo service looks like this

import { Injectable } from '@angular/core';
import { Todo } from './todo.model';
import { HttpClient } from '@angular/common/http';
import { Observable, BehaviorSubject, map, pipe } from 'rxjs';

@Injectable()
export class TodosService {
  private todos$ = new BehaviorSubject<Todo[]>([]);
  constructor(private http: HttpClient) {
    this.http
      .get<Todo[]>('https://jsonplaceholder.typicode.com/users/1/todos')
      .subscribe((todos) => {
        this.todos$.next(todos);
      });
  }

  public getTodos(): Observable<Todo[]> {
    return this.todos$;
  }

  public getCompletedTodos(): Observable<Todo[]> {
    return this.todos$.pipe(
      map((todos) => todos.filter((todo) => todo.completed))
    );
  }

  public addTodo(event: string) {
    const newArray: Todo[] = [
      ...this.todos$.value,
      {
        userId: 1,
        id: this.todos$.value.length,
        title: event,
        completed: false,
      },
    ];
    this.todos$.next(newArray);
  }
}

And my add-todo component looks likes this.

import 'zone.js/dist/zone';
import { Component } from '@angular/core';
import { TodosService } from './todo.service';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-add-todo',
  standalone: true,
  imports: [CommonModule, FormsModule],
  providers: [TodosService],
  template: `
    <label>Add Todo</label>
    <input type="text" [(ngModel)]="todo" />
    <button (click)="addNewTodo()">Submit</button>
  `,
})
export class AddTodo {
  todo: string = '';
  constructor(private todoService: TodosService) {}

  addNewTodo() {
    this.todoService.addTodo(this.todo);
    this.todo = '';
  }

  ngOnInit() {}
}

I know that my addTodo method is being called by the component because I can console log the newArray value and see it when I try to add a new one. But for some reason my new todo isn't being displayed.

What's weirder is that this works fine when I call addTodo inside the constructor of my service. When done this way, my component that displays the todos updates properly.

  constructor(private http: HttpClient) {
    this.http
      .get<Todo[]>('https://jsonplaceholder.typicode.com/users/1/todos')
      .subscribe((todos) => {
        this.todos$.next(todos);
      });

      setTimeout(()=> {
        this.addTodo('Todo from inside setTimeout')
      }, 3000)
  }

Todo App

So why does my list-todo component not update when addTodo is called from a component, rather than inside the service itself?

I've created a stackblitz demo


Solution

  • As mentioned by @BizzyBob in the comments, if you provide TodosService on component-level, each component will get their own instance of a TodosService and there will be no shared states between them.

    In your case you can solve this by providing TodosService inside your App component in your main.ts and removing the individual provider arrays from your standalone components.

    Your App component will look like:

    @Component({
      selector: 'my-app',
      standalone: true,
      imports: [
        CommonModule,
        TodoList,
        TodoListSubscribe,
        HttpClientModule,
        AddTodo,
      ],
      providers: [TodosService],
      template: `
        <app-todo-list></app-todo-list>
        <app-add-todo></app-add-todo>
      `,
    })
    export class App {}