DEV Community

Cover image for Episode 24/37: Signals Reset Pattern
ng-news for This is Angular

Posted on

Episode 24/37: Signals Reset Pattern

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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),
  }));
}

Enter fullscreen mode Exit fullscreen mode

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

Which @angular/* package(s) are relevant/related to the feature request?

core

Description

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());
  }
}
Enter fullscreen mode Exit fullscreen mode

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());
Enter fullscreen mode Exit fullscreen mode

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.

Proposed solution

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
Enter fullscreen mode Exit fullscreen mode
const a = signal(7);
const b = writableComputed(() => a() + 1);
b.set(9);
// A design question - 8 or 9
Enter fullscreen mode Exit fullscreen mode
const a = signal(7);
const b = writableComputed(() => a() + 1);
a.set(11);
b.set(9);
// A design question - 12 or 9
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode

Alternatives considered

Using effect. The downside is with required inputs.

tempValue = signal<string | undefined>(undefined);
effect(() => this.tempValue.set(this.value()), {allowSignalWrites: true});
Enter fullscreen mode Exit fullscreen mode

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)