A while ago we had a case when a business logic service (store) needed multiple instances created dynamically.
The service responsibility is handling a server based aggregation - it has quite a bit of logic. Currently it handles a single aggregation and using injection tokens we can create multiple instances of that service when needed for different aggregations across the app (known statically).
The problem came up when we needed an instance of this service for an unknown amount of aggregations decided at runtime. The case was a grid with dynamic columns based on user decision and each column can show an aggregation - so each should have its own service.
How can I do that? Seems services can't be created dynamically and destroyed dynamically. The only options I thought about are either create the service without DI (new) or duplicate the logic for a list of aggregations while retaining the single one (using rx groupBy and other complications).
#Dynamic services create and destroy
303 messages Β· Page 1 of 1 (latest)
Services can be created and destroyed per component. Depending how your app is structured that could be a way as well.
@cedar pasture ye I know. Unfortunately those services are not connected to a component
Is it a bad idea to create the service without DI? Or maybe there is a better way to achieve this behavior. Considering the service state is global - what's the best way to design it?
I guess it would help to understand what needs them to be created dynamically ?
@cedar pasture I wrote that actually but will rephrase it.
I need to dynamically create the service with rxjs server call and state for every column the user clicked (all retained and not component based). The columns are dynamic and based on user decision so they can't be determined statically.
I could use an entity (ngrx style) but then I need to implement the multiple case and the single case twice. In addition the multiple case requires grouping server calls and instance ids and it becomes a bit of a mess
But I dont think I understand why the services need to be created dynamically.
What do the services do ?
Handle the state and server calls. Each column has its own different instance of the column values aggregation
And the columns or not statically known and those aggregations need to live in parallel to each other (server calls and state)
if i understand correctly, your columns are created dynamically. Assuming a column has a component, it can provide a service. In that case the service is created and destroyed all together with its column component dynamically.
That is exactly what I meant before, so I would indeed try and get a setup like that.
I am not sure I get what the service does, it's all very vague.
But it does read as if every column should be a component.
If its not, that should probably be the first step if u ask me
@severe pier @cedar pasture unfortunately I can't as the state should persist even if I navigate to another page and back. Using a component provider it will be destroyed on routing.
Calls the server and gets the data. It saves the loading error and data in state and allows selecting items. Doesn't matter much what else it does. But it's stateful
A service that calls a server and gets the data doesnt sound like something to be created dynamically. I am not trying to argue or anything, but I have a hard time understanding why it has to be created dynamically. So I guess I cant be much of help.
@cedar pasture Consider a service that has isLoading, hasError and data in state. It has a method that calls the server and populate the data (update sloading and error too). Of course that's simplified. Now I need to do it for every column. The server calls should run in parallel and the state should be for each column. Isn't it "screaming" multiple instances?
I think the APi side could be a global service.
And the state could perhaps be a non-service, just a class.
It doesnt scream multiple instances to me tho.
Not saying it cant be.
What about the rxjs stream then? With the switch map
Not sure what u mean.
I assume all of those contact the same endpoint?
With some different arguments?
Indeed. But each server call returns an observable. Each instance should run in parallel but has switch map on his own
So that screams: 1 global service for the data retrieval.
Anyway I think the focus here is to hard on how you think you want to achieve it, rather than what u want to achieve it, making it harder to help.
every API call returns an observable.
Never did that mean I needed multiple service instances.
(I never needed multiple service instances in such a way that I had to manually instantiate them)
Not saying that means its not something you might need. Just saying I am still not convinced thats what u need, yet its the focus of this thread.
If u beloeve thats what u need, manually instantiate them.
But I would recommend trying to reconsider that approach.
@cedar pasture An example of such "server call" stateful method:
fetch = this.asyncFn<string>(
pipe(
tap(() => this.updateIsLoading(true)),
switchMap((value) => this.backend.something(value)),
tapResponse(
(res) => this.updateData(res),
(err) => {
this.updateHasError(true);
this.showError(err);
},
() => this.updateIsLoading(false)
)
)
);
Now consider this service has state, updating state logic, selecting state logic and two server calls
I think doing the HTTP call, and tracking the state should be seperated.
I would recommend providing an actual example to show what you are trying to achieve.
Non of the above shows why it cant be a singleton.
I do believe if u talk a bout state for a column, that each column needs to be a component, and track its state internally (or in a service for the component).
If u need to track state across navigation, u can still sync things to a global state as needed
I always try to avoid syncing... especially if I want to the server call to continue working while navigation away
Which is why the server call should be in a global service.
Because now I need groupBy in snippet I sent
fetch then need to accept id and groupBy on it
and if all this logic exists sometimes alone (not dynamically) I have a lot of code to duplicate
Im sorry, I dont see why that would be the case.
I guess you are very convinced about the solution. Which is fine, but then u will have to manually create them.
So say AggService can be created statically X times or dynamically X times
I feel like I am trying to argue, while I am just trying to understand. Honestly, I still see no reason why you need to manually create your services. it's not like you are the only person having applications with data like that. But I cant for the life of me understand why you are so sure about the fact that dynamically creating the service is your only option, or the only option that is done in a nice way.
Hmm I will try to explain through a better example then
You could have something like this:
columnState = new MyStateObject({
getData: this,backend.getData()
});
and internally, columnState is a class with properties to handle errors, loading etc.
That isnt a service and just a class.
And is fine to create like that.
Maybe you dont mean to create a service dynamically, but just instantiate a class. Which is fine to do.
But not every class is a service.
Say you have a full grid "store" service - it has filtering sorting, server calls and uses other services too and a lot of logic related to the grid.
Now you see you need the exact same functionality but not for one grid in the app but to 10 grid.
That's the original (already solved issue I had)
Ye that's a possibility... wanted to stick with DI if I could
Use a grid component, with a grid service
Every component instance has it's own service instance.
That as the initial solution. But then the product said - why when I navigate away and back everything I did gets deleted?
That's when you need to move some things to a global state.
Not manually instantiate things.
Exactly - but I have now 10 grids which are the same in the global state
U can track them by An identitier. Just like u would in a db
If u have 10 grids that 'eed to be Remembered cross navigation, u have a global state for 10 grids.
Right - but then I need to group every grid server call by the identifier and handle it all as an entity. It also means ALL the grids are in one place even those in lazy loaded modules
Later came another request - this specific grid (which is the same like the global one) do need to be tied to a component lifecycle
Duplicate again?
I mean the same global single grid state tied now to a component lifecycle
If it's different, it's not the same
So I thought - I should have a service handling a single grid - It can be used as a global provider or a local provider - makes more sense (solved with injection tokens and deps)
So im not sure how it's the same when u say it's not
I mean everything is the same except the providing level (compnent vs root)
The good thing about global state is that it can also keep state that is bound to a component
Not the other way around
Indeed I could add a grid to the global entity on init and remove it on destroy - but the global multiple grids service was damn complicated
Anyway, i don't want to keep arguing. I still am not sure i understand why you would need it. But you are free to do so
There is no point if you are so convinced. Either show a more concrete, working, example. Or instantiate them how you believe they need to be instantiated
The difference for example with the "global entity" vs instances in server call rx stream:
fetch = this.asyncFn<string>(
pipe(
tap(() => this.updateIsLoading(true)),
switchMap((value) => this.backend.something(value)),
tapResponse(
(res) => this.updateData(res),
(err) => {
this.updateHasError(true);
this.showError(err);
},
() => this.updateIsLoading(false)
)
)
);
fetch2 = this.asyncFn<{ id: string; value: string }>(
pipe(
groupBy((payload) => payload.id),
mergeMap((g) =>
g.pipe(
tap(() => this.updateIsLoading(g.key, true)),
switchMap((payload) => this.backend.something(payload.value)),
tapResponse(
(res) => this.updateData(g.key, res),
(err) => {
this.updateHasError(g.key, true);
this.showError(err);
},
() => this.updateIsLoading(g.key, false)
)
)
)
)
);
Much more complicated - true for updaters, selectors and the state itself too
@cedar pasture I just wanted to make it less complicated that's why I thought of instances instead of one global shared entity for everything
I don't think manually instantiating services makes things less complicated.
And I am not arguing π Just trying to let you know the road to that idea
I also am convinced that both of those van be simplified
The first is the simple the second is the more complicated
Yes, i cant see why the second is the global variant of the first.
Again, you are looking for ways to improve, yet focused on what you have and believe is the only solution.
Provide a stackblitz with how you do it manually.
Pretty sure People Will be Happy to try and rework
Hmm maybe this is the point - each "instance" lives on it's own
Hence the grouping
But you keep talking as if your scenario is so Unique, while all i see is typical state.
Every part of my state lives on it's own
Eg multiple wizards with state that is different per usage, never needed something like that
If there is a server call how do you prevent one wizard call cancelling the other?
Considering on the specific wizard you want switchMap functionality on the server call
I mean - parallel wizards server calls with specific wizard switching
That just works.
Every call to fetchData returns a different observable
Through a singleton
That's how rxjs works.
getData() {
return this.http.get()
}
Somewhere Else.
That's just an API facade - no business here not global state
Correct.
That else - is it duplicated?
And any call to that returns a different observable
Per wizard
I mean - the actual state handling and Rx pipe - if it needs to be global
Where do you put it with multiple wizards
In a global service
Which has something like my first snippet? But you have multiple wizards
This global service need some mapping I guess?
State per wizard - call per wizard - getters per wizard
@cedar pasture That global service is the point - how would you write it considering 10 wizards all doing the same?
That depends. It's Not like what u shared is the only way
Hmmm I would love to know other ways then. What I ended up with is:
- The group by way
- Classes way with new
- Service instances (injection tokens)
The main question is based on that when a new requirement came
Why group by ?
Otherwise switchMap will mix wizards
In your solution maybe
Again, let's stop talking about these potential issues and share a stackblitz that shows the issues
That's more productive
Als you can simplify rx streams by splitting them
I would expect to start with some "getGridByIdFromState()"
Which reads alot better, and reduces complexity
Maybe im just not understanding it. Regardless, showing actual code always helps.
Either way if a global service causes issues with your switchMap, rethink the rxjs stream. Not consider manually instantiating services, imo.
@cedar pasture I will try to create a simple stackblitz example
Might make it easier to understand my requirement
I will do it with my "instances" solution and let me know if there is an easier way
The problem with my way is dynamic as stated in the header
Hope we can get on the same line after it
@cedar pasture A small example for now (Using angular 16 as it's easier):
https://stackblitz.com/edit/angular-kw9z8m?file=src/main.ts
Thnks. At least its not manually instantiating the services, right?
The first thing that comes to mind is that this indeed defeats the purpose of a store, if u need to create a new one per counter.
But also, the fact that the backend is inside the store is weird and not something I see commonly happen.
A store is to hold state, not to execute any http calls.
This does feel a lot of like react to some degree π
Any reason you arent using something like ngrx?
@cedar pasture Hmm I wonder... ngrx has effects (kind of in store) and component store (with effects)
Ye this feels like a lot as if you are rebuilding ngrx.
Which I guess can be a fun thing to do.
But also unneccesary
Well in NGRX you can't have so called "instances" - which is the main problem here
So I can't see it helping
What do u mean with instances?
All I see is 3 counters, with 3 values.
Thats the classroom example of ngrx.
As you can see I have three counters - I can use an entity state in ngrx. But I also have server call
And we are back to groupBy
I dont see any reason to use entity.
And I still have no idea why you keep bringing up groupby.
If there was one generate for all counters
And you click cancel
what will happen? All cancelled
Unless you somehow separate the streams per counter
Thats not ngrx doing that or anything
Yes, which you should, if thats what u want.
Indeed - but instancing here is kind of solving it without handling all the parallel stuff and always having it in mind... or do you have a better idea?
I dont believe groupBy is the only thing that allows you to seperate the stream tho
Oh I am sure it isn't... just easiest afaik π
I dont think manual instantiating it is the easiest.
Despite the fact that I believe what you have doesnt necesarily look bad.
I do think its not common.
Well if you can show this example in an easier way I will gladly adapt π
Anyway the initial question came when those three counters became a list of unknown size (have add, remove, clear)
Injection tokens won't be enough
So your state should be an array of counters.
Im still digesting the stackblitz, like whats the point of createAsync.
Indeed! But I do need counter alone too without it being a part of an array in other app parts
Think ngrx effect
Then u pull it from the array ?
Well then I have an array of static and dynamic - so reset all becomes based on ids which I handle
And if all I want is one counter I need now to work with the counters array with id for my counter for no reason
But your state is an array of counters.
That is literally what you are saying it is.
Your state should represent the data you have
For exmaple:
<app-counter [counterStore]="counterOne"></app-counter>
<app-counter [counterStore]="counterTwo"></app-counter>
<app-counter [counterStore]="counterThree"></app-counter>
<app-counter *ngFor="let counter of counters" [counterStore]="counter"></app-counter>
Mixed I mean
In that case, it should be different state.
Anyway, I am happy to mess around with your stackblitz later today (its dinner time) or over the weekend.
Would be nice if you could remove some things tho
do we need createAsyncFn?
Or TapObserver?
The problem is now the counter logic id unfortunately duplicated... once for array and once for single
Consider a more heavy store and that's a no no
That's your effect
Im not sure, I dont try and be DRY that hard.
Just a utility for rxjs
But is it required?
In counter I guess... But what about a 1k lines service?
Nothing is required π Just makes it easier and more readable imo
Just more code to rework.
What I have a hard time wrapping my head around is the idea of having a store that is so tightly connected to all those things.
Thats not what a store is about
Hmmm... I think that's the bare minimum to make it work easily without private subjects... like component store createEffect
Well isn't it exactly ngrx in one file? state+reducer+selector+effect?
Or component store too is exactly the same
Cant tapResponse just be tap?
Looks like it can, atleast it works.
Ye... but errors will continue (no EMPTY) and you need finalize
In the end, sure. But do we just for the sake of the SB ?
There are no errors?
tap + finalize should be enough I guess?
Not sure why u need finalize?
Cancel won't update is loading back to false otherwise unless you do it after the next always
takeUntil doesn't invoke complete on tap if comes after it
there is already finalize on tap()?
so ye can work
Sorry, just want less to worry about haha
Changed it π
From the looks of it, I dont see an easy way to solve this without entirly doing everything different tbh
Everything is so connected to each other that I believe there isnt much u can do .
Still if I end up with a good way I will be happy π
At the end I need three counters with the same logic and UI
And later dynamic ones too
And when I change something in the counter I want to change once
Like createAsync is what u call the effect, but it's also to update the store
I mean, it all looks pretty nice if u ask me.
But very tightly connected
From component store docs:
Yeah that's fair π€£
NGRX has some serious boilerplate with all those files
Currently aside from maybe having a better way to do stuff I am stuck with the dynamic part... π¦
Hence the original question
It can and it is in my real app
but then came signals π
Component store suffer from missing batched execution and diamond problem
signals don't
profit
@cedar pasture In my real app it looks better with types and all:
// Simplified
export function provideCounterStore(
provideAs: ProviderToken<CounterStore>,
params: CounterParams
): Provider[] {
const paramsToken = new InjectionToken<CounterParams>('Counter Store Params');
const paramsProvider: Provider = {
provide: paramsToken,
useValue: params,
};
const storeProvider: Provider = {
provide: provideAs,
useClass: CounterStore,
deps: [paramsToken, BackendService],
};
return [paramsProvider, storeProvider];
}
// Real life
export function provideCounterStore(
provideAs: ProviderToken<CounterStore>,
params: CounterParams
): Provider[] {
const [paramsToken, paramsProvider] = createStoreParams(params);
const counterProvider = createStoreProvider(
provideAs,
CounterStore,
[paramsToken]
);
return [paramsProvider, counterProvider];
}
So you want ngrx with signals?
@cedar pasture well if ngrx adds any benefit
What i mean is, if u use ngrx today, why not reverse engineer how they do it ?
Ngrx store works with actions which is another way
Component store is practically the SB just without signals
The problem with dynamic isn't solved by ngrx
At the end of the day component store is just a service
I thought in your current project u solved it with ngrx but without signals ?
Well as you saw in the SB I solved static instances not dynamic hence this question to begin with
Ngrx vs signals is just "internal" service flavor
Yeah i think dynamic is complicated in either case
@cedar pasture in regular ngrx I guess it is and in services land I just need somehow to create and destroy them dynamically
Anyway if you have another idea how to tackle such problem I am more than happy to hear
Hope you understand now my rationale for choosing the way I chose
Im seeing what you do, not yet why. But i will try and get my hands on it.
Yeah that would just be without DI afaik.
But yeah you know that
Well mostly simplicity and code reuse. Of course I could be wrong and there exists a better way
Not sure i find any of this simple π€£
But that's probably personal. I think the code is clean tho π
Well code reuse tends to be tougher at first.. same as new code you have never seen
And maybe it is just tough and I am biased
@cedar pasture LMK when you have a SB to show π
I spent yesterday evening looking at a couple of things, it does ask alot to rework that entirely. I think we both agreed on what you can do, as in using a global service/ component service, but you didnt like that idea. Whatever i do, i end up going towards that so i don't think it's wise to keep spending my evening on it, because im convinced you know what i want to do, but believe it's not something you want because the added complexity.
I can see what you mean with added complexity. But i believe what you want to do is complicated anyway, especially if you want static and Dynamic, as well as local and global state.
Bypassing di isn't the solution i would go for, but based on what you said i guess it's what you want .
I would prefer whatever groupby or the like.
Because i believe state should reflect state, how you use it and how you slice parts of that state is a different concern.
@cedar pasture I don't wish to bypass di too... The problem with the global approach is local vs global vs dynamic - should be all together and with custom lifecycle handling
What i find the most difficult to reason about is that you try to use a component store like solution for state that needs to be global , which i think is not the idea.
So I can't just have a grid state... I need a grid list state with support for local and global built in (dynamic local together with dynamic global in one place seems like hell)
I would solve those problem seperatly, global state needs a global solution. Local a local (Well local can use a global one, not vica versa)
You might consider it not dry, but it's two different things solved in different ways, typically.
Looking at your SB, Im pretty convinced you are smart enough to know whether it's not a good idea, so i might not be the best to try and assist further (one does not randomly rebuild such a state management system on a friday evening for someone else, i guess). I never needed a solution like yours, and i probably would try and solve it differently is all i can say. I know that is far from helpful, apologies.
@cedar pasture first of all thanks for all the time you invested!
And second I agree you should separate local from global
And that's how I started (let's put aside if ngrx is the right choice for global)
I mean, happy to help but i doubt i have been helpful π€£
The point that "broke" me was when my global grid state (2k loc) was needed locally in a dialog. I could of course change my global grid state to an entity like with all the implications or duplicate those 2k loc
Thing is it felt wrong
So I ended up thinking how could I have this store available locally and globally while being the same code without complicating the already complicated grid store
Well considering entity - I will try and dig further into DI with all the new changes and might find some solution I guess
Wish I could use global for global and local for local... But they are identical in my case π¦
@cedar pasture Well seems it was easy with the new environment injector... or more specifically createEnvironmentInjector. Updated the SB:
https://stackblitz.com/edit/angular-kw9z8m?file=src/main.ts
A bit complicated though (the infra more than the usage)
@cedar pasture Once again thanks for you help and mostly the "brain-storming" π
Seems I found a way - just need to consider if the added complexity is worth it.
Currently from the usage site - it is pretty clean
That's interesting that its that simple. I think usage simplicity is more important than the thing itself ππ
@cedar pasture actually it can be a simple state management... Just don't know if it has any value except my extreme case compared to other solutions
Or maybe it's just wrappers around DI
Ye saw that thanks. Going in the right direction:)
@cedar pasture sry to bump it - but did some experiments and seems like the way I showed here has problems when the params are not statically known. For example passing a service signal into the store as parameter isn't possible now as params are decided before injection. I decided to change the way it works to be more like inject functions.
Let me know what you think