#Dynamic services create and destroy

303 messages Β· Page 1 of 1 (latest)

steel cedar
#

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).

cedar pasture
#

Services can be created and destroyed per component. Depending how your app is structured that could be a way as well.

steel cedar
#

@cedar pasture ye I know. Unfortunately those services are not connected to a component

steel cedar
#

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?

cedar pasture
steel cedar
#

@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

cedar pasture
#

What do the services do ?

steel cedar
#

And the columns or not statically known and those aggregations need to live in parallel to each other (server calls and state)

severe pier
#

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.

cedar pasture
cedar pasture
#

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

steel cedar
#

@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.

steel cedar
cedar pasture
steel cedar
#

@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?

cedar pasture
#

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.

steel cedar
cedar pasture
#

I assume all of those contact the same endpoint?

#

With some different arguments?

steel cedar
#

Indeed. But each server call returns an observable. Each instance should run in parallel but has switch map on his own

cedar pasture
#

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.

steel cedar
#

@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

cedar pasture
#

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

steel cedar
#

I always try to avoid syncing... especially if I want to the server call to continue working while navigation away

cedar pasture
steel cedar
#

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

cedar pasture
#

I guess you are very convinced about the solution. Which is fine, but then u will have to manually create them.

steel cedar
#

So say AggService can be created statically X times or dynamically X times

cedar pasture
#

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.

steel cedar
#

Hmm I will try to explain through a better example then

cedar pasture
#

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.

steel cedar
#

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)

steel cedar
cedar pasture
#

Every component instance has it's own service instance.

steel cedar
cedar pasture
#

That's when you need to move some things to a global state.

#

Not manually instantiate things.

steel cedar
#

Exactly - but I have now 10 grids which are the same in the global state

cedar pasture
#

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.

steel cedar
#

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?

cedar pasture
#

No?

#

Requests and state are two different things

steel cedar
cedar pasture
#

If it's different, it's not the same

steel cedar
#

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)

cedar pasture
#

So im not sure how it's the same when u say it's not

steel cedar
cedar pasture
#

The good thing about global state is that it can also keep state that is bound to a component

#

Not the other way around

steel cedar
#

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

cedar pasture
#

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

steel cedar
#

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

cedar pasture
#

I don't think manually instantiating services makes things less complicated.

steel cedar
#

And I am not arguing πŸ™‚ Just trying to let you know the road to that idea

cedar pasture
#

I also am convinced that both of those van be simplified

steel cedar
#

The first is the simple the second is the more complicated

cedar pasture
#

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

steel cedar
#

Hence the grouping

cedar pasture
#

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

steel cedar
#

Considering on the specific wizard you want switchMap functionality on the server call

#

I mean - parallel wizards server calls with specific wizard switching

cedar pasture
#

Every call to fetchData returns a different observable

#

Through a singleton

#

That's how rxjs works.

steel cedar
#

Could you provide an small snippet?

#

That means you subscribe in usage

cedar pasture
#

getData() {
return this.http.get()
}

steel cedar
#

Eh... Where do you update the state?

#

Based on the server call

cedar pasture
#

Somewhere Else.

steel cedar
#

That's just an API facade - no business here not global state

steel cedar
#

That else - is it duplicated?

cedar pasture
#

And any call to that returns a different observable

steel cedar
#

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

cedar pasture
#

In a global service

steel cedar
#

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?

cedar pasture
#

That depends. It's Not like what u shared is the only way

steel cedar
#

Hmmm I would love to know other ways then. What I ended up with is:

  1. The group by way
  2. Classes way with new
  3. Service instances (injection tokens)
#

The main question is based on that when a new requirement came

cedar pasture
#

Why group by ?

steel cedar
#

Otherwise switchMap will mix wizards

cedar pasture
#

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.

steel cedar
#

@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

steel cedar
cedar pasture
#

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?

steel cedar
#

@cedar pasture Hmm I wonder... ngrx has effects (kind of in store) and component store (with effects)

cedar pasture
#

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

steel cedar
#

Well in NGRX you can't have so called "instances" - which is the main problem here

#

So I can't see it helping

cedar pasture
#

What do u mean with instances?

#

All I see is 3 counters, with 3 values.

#

Thats the classroom example of ngrx.

steel cedar
#

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

cedar pasture
#

I dont see any reason to use entity.

#

And I still have no idea why you keep bringing up groupby.

steel cedar
#

If there was one generate for all counters

#

And you click cancel

#

what will happen? All cancelled

cedar pasture
#

Not neccesarily.

#

That all depends how you write things

steel cedar
#

Unless you somehow separate the streams per counter

cedar pasture
#

Thats not ngrx doing that or anything

cedar pasture
steel cedar
#

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?

cedar pasture
#

I dont believe groupBy is the only thing that allows you to seperate the stream tho

steel cedar
#

Oh I am sure it isn't... just easiest afaik πŸ™‚

cedar pasture
#

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.

steel cedar
#

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

cedar pasture
#

So your state should be an array of counters.

#

Im still digesting the stackblitz, like whats the point of createAsync.

steel cedar
#

Indeed! But I do need counter alone too without it being a part of an array in other app parts

cedar pasture
steel cedar
#

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

cedar pasture
#

That is literally what you are saying it is.

#

Your state should represent the data you have

steel cedar
#

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

cedar pasture
#

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?

steel cedar
#

Consider a more heavy store and that's a no no

steel cedar
cedar pasture
#

Im not sure, I dont try and be DRY that hard.

steel cedar
cedar pasture
#

But is it required?

steel cedar
steel cedar
cedar pasture
#

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

steel cedar
steel cedar
#

Or component store too is exactly the same

cedar pasture
#

Looks like it can, atleast it works.

steel cedar
#

Ye... but errors will continue (no EMPTY) and you need finalize

cedar pasture
#

There are no errors?

steel cedar
#

tap + finalize should be enough I guess?

cedar pasture
#

Not sure why u need finalize?

steel cedar
#

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

cedar pasture
#

there is already finalize on tap()?

steel cedar
#

Ah that's new... maybe new rxjs feature?

#

I don't have it locally

cedar pasture
#

U have it in your SB

#

πŸ˜„

#

Ah no

steel cedar
#

so ye can work

cedar pasture
#

Sorry, just want less to worry about haha

steel cedar
#

Changed it πŸ™‚

cedar pasture
#

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 .

steel cedar
#

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

cedar pasture
#

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

steel cedar
#

From component store docs:

cedar pasture
#

Yeah that's fair 🀣

steel cedar
#

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

cedar pasture
#

Why Cant this just be Component store tho?

#

Connected to counter component?

steel cedar
#

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];
}
cedar pasture
#

So you want ngrx with signals?

steel cedar
#

@cedar pasture well if ngrx adds any benefit

cedar pasture
#

What i mean is, if u use ngrx today, why not reverse engineer how they do it ?

steel cedar
#

The problem with dynamic isn't solved by ngrx

#

At the end of the day component store is just a service

cedar pasture
#

I thought in your current project u solved it with ngrx but without signals ?

steel cedar
#

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

cedar pasture
steel cedar
#

@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

cedar pasture
#

Im seeing what you do, not yet why. But i will try and get my hands on it.

cedar pasture
#

But yeah you know that

steel cedar
cedar pasture
#

Not sure i find any of this simple 🀣

#

But that's probably personal. I think the code is clean tho πŸ‘Œ

steel cedar
#

And maybe it is just tough and I am biased

steel cedar
#

@cedar pasture LMK when you have a SB to show πŸ™‚

cedar pasture
# steel cedar <@227872143281487872> 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.

steel cedar
#

@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

cedar pasture
#

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.

steel cedar
#

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)

cedar pasture
cedar pasture
#

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.

steel cedar
#

@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)

cedar pasture
steel cedar
#

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 😦

steel cedar
#

@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)

StackBlitz

A angular-cli project based on @angular/animations, @angular/common, @angular/compiler, @angular/core, @angular/forms, @angular/platform-browser, @angular/platform-browser-dynamic, @angular/router, core-js, rxjs, tslib and zone.js.

steel cedar
#

@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

cedar pasture
#

That's interesting that its that simple. I think usage simplicity is more important than the thing itself πŸ‘ŒπŸ‘

steel cedar
#

@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

cedar pasture
steel cedar
#

Ye saw that thanks. Going in the right direction:)

steel cedar
#

@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