Search code examples
javascriptangulartypescriptrxjsangular-signals

Angular signals react to local state changes, but not to changes from input signal


I have this scenario, where the parent component will fetch user details and show then on the UI, code below:

@Component({
  selector: 'app-root',
  imports: [Child, FormsModule],
  template: `
    <div> User Details</div>
    @if(resource.value(); as user) {
      <div> Title: {{user.title}}</div>
      <app-like-button [likes]="user.id"/>
    }
    <button (click)="refresh()">Reload User Data</button>
  `,
})
export class App {
  http = inject(HttpClient);
  id = signal(1);
  resource: ResourceRef<UserDetails> = rxResource({
    request: () => this.id(),
    loader: ({ request: id }: any) =>
      this.http.get<UserDetails>(
        `https://jsonplaceholder.typicode.com/todos/${id}`
      ),
  });

  refresh() {
    this.resource.reload();
  }
}

Similarly I have a likes component which is responsible for showing the count and the like/dislike button.

@Component({
  selector: 'app-like-button',
  imports: [FormsModule],
  template: `
  <div> Child </div>
    <div>likes: {{likes()}}</div>
    <button (click)="like()">Like</button>
    <button (click)="unLike()">Dislike</button>
  `,
})
export class Child {

  // local state for this component
  likes: ModelSignal<number> = model.required<number>();

  constructor() {
    effect(() => {
      const likesCount = this.likes();
      // trigger an API call to save the like button press:
      console.log(likesCount);
    });
  }

  like() {
    this.likes.update((likesPrev: number) => likesPrev + 1);
  }

  unLike() {
    this.likes.update((likesPrev: number) => likesPrev - 1);
  }
}

In the below code, I have an effect which reacts to changes of the likes signal, by react I mean the changes are saved to the database using a put API call.

The problem is that, I am using an effect which reacts to likes signal changes. So during initialization the PUT API call is called, which is not necessary.

I would like the API to react to only local state updates of likes signal.

Stackblitz Demo

Expected result, is that API call ( represented by console.log) should not fire on initial load.


Solution

  • How about taking this into a different direction. Make the Child component a dumb, presentational component. In the parent component do whatever put you need to by registering a callback to the likesChange event on the Child.

    Child

    @Component({
      selector: 'app-like-button',
      imports: [FormsModule],
      template: `
      <div> Child </div>
        <div>likes: {{likes()}}</div>
        <button (click)="updateLikes(1)">Like</button>
        <button (click)="updateLikes(-1)">Dislike</button>
      `,
    })
    export class Child {
      readonly likes: ModelSignal<number> = model.required<number>();
    
      protected updateLikes(relativeChange: number): void {
        this.likes.update(x => x + relativeChange);
      }
    }
    

    Parent (template fragment)

    <app-like-button [likes]="user.likes" (likesChange)="updateLikes($event)" />
    

    Parent (component class fragment)

    protected updateLikes(likeCount: number): void {
      const user = untracked(this.resource.value);
      this.http.put(/* ... update likes ...*/);
    }
    

    By doing this, the Child component becomes much more flexible and reusable, and you've gotten around any hackiness from using an effect to handle the update. It also makes a certain amount of design sense that the class that loads the data would be the same to update the data.