question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Computed properties (model => model synchronisation)

See original GitHub issue

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

core

Description

Angular dirty-checking mechanism works well for synchronising (propagating) model changes to the DOM. However, there are legitimate situations where one would like to compute / derive a new model value (from the existing model values) before rendering. Angular has no such concept - even if requested by users (ex.: #20472).

Proposed solution

We don’t have any concrete solution proposal at the moment but we are exploring the space and will open a RFC when the solution space becomes clearer. But even without the concrete proposal on a table, there are certain properties that we would like to see in the final solution:

  • semantics of “recompute only if one of the dependencies changed”;
  • composability: it should be possible to construct of chain of computed values (computed depending on other computed);
  • great integration in the framework, ex.:
    • @Input - it should be possible to compute new model value based on input values changing over time;
    • convenient syntax for accessing computed values in a component / teamplate.

Alternatives considered

While Angular doesn’t have the dedicated concept of the computed properties, there are several existing mechanisms that can serve the purpose (although with different caveats). Here is an overview of the possible approaches that work today.

Use an external reactivity system

Many Angular users integrate RxJS, or similar push-based reactivity systems, into their template rendering pipeline. Most (all?) of the push-based reactivity systems have a concept of deriving values (ex. combinelatest in RxJS).

Use memoization

We could introduce a simple memo utility function. A memo function could just compare previous / new arguments and execute re-computation when needed, similar to what the pure pipe does today. Ex. (taken from #46135):

export class AppComponent {
  fruit = 'banana';
  idFruit = 0;

  getNameFruitUrl = memo((name: string) => `/fruits/${name}.jpg`);

  getIdFruitUrl = memo(() => `/fruits/${this.idFruit}.jpg`, () => [this.idFruit]);

  getLargeFruitUrl = memo((large?: boolean) => `/${large ? 'large' : 'small'}/${this.fruit}.jpg`,
    () => [this.fruit]
  );
}

Here is a working stackblitz: https://stackblitz.com/edit/angular-ivy-txzpmf?file=src%2Fapp%2Fapp.component.ts,src%2Fapp%2Fmemo.ts

Existing issues / PRs

The idea of computed properties was discussed in the past in the issue tracker - most notably in #20472 (which generated number of comments and ~70 upvotes).

Issue Analytics

  • State:open
  • Created a year ago
  • Reactions:25
  • Comments:8 (1 by maintainers)

github_iconTop GitHub Comments

4reactions
th0rcommented, Sep 27, 2022

getIdFruitUrl = memo(() => '/fruits/${this.idFruit}.jpg', () => [this.idFruit]);

I would strongly vote agains React’s useEffect-like manual list of dependencies - it’s a road to the dead-end as DX of this approach is terrible and this list is very difficult to keep in sync with the code in the memoized function.

Deps should be collected automatically just like in the majority of reactive frameworks/libs.

3reactions
maxime1992commented, Sep 27, 2022

In my opinion, having a decorator that turns an input into an observable would be fantastic.

It would:

  • Integrate very well with the rest of the API that uses observables (like router and http)
  • Help with zoneless CD. If everything is an observable, you know exactly when to trigger change detection. When the async pipe or equivalent (ngrxPush pipe and co) receive a value, you know a CD cycle is needed
  • Be easily composable, for example:
@Component(...)
class MyComponent {
  @ObservableInput('input1') input1$!: Observable<number>;

  public input1Updated$: Observable<number> = this.input1$.pipe(map(x => x * 2));
}

Or with a more realistic example:

@Component(...)
class MyComponent {
  @ObservableInput('currentUserId') currentUserId$!: Observable<UserId>;

  public currentUser$: Observable<User | null> = this.currentUserId$.pipe(
    startWith(null),
    switchMap((currentUserId) =>
      concatMap(
        of(null),
        this.http.get(`https://my-api/users/${currentUserId}`)
      )
    )
  );

  constructor(http: HttpClient) {}
}

The only point I can think of that’d need a bit more thinking would be how to get an equivalent of ngOnChanges because if you’ve got plenty of inputs, and during a CD cycle you change them all, you do not want to use all the individual observables with a combineLatest as it’d fire quite a few times. An easy workaround would be to use combineLatest but with a debounceTime for example but DX wouldn’t be that good.

And with this, would come the question of being type safe etc.

A possible alternative to that solution (for the previous example) would be something like:

@Component()
class MyComponent {
  @InputChanges() changes$!: Observable<{
    userId: UserId;
    prop2: any;
    prop3: any;
  }>;

  public currentUser$: Observable<User | null> = this.changes$.pipe(
    map((c) => c.userId),
    distinctUntilChanged(),
    startWith(null),
    switchMap((currentUserId) =>
      concatMap(
        of(null),
        this.http.get(`https://my-api/users/${currentUserId}`)
      )
    )
  );

  constructor(http: HttpClient) {}
}
Read more comments on GitHub >

github_iconTop Results From Across the Web

Lesson 16: Synchronize persistent and computed properties
Simple, you can use two properties, one calculated and one persistent, and synchronize both using JPA callback methods. You're going to learn how...
Read more >
Bindings, Observers, Computed Properties: What Do I Use ...
Use computed properties to build a new property by synthesizing other properties. · Observers should contain behavior that reacts to changes in another...
Read more >
Computed properties in Swift | Swift by Sundell
Properties are for data​​ So just like how stored properties make up the data that a type is storing, computed properties can be...
Read more >
Computed properties in Backbone - javascript - Stack Overflow
The idea is first to compute the attributes on model initialization, and second to let the model listen to its own changes and...
Read more >
The Guide to Promises in Computed Properties - Ember Igniter
Clients of the Recipe model will call vegetarianIngredients regardless of whether it's a real property (storage) or a computed property (computation).
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found