79694837

Date: 2025-07-08 21:21:06
Score: 2
Natty:
Report link

Before signals, observables where the best (only ?) way to implement Reactive CRUD on data. now we have Signals, and I would recommend to use them for CRUDing data.

-- It's not my intent to ditch Observables, they are still useful for other scenarios --

There are some particularly useful Angular APIs that you might want to use

resource, rxResource and httpResource

resource (and their derivatives) expose signals called value , error and isLoading which emit what their names suggest, this is really useful, as managing data usually need these 3 state sources.

I will not explain how to use them here, they are explained in Angular docs and any attempt to explain them here would be incomplete, but this article pretty much summarizes the mental model of moving from rxjs based CRUD into signals

https://medium.com/@robmanganelly/simplifying-crud-with-angulars-new-reactive-apis-c0277a001f2f

you might be able to reuse the code examples there to implement your User service.

If you insist to use observables you have other apis that can turn signals into observables, (people usually go the other way around, but in some cases you need to preserve observables for backward compatibility.

toObservable(someSignal) will give you the desired signature, and the article above have an example for making stateful CRUD with observables.

My only recommendation is that no matter the API you use (rxjs or signals) stick to best practices and design principles.

Check this code for examples of implementation (taken from mentioned article)

export type Cat = {
  age: number;
  breed: string;
  color: string;
  name: string;
  id: string;
};

export type CatParams = Omit<Cat, 'id'>;

@Injectable({
  providedIn: 'root',
})
export class CatsService {
  private readonly http = inject(HttpClient);

  private readonly apiCats = '/api/v1/cats';

  private readonly catParams = signal<Params>({});

  private readonly catResource = rxResource({
    // request changed to 'params' in v20
    request: () => this.catParams(),
    defaultValue: [],
    loader: ({ request }) => this.getCats$(request), 
  });

  private getCats$(params: Params) {
    return this.http
      .get<Cat[]>(this.apiCats, { params })
      .pipe(map((cats) => cats.map((c) => this.transformCat(c))));
  }

  private postCat$(payload: CatParams) {
    return this.http
      .post<Cat>(this.apiCats, payload)
      .pipe(map((cat) => this.transformCat(cat)));
  }

  private putCat$(payload: Partial<CatParams>) {
    return this.http
      .put<Cat>(this.apiCats, payload)
      .pipe(map((cat) => this.transformCat(cat)));
  }

  private deleteCat(id: string) {
    return this.http.delete(`${this.apiCats}/${id}`);
  }

  private transformCat(cat: Cat) {
    return { ...cat };
  }

  private sanitizeParams(params: Partial<CatParams>): Params {
    const p: Params = {};
    for (const k in params) {
      const v = params[k as keyof CatParams];
      if (v !== undefined && v !== null && v !== '') {
        p[k] = v;
      }
    }
    return p;
  }

  // public api of the service

  // asReadonly prevents assigning to the cats value 
  // without using the service interface
  readonly cats = this.catResource.value.asReadonly(); 

  readonly loading = this.catResource.isLoading;

  readonly error = this.catResource.error;

  searchCats(params: Partial<CatParams>) {
    const p = this.sanitizeParams(params);
    this.catParams.set(p);
  }

  async createCat(payload: CatParams) {
    const n = await firstValueFrom(this.postCat$(payload));
    this.catResource.reload();
    return n;
  }

  async updateCat(payload: Partial<CatParams>) {
    const u = await firstValueFrom(this.putCat$(payload));
    this.catResource.reload();
    return u;
  }

  async deleteCatById(id: string) {
    await firstValueFrom(this.deleteCat(id));
    this.catResource.reload();
  }
}
Reasons:
  • Blacklisted phrase (1): this article
  • Blacklisted phrase (0.5): medium.com
  • Contains signature (1):
  • Long answer (-1):
  • Has code block (-0.5):
  • Contains question mark (0.5):
  • Low reputation (0.5):
Posted by: Rob