At TechStackNation, Alex Rickabaugh showcased a Signals reset pattern that clears child signals whenever their parent signals change.
The approach leverages computed signals to perform updates synchronously, and an upcoming Angular release may introduce a new function to streamline it even further.
Alex Rickabaugh, who leads the Angular framework team, presented a reset pattern for Signals during his TechStackNation appearance.
His demo illustrated two Signal groups—parents and children—where parent changes trigger a reset of the children, but child updates leave the parent group untouched.
The technique he shared relies on a computed Signal that wraps nested Signals to implement this reset behavior.
This pattern proves valuable in scenarios such as input functions, where you need a read-only signal that updates internally yet remains in sync with incoming parent changes.
Despite effects being able to replicate the outcome, computed signals provide immediate updates, sidestepping the race conditions that can arise from effect-driven asynchronous synchronization.
An "effect-based" implementation would look like:
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);
}
}
Employing the "reset pattern" alongside computed means the BasketComponent must be set up as follows:
@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),
}));
}
Pawel Koszlowski mentioned that Angular's team may introduce a dedicated utility to handle this pattern, perhaps as early as Angular 19, though the details are still under discussion.
Add WritableComputed to allow computed with write operations
#55673
core
A revival of the use case outlined in #50498
Right now, signal gives you a writable signal, while computed yields a signal that cannot be written. What’s absent from the picture is a writableComputed.
Here’s the scenario that calls for it: a required input named value exists, yet the component should permit local edits without leaking them to the parent. Only when the user clicks an apply button does the component push the updated value through an output. The implementation would look along these lines:
class Test { value = input.required<string>(); tempValue = signal(/*??*/); commit = output<string>() onEdit(newValue: string) { this.tempValue.set(newValue); } onApply() { this.commit.emit(this.tempValue()); } }
A couple of issues arise here. To start with, tempValue doesn't derive from value. Then, if value gets updated externally, I'd want tempValue to be cleared out and reinitialize against the latest value. With a writableComputed in place, this is how it would look:
tempValue = writableComputed(() => this.value());
So whenever value is updated, tempValue takes on that new state as its own. If someone modifies tempValue, that change holds until value itself is altered, at which point the reset occurs.
Introducing writableComputed—a mechanism that observes the value from the writable component and resets it to the computed output each time that function executes. Illustration:
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
Leveraging effect is a possible path. The catch arises when inputs are mandatory.
tempValue = signal<string | undefined>(undefined); effect(() => this.tempValue.set(this.value()), {allowSignalWrites: true});
Consequently, tempValue starts out as undefined, and by the time ngOnInit runs, it remains undefined even though value has already been populated with the real value. At that same point, a computed signal will hold its computed value as well. Invoking writableComputed within ngOnInit will then retrieve the accurate computed result.
Alternatively, this scenario might indicate a fundamental flaw in my current approach, and I am interested in hearing alternative strategies to address it appropriately.
