Reputation: 57986
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.
Upvotes: 1
Views: 85
Reputation: 20599
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.
Upvotes: 3
Reputation: 31
If you convert to use a linkedSignal
rather than a model
, then you can access the previous state of the signal and allow having undefined
as the initial state. And subsequently, no side effects like the PUT
inside of the signal effect
until there is a value.
Additionally, I think that a linkedSignal
is arguably a better signal primitive than a model
signal in this scenario for local state. The likes
was only a model
for the sake of local state, but otherwise as far as the sample code goes it is not being used to sync state in the parent or emit outputs to the parent. Therefore, a one way & immutable input
with an associated likesLocal
is a better fit.
export class Child {
// no longer local
likes: InputSignal<number> = input.required<number>();
// local state for this component
likesLocal = linkedSignal({
source: this.likes,
computation: (res, prev) => {
return prev ? res : undefined;
},
});
constructor() {
effect(() => {
const likesCount = this.likesLocal();
if (this.likesLocal() !== undefined) {
console.log(likesCount);
}
});
}
}
Upvotes: 2