Alex Rickabaugh demonstrated a reset pattern for Signals at TechStackNation, where changes in parent signals reset child signals.
This pattern relies on computed
signals for synchronous updates and could be further simplified by a new function expected in future version of Angular.
Alex Rickabaugh, the tech lead of the Angular framework, appeared at TechStackNation to showcase a reset pattern for Signals.
He demonstrated a scenario involving two groups of Signals—parents and children—where changes in the parent group reset the children, while changes in the children do not affect the parents.
Alex’s solution involved combining a computed
Signal with nested Signals to achieve this behavior.
This approach is also useful in cases like input
functions, where a read-only signal needs to be updated within a component but should still reflect any changes from the parent.
While an effect could achieve a similar result, computed signals offer synchronous updates, avoiding delays in signal synchronization caused by asynchronous effects.
An "effect-based" example would be:
interface Product {
id: number;
name: string;
}
@Component({
selector: 'app-basket',
template: `
<p>Selected Product: {{ product().name }}</p>
<input [(ngModel)]="amount" name="amount" />
<button mat-raised-button>Add to Basket</button>
`,
standalone: true,
imports: [FormsModule, MatButton, MatInput],
})
export class BasketComponent {
// parent signal
product = input.required<Product>(); // <-- parent signal
// child signal
amount = signal(0);
resetEffect = effect(() => {
this.product();
untracked(() => {
this.amount.set(0);
});
});
}
@Component({
selector: 'app-basket-container',
template: `
<div class="gap-x-2 mb-5 flex">
<button mat-raised-button (click)="previousProduct()" [disabled]="productIx() === 0">←</button>
<button mat-raised-button (click)="nextProduct()" [disabled]="productIx() >= products.length - 1">→</button>
</div>
<app-basket [product]="selectedProduct()"></app-basket>`,
standalone: true,
imports: [BasketComponent, MatButton],
})
export class BasketContainerComponent {
protected readonly products = [
{ id: 1, name: 'Apple' },
{ id: 2, name: 'Banana' },
{ id: 3, name: 'Orange' },
]
protected productIx = signal(0);
protected selectedProduct = computed(() => this.products[this.productIx()]);
nextProduct() {
this.productIx.update((value) => value + 1);
}
previousProduct() {
this.productIx.update((value) => value - 1);
}
}
And the "reset pattern" with computed
would require the following BasketComponent
:
@Component({
selector: 'app-basket',
template: `
<p>Selected Product: {{ product().name }}</p>
<input [(ngModel)]="state().amount" name="amount" />
<button mat-raised-button>Add to Basket</button>
`,
standalone: true,
imports: [FormsModule, MatButton, MatInput],
})
export class BasketComponent {
product = input.required<Product>();
state = computed(() => ({
product: this.product(),
amount: signal(0),
}));
}
According to Pawel Koszlowski, the Angular team is considering adding a new function for this pattern, potentially as soon as Angular 19.
Add WritableComputed to allow computed with write operations #55673
core
Revival with use case of #50498
Currently signal
produces a writable signal.
Currently computed
produces a non writable signal.
What I am missing is a writableComputed
.
Use case example:
I have a required input called value
but inside the component I can edit this value without affecting the outside. At the end an apply button is clicked and I emit the new value via an output.
Something like this:
class Test {
value = input.required<string>();
tempValue = signal(/*??*/);
commit = output<string>()
onEdit(newValue: string) {
this.tempValue.set(newValue);
}
onApply() {
this.commit.emit(this.tempValue());
}
}
The first problem is tempValue
isn't based on value
.
The second problem is when value
is changed from outside. At this point I wish to discard tempValue
and start fresh with the new value
.
If I had writableComputed
I could do:
tempValue = writableComputed(() => this.value());
Which means anytime value
changes it becomes the new tempValue
value. If tempValue
is edited the new edit is the tempValue
new value until something gets changed in value
.
A new writableComputed
which tracks the value inside based on the writeable part and will get reset each time the computed function runs to what it creates.
Examples:
const a = signal(7);
const b = writableComputed(() => a() + 1);
// b is 8
const a = signal(7);
const b = writableComputed(() => a() + 1);
b.set(9);
// A design question - 8 or 9
const a = signal(7);
const b = writableComputed(() => a() + 1);
a.set(11);
b.set(9);
// A design question - 12 or 9
const a = signal(7);
const b = writableComputed(() => a() + 1);
// b is 8
// Later
b.set(10)
// b is 10
// Later
a.set(20);
// b is 21
Using effect. The downside is with required inputs.
tempValue = signal<string | undefined>(undefined);
effect(() => this.tempValue.set(this.value()), {allowSignalWrites: true});
This will result in tempValue
having undefined
initially and for example in ngOnInit
it will still have undefined
although value
already have the actual value set. Also a computed will have its value set too.
using writableComputed
in ngOnInit
will have the correct computed value.
Another option is that scenario hints that something I am doing is wrong from the basis and I would like to hear better suggestions how to handle it.
Top comments (0)