Why Converting Observable to WritableSignal in Angular 16 throw error of missing properties

3.5k Views Asked by At

I have the following simple code on my component:

import {Component, effect, signal, WritableSignal} from '@angular/core';
import {AppService} from "../app.service";
import {toSignal} from "@angular/core/rxjs-interop";

@Component({
  selector: 'app-parent',
  templateUrl: './parent.component.html',
  styleUrls: ['./parent.component.css']
})
export class ParentComponent {

  translations: WritableSignal<{data: {}}> = signal({data: []});

  constructor( private appService: AppService) {
    this.translations = toSignal(this.appService.getTranslations());
    effect(() => {
      console.log('translation API:', this.translations());
    });
  }

  changeValue(): void {
    this.translations.set({data: {hello: 'hallo'}})

  }
}

FYI: this.appService.getTranslations() returns an observable

I'm trying out the new features released with Angular v16, and how to convert Observables to Signals.

What I wanted to do on the above code is, I change the value of the WritableSignal Object and log its value on change.

I'm getting the following error:

TS2739: Type 'Signal ' is missing the following properties from type 'WritableSignal{ data: {}; }>': set, update, mutate, asReadonly

Help please.

1

There are 1 best solutions below

0
On BEST ANSWER

Observables are "read-only", and thus unsuitable for a WritableSignal. You can subscribe to them, but not call a method like next to push new data.
Subjects could potentially be converted to a WritableSignal, but there is no utility method as far as I know.

What the error you get means is that the object Signal, which is returned by toSignal, is missing properties/methods from WritableSignal, all the "write" ones; Signal is the "read-only" version of WritableSignal.

For this case, you should subscribe to your observable, and set the signal in the callback.
If AppService uses HttpClient behind the scenes, no need to unsubscribe since it is done automatically on the first success callback.

constructor(private appService: AppService) {
  this.appService.getTranslations().subscribe(data => {
    this.translations.set(data)
  });
  effect(() => {
    console.log('translation API:', this.translations());
  });
}

If it's not using HttpClient, and you only want to retrieve the data one time on construction, use the first() RxJS operator.

constructor(private appService: AppService) {
  this.appService.getTranslations().pipe(first()).subscribe(data => {
    this.translations.set(data)
  });
  effect(() => {
    console.log('translation API:', this.translations());
  });
}

If it's not using HttpClient, and you need to get regular updates, you have to unsubscribe on destroy.
Since you are testing the Angular 16 developer preview, you can use the new DestroyRef for this rather than the OnDestroy.ngOnDestroy hook.

constructor(
  private appService: AppService,
  destroyRef: DestroyRef
) {
  const sub = this.appService.getTranslations().subscribe(data => {
    this.translations.set(data)
  });
  destroyRef.onDestroy(() => {
    sub.unsubscribe();
  });
  effect(() => {
    console.log('translation API:', this.translations());
  });
}

And finally, if you still want to use toSignal, your translations field has to be of type Signal. But then, your changeValue method will no longer work as Signal does not have a method set.