angularcomputed-propertiesangular-signals

Angular Signals and Computed Properties


I am having a hard time wrapping my head around the concept of Signals and Computed Properties. I have some dummy data, dummyTasks, that is a simple array of task objects, and a selectedUserTasks property that represents the tasks of a selected user. So, I just don't understand how my code works when I make dummyTasks a signal. When I leave it as a simple property-binding value, deleting a task doesn't work. I would like to know why. All answers are appreciated. Thanks. Here is my code:

export class TasksComponent {
  selectedUser = input<User>()
  selectedUserId = input.required<string>()
  selectedUserTasks = computed(() => (
    this.dummyTasks.filter((task) => this.selectedUserId() === task.userId)
  ))

  dummyTasks = [
    {
      id: 't1',
      userId: 'u1',
      title: 'Master Angular',
      summary:
        'Learn all the basic and advanced features of Angular & how to apply them.',
      dueDate: '2025-12-31',
    },
    {
      id: 't2',
      userId: 'u3',
      title: 'Build first prototype',
      summary: 'Build a first prototype of the online shop website',
      dueDate: '2024-05-31',
    },
    {
      id: 't3',
      userId: 'u3',
      title: 'Prepare issue template',
      summary:
        'Prepare and describe an issue template which will help with project management',
      dueDate: '2024-06-15',
    },
  ]

  onCompleteTask(id: string) {    
    this.dummyTasks.set(this.dummyTasks().filter((task) => task.id !== id))
  }
}

When I initialize dummyTasks as a basic property-binding property, not as a signal, and deleting a task, or in this case, simply filtering out the task, does not work:

onCompleteTask(id: string) {    
    this.dummyTasks = this.dummyTasks.filter((task) => task.id !== id)
  }

Solution

  • On its first read, a computed signal will keep track of all the signals that were called from its function. On the next read, the following will happen:

    In your case selectedUserTasks is getting updated only because selectedUserId() keeps getting called inside the filter method. Changes to dummyTasks have no effect since it is not a signal and overlooked in the view if OnPush change detection is used.

    It's important to note that had dummyTasks been empty, selectedUserTasks would never be anything but an empty array since selectedUserId would not have been called in the filter function.

    Here's a better way this could have been written:

    export class TasksComponent {
      selectedUser = input<User>()
      selectedUserId = input.required<string>()
      selectedUserTasks = computed(() => 
        this.dummyTasks().filter((task) => this.selectedUserId() === task.userId)
      });
    
      dummyTasks = signal([
        {
          id: 't1',
          userId: 'u1',
          title: 'Master Angular',
          summary:
            'Learn all the basic and advanced features of Angular & how to apply them.',
          dueDate: '2025-12-31',
        },
        /* ... more tasks ...*/
      ]);
    
      onCompleteTask(id: string) {    
        this.dummyTasks.update(x => x.filter((task) => task.id !== id));
      }
    }
    

    Now, whenever *dummyTasks changes, selectedUserTasks will change as well. The value of selectedUserId may or may not cause selectedUserTasks to change. This is because if dummyTasks has 0 elements, the value of selectedUserId won't get read. That's okay though since its value isn't necessary since filtering an empty array will always be an empty array.

    Here's a good rule of thumb: If you're using OnPush change detection or going zone-free, then any value accessed in your template should only come from a Signal, Observable with AsyncPipe, or template variable.