Help needed for best practice combining singletons and aurelia store

I am using aurelia-store to give me important updates on changes on choosenCompanyId since this have important filtering implications.

but if i only listen to choosenCompanyId in example my dropdownlist custom-element and my singleton Data Class. i will get random timing issues due to both parts listening to the same command.

I Solved this by making the data class dispatch another action that the element is listening to. but i dont like this. since i already have to inject the class regardless. it feels redudant.

Want input on how to solve this more fluid

the actual singleton class

import {inject} from 'aurelia-framework'
import {Api} from "../Classes/Api";
import {distinctUntilChanged, pluck} from "rxjs/operators";
import {Store} from 'aurelia-store';


@inject(Api, Store)
import {autoinject} from 'aurelia-framework'
import {Api} from "../Classes/Api";
import {distinctUntilChanged, pluck} from "rxjs/operators";
import {Store} from 'aurelia-store';
import {TransactionCategoriesStore} from './stores/transactionCategoriesStore'


@autoinject()

export class TransactionCategories {
    promise;
    private promiseResolve: (value?: (PromiseLike<any> | any)) => void;
    private promiseReject: (reason?: any) => void;
    private api: Api;
    private store: Store<any>;
    private userState: any;
    private transactionCategoriesStore: TransactionCategoriesStore;

    constructor(api: Api, store: Store<any>, transactionCategoriesStore: TransactionCategoriesStore) {
        this.promise = new Promise((res, rej) => {
            this.promiseResolve = res;
            this.promiseReject = rej;
        });
        this.transactionCategoriesStore = transactionCategoriesStore
        this.api = api;
        this.store = store;


        this.store.state.pipe(pluck('UserSettings', 'choosenCompanyId'), distinctUntilChanged()).subscribe((newState) => {
            this.userState = newState;
            this.api.find("transactionCategories/")
                .then((result) => {
                    this.promiseResolve(result);
                    this.transactionCategoriesStore.transactionCategoriesUpdated(result);
                })
                .catch((error) => this.promiseReject(error));
           
        });
    }
    
    getPromise() {
        return this.promise;
    }
}

the customn-element

import { bindable, customElement, autoinject, observable } from 'aurelia-framework'
import { TransactionCategories } from "../../Classes/transactionCategories";
import { ILookupOptionsFunctionParameter } from "../../../nodeOverrides/materialize";
import {Store} from "aurelia-store";
import {distinctUntilChanged, pluck} from "rxjs/operators";
import {Subscription} from "rxjs";

@autoinject
@customElement('transaction-categories-lookup')
export class transactionCategoriesLookup {
    transactionCategoriesClass: TransactionCategories;
    p: ILookupOptionsFunctionParameter<string>;
    clearFilter: any;
    @bindable() value: string;
    @bindable() placeholder: string;
    @bindable() typeName: string;
    private filteredByTypeTransactionCategories: any;
    private store: Store<any>;
    private cats: any;
    private categorysub: Subscription;
        
    constructor(transactionCategories: TransactionCategories,store : Store<any>) {
        //even if we use aurelia store to listen to changes in transactionCategories values we need to inject the data class to ensure we tell aurelia that we need the class and then listen to actions dispatched by it
        this.transactionCategoriesClass = transactionCategories;
        this.store = store;
       
    }
    async attached(){
        //listening to aciton dispatched by TransactionCategoriesClass
        this.categorysub =  this.store.state.pipe(pluck('TransactionCategories'), distinctUntilChanged()).subscribe((newState) => {
            if(newState) {
                this.cats = newState;
                this.filterTransactionCategoriesByType();
            }
        });
    }
    async detached(){
        this.categorysub.unsubscribe()
    }
    async filterTransactionCategoriesByType() 
    {
        let filteredRows = this.cats.filter(c => c.TypeNameShort.toLowerCase().includes(this.typeName.toLowerCase()));
        if (filteredRows && filteredRows[0] && filteredRows[0].TransactionCategoryData)
        {
            this.filteredByTypeTransactionCategories = filteredRows[0].TransactionCategoryData;
        }
        else {
            this.filteredByTypeTransactionCategories = null;
        }
    }

    categoryOptionsFunction = async (p: ILookupOptionsFunctionParameter<string>) => {
        this.p = p;
        if (this.p.value) {
            return [this.p.value];
            //return  this.filteredByTypeTransactionCategories;
        } else {            
            if (this.p.filter) {
                //return await this.filteredByTypeTransactionCategories.filter(c => c.Name.toLowerCase().includes(p.filter.toLowerCase()));
                return  this.filteredByTypeTransactionCategories;
            }
            return this.filteredByTypeTransactionCategories;            
        }
    }
}
1 Like

I have to admit that I absolutely dont get what you’re after :slight_smile: the example is also way too complex with parts that arent related to the question like this.promise but adding more clutter. Could you try to build a minimalistic sample which you can share either via GitHub or codesandbox. Especially also try to point out exactly what that timing issue is that you’ve mentioned.

There are multiple things to note here but without the full idea what you’re exactly after its hard to propose anything.

The return of this.promise is for other classes or elements in the future to receive the data without having to do a new api call.

And in this example there is no timing issues since i am updating the data class first then dispatching an update to the store which the element is listening to.

my question is more in terms of. ok i am listening to the store now. but i still have to inject the data class into the element to make shure the dataclass is somehow booted up somewhere, it dosnt need to be this element. but say i have 10 different elements that need the same data . but u dont know which one needs it first. i dont want to initialise everything at the start of the application. i want on a need basis. but when it its loaded it is cached until somone changes the company Id.

  1. so the project used a dataclas with a simple promise to cache the data. then i realized i need to relead the cached data whenevern the companyId changes. hence thats why the dataclass is listening to choosenCompanyId. Still not a problem to just inject the dataclass wherever i want it.

  2. then comes my @customElement(‘transaction-categories-lookup’) that i am doing some postprocessing on the data. I kinda do realise in this moment when writing this. that i could probably get by just doing that filtering alsp in the categoryOptionsFunction which is what returns data to the lookup field. But lets entertain the ide that i want the this.filterTransactionCategoriesByType(); to be run whenever the data in the dataclass has changed. 1 solution is what i already done in the example. the dataclass dispatches a new event that the element is listening to.

Buuuut. this just feels a but redundant. i am both injecting the dataclass and listening to the store instead of getting the valyes directly just so that i can handle the timing. is there a smoother way of doing that. and keeping the dependency injection intact. one of the good benefits of DI is that you get stuff you need when u need it.

If I understand correctly, this is actually a fairly common design problem in SPAs and one that does not have a single (or easy) answer. It could be worth expanding upon with examples, but let’s distill the problem down to a few bullet points first:

  • There is server-side data which is needed by multiple components
  • This data is retrieved based on some ID selection which can be changed by user, but also may have some default value on initial navigation.
  • This data should only be loaded when needed by one or more components

The key moving part here is a singleton service / data store that aforementioned components (directly or indirectly) retrieve the data from. If multiple components request the data at the same time, only one request goes out and the others will await a cached promise.

To unify the logic for initial load with the logic for selection change, I tend to use the router. The selected id would be a parameter, and you would load the data in the (async) activate hook. This would also block rendering of child components that need the data and thus kind of solves multiple problems at once.

It doesn’t seem very intuitive to me to try to have a dropdown selection change going through the store and letting the store handle all of this infrastructure and application-level logic, but I don’t know the store that well. @zewa666 is there perhaps some neat way to pipe events into async operations or would you agree that this might be a job better suited for the router or some other non-store mechanism (like event aggregator)?

2 Likes

I think you are kinda spot on in your way of describing the problem. having that singleton service class makes life easy for the most part. but when i need it updated based on some changes in parts where i dont controll or atleast not part of the current viewmodel.

but at the moment the dropdown that chooses company resides outside the aurelia app. so i am just listening to a event from that dropdown. and then dispatching a store action from that. this way there is a very small interaction between the “old legacy” code and the new aurelia app. and when or of at all i will get time to move the dropdown and all that stuff into aurelia. it will be very easy to dispatch the same action. from the new element. not needing to change anything else.

and to clarify the store aproach is working for the most part splended. The real issue as i mentioned is how to handle the timings if not using the store.

example 1. component/elment A need to listen to companychange and update still not a problem you just connect the component/elment to the store and listen to changes on company. Good stuff no problem or ugly code.

example2. Comonent/Element B need to run some logic/update when Component/element A has updated becouse of some other event example like that company changed.

if i am using store i have to connect component/element B to the store. nice stuff no problems here.

But now comes that part that makes compining these two principle stupid.
To make shure that my singleton is booted up at some time. i have to inject component A into component B even if the data flow comes throught the store.

And maybe using store here is not the best way to do it at all. but what would be the better option to solve it. i dont think code example is needed here becouse this is more general principles then code implementaion.

1 Like

Also using the active method to load this data will make the viewmodel very thightly coupled with the classes caching the promise logic wise ?

1 Like