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.
Expected result, is that API call ( represented by console.log
) should not fire on initial load.
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.
@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);
}
}
<app-like-button [likes]="user.likes" (likesChange)="updateLikes($event)" />
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.