Thanks to visit codestin.com
Credit goes to github.com

Skip to content

adding ability to define computed properties easily #1175

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
shairez opened this issue Jan 31, 2022 · 7 comments
Open

adding ability to define computed properties easily #1175

shairez opened this issue Jan 31, 2022 · 7 comments
Assignees

Comments

@shairez
Copy link

shairez commented Jan 31, 2022

Problem Solved By The Feature

I want to be able to define computed properties in an easier way

For example, currently I have to do this:

this.state.connect(this.state.select(selectSlice(['sort', 'products'])),
            (state, { sort, products }) => {
                return {
                    sortedProducts: products.sort(sort)
            };
        });

(maybe I'm doing it wrong and there's a better way...)

Solution

I'd like to have something like this:

// POSSIBLE OPTIONS: 

this.state.compute('nameOfComputedProp', ['prop1', 'prop2'], ({prop1, prop2}) => prop1 + prop2);

this.state.compute('nameOfComputedProp', ['prop1', 'prop2'], (state, {prop1, prop2}) => prop1 + prop2);

this.state.compute('nameOfComputedProp', (state) => state.prop1 + state.prop2);

alternatives Considered

Maybe an operator?
But I think it's a quite common use case to justify a method

Additional Context

I don't mind creating a PR for that if you approve the design

@BioPhoton
Copy link
Member

BioPhoton commented Jan 31, 2022

Hi! Thanks for the issue!

Did you check the overloads?

E.g. we have:

this.state.connect('nameOfComputedProp', value$, (state, value) => state.prop1 + value);

Is it helpful?

@shairez
Copy link
Author

shairez commented Jan 31, 2022

Thanks!
Yeah I saw that option, (I believe this is what I'm using in my example of the "problem")

The distinction in my case is that the external observable value$ in my case are "internal observables" which is why I used the "selectSlice" on this.state.select

I want to compute a property from other properties on the same component state.

@push-based-bot
Copy link

push-based-bot commented Jan 31, 2022

What about a operator like this?

vm$ = selectComputed(
   this.state,
   'nameOfComputedProp', ['prop1', 'prop2'], (state, {prop1, prop2}) => prop1 + prop2
 );

@BioPhoton
Copy link
Member

BioPhoton commented Feb 1, 2022

Problem

interface Model {
selectedProductId: number;
sort: boolean;
products: any[];
sortedProducts: any[];
}

this.state = new RxState<ViewModel>();

this.state.connect(this.state.select(selectSlice(['sort', 'products'])),
            (state, { sort, products }) => {
                return {
                    sortedProducts: products.sort(sort)
            };
        });

interface ViewModel {
selectedProductId: number;
sort: boolean;
sortedProducts: any[];
}

vm$ = this.state.select(selectSlice(['selectedProductId', 'sort', 'sortedProducts']));
<ng-conainer *rxLet="vm$; let vm">
<b>Selected Id: {{vm.selectedProductId}}</b>
<button (click)="sort()">
{{vm.sort ? 'ASC' : 'DSC'}}
<button>
<ul>
    <li *ngFor="let product of vm.sortedProducts">{{product}}</li>
</ul>
</ng-container>

The conceptual problem I see here is we store derived state directly instead of computing it.

sortedProducts is a derivation of sort and list. So if we store products also in the state instead of just computing it we trigger additional state updates.

If we look at it from the 2 perspectives DX and performance we can say:

DX
The benefit of your solution is to have one place where you keep all the ViewModel data vm$.

We already thought about this operator here in a different way here:

https://github.com/rx-angular/rx-angular/pull/582/files#diff-709947f608ed9256ca5ec83d8522af544187476861987ce0ab41715ead09c5d4

So the above suggested selectComputed would be an extension of this one and keep the vm data in one place and therefore favors a minimal setup with one vm variable and one rxLet in the template.

Performance

From a performance perspective, we can consider 3 major things here:

  • memory
  • scripting
  • rendering

As we store the sorted list also in the state with connect we hold them 2 times in our memory even if nobody is consuming the sorted list.

We could avoid sorting too often by implementing some distinction logic to avoid:

  • over emission by using coalescing techniques
  • over computation be distinctUntilChanged etc
  • sharing the computation for multiple subscribers

but the impact would not be that much compared to what we can do in the template only the sharing part is important in the component class.

The rendering performance is most impactful. In the push pipe and rxLet directive we avoid already:

  • over emission by using coalescing techniques
  • over computation be distinctUntilChanged etc
  • not respecting undefined's for lazy template creation

In addition *rxLet only updates a template snipped, not the whole template. So multiple *rxLet chunks the render work in smaller pieces.


With this in mind, I would avoid having additional logic in the core of RxState and implement selectComputed as top-level API, But I do see the downside of passing this.state as an argument!

Let me know if my example above is correct or if you would like to have products also unsorted in the template. This would need an adoption then.

@shairez
Copy link
Author

shairez commented Feb 1, 2022

Wow, first of all great job @BioPhoton of summarizing our conversation from yesterday

Few things I thought about -

  1. I think it shouldn't be called selectComputed because we are actually "assigning" it on the vm and not just querying.
    so maybe something like setComputed or connectComputed (to be consistent with the current API).

  2. I don't mind it being a separate function and not part of the core (unless most of the people need it, and then we should consider folding it to the core as a method).

  3. If I need to "unsort" then yeah... maybe we need to figure out how to pass "undefined" as a value to unsort it, currently I believe it wouldn't work because of the filtering right?

@BioPhoton
Copy link
Member

I found the old PR we discussed in the call:

Opened it here for you to check: #1192

Also added some docs on that area here:
https://github.com/rx-angular/rx-angular/blob/d2d52ff89bc749c979d6583b860f4c497c43c2a3/libs/state/selections/docs/Readme.md

WDYS?

@BioPhoton
Copy link
Member

BioPhoton commented Mar 9, 2022

So I had more time to progress with docs on the new operator.

With the old PR the implementation only for works for { prop: 42, prop2 = number$}.
Not adding multiple additional slices. (slice1$, slice2$)

  
  // ✔ Target API suggestion  👇
  viewModel$ = smosh({ 
    prop1: 'prop1', // string
    prop2: prop2$ // Observable<'prop2'>
    }, 
    slice1$, // Observable<{prop3: 3}>
    slice2$  // Observable<{prop4: 'four'}>,
    // durationSelector$ (optional) => test DX
  );

  // Result: 
  // Observable<{
  //   prop1: 'prop1',
  //   prop2: 'prop2',
  //   prop3: 3,
  //   prop4: 'four'
  // }>

Let me know how you want to proceed.

Here is the new explanation of the problem:

Advanced derivation architecture

Goal Architecture

frontend--micro-glue_michael-hladky

rx-angular--state--selections--overview-diagram--michael-hladky

The problem

We have the following state sources to manage:

  • the list of products received form global state - Product[]
  • the title of the list including it's number of children computen in the component class - string
  • the sort direction triggered over a UI element click - boolean

A setup of the compoents class based on RxState could look like this:

@Component({
  selector: 'app-problem',
  template: `
    <ng-container *rxLet="viewModel$; let vm">
      <h1>{{vm.title}} - {{vm.sortDirection}}</h1>
      <ul>
        <li *ngFor="let item of vm.sortedList">{{item}}</li>
      </ul>
    </ng-container>
    `,
  providers: [RxState],
})
export class ProblemComponent {
  
  viewModel$: Observable<ViewModel>; // ???

  constructor(private globalState: GlobalState, private state: RxState<Model>) {
    this.state.connect('title', this.globalState.title$);
    this.state.connect('products', this.globalState.products$);
  }
  
  toggleSort() {
    this.state.set('sort', ({sort}) => !sort))
  }
}

In a components template we want to render the the UI for the above explained view model SelectionScreen1.

interface SelectionScreen1 {
  title: string;
  sortDirection: 'asc' | 'desc' | 'none';
  sortedList: Array<{ id: number }>
}

A common implementations looks like this:

// template removed for brevity
export class ProblemComponent {
  
  private sortedList$ = this.state.select(
      selectSlice(['sortDirection', 'list']),
      map(() => {
         // sort `list` by `sortDirection` to `sortedList` here
        return sortedList;
      })
  );
  
  viewModel$ = this.state.select(
    selectSlice(['title', 'sortedList', 'sortDirection'])
  )

  //                                                    ❌ BAD: modle viewmodel mix up 👇
  constructor(private globalState: GlobalState, private state: RxState<Model & Pick<ViewModel, 'sortedList'>>) {
    // ...
    
    // ❌ BAD: store derived state 👇
    this.state.connect('sortedList', this.sortedList$);
  }
  
  // ...
}

Selections (6)

By removing the sorted list form the state and moving it into the selection
we can clean up the state's typing and have a nice separation of which data is owned by the component (model) and which data is owned by the template (view model)

// template removed for brevity
export class ProblemComponent {
  
  private sortedSlice$ = this.state.select(
      selectSlice(['sortDirection', 'list']),
      map(({list, sortDirection}) => {
        // sort `list` by `sortDirection` to `sortedList` here
        return { sortDirection, sortedList };
      })
  );
  
  // ✔ GOOD: Derive view model from model 👇
  viewModel$ = smosh({ title: this.state.select('title')}, this.sortedSlice$);

  
  // ✔ GOOD: Derive view model from model 👇
  viewModel$ = smosh({ 
      title: this.state.select('title'),
      ...this.sortedSlice$
  });



  constructor(private globalState: GlobalState, private state: RxState<Model>) {
    // ...
    
  }
  
  // ...
}

Selections (7)

@BioPhoton BioPhoton self-assigned this Mar 9, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants