mrpmorris / Fluxor

Fluxor is a zero boilerplate Flux/Redux library for Microsoft .NET and Blazor.
MIT License
1.22k stars 139 forks source link

Fluxor States don't get updated when are injected in DI scopes different than the application's root scope #402

Closed alexandrutatarciuc closed 1 year ago

alexandrutatarciuc commented 1 year ago

I have a message handler that inherits from DelegatingHandler and I use it for the HTTP client. The service itself is registered as transient. In my message handler I inject IState<MyState> (constructor dependency injection) and in the constructor I hook into MyState.StateChanged += HandleStateChanged. However, my state is always null and my HandleStateChanged method is never called. If I use my message handler as a service by injecting it, it works just fine, but if it is used by the HTTP client as a delegating handler - it doesn't work. My use case is that I have an id in MyState and I want all of my requests to contain a custom header with the id as the value. I've been struggling to understand what is the main problem, any guidance is appreciated.

mrpmorris commented 1 year ago

If you can upload a repro somewhere, I'll have a look for you.

PS: HttpClient should be registered as scoped.

alexandrutatarciuc commented 1 year ago

First, I am sorry a late response. I had more time to look into it. It looks like this issue appears to happen when you are using the fluxor state in a DI scope that's different than the app's scope (root DI scope I guess).

If you have a service that must be added to a component's scope in Blazor, like a ViewModel, then you should inherit from OwningComponentBase and use ScopedServices to call GetService to set the ViewModel value. By doing this, as far as I understand, you bound the service to the component's scope so now IDisposable.Dispose on that service will actually be called when the component is disposed. If I try to inject IState<MyState> in the ViewModel, they'll be no changes to the value of the state. In fact, if I inject the IDispatcher and dispatch an action, the reducer will never be called as well.

The same issue happens to be for a service that derives from DelegatingHandler, because the scope of that service isn't the root any longer, but the HttpClient's DI scope (or request's scope, I am not sure).

Am I missing any option when registering Fluxor with .AddFluxor() or do you have any clue how to make a workaround for that? I will try to send a repro tomorrow.

alexandrutatarciuc commented 1 year ago

Check the counter page

mrpmorris commented 1 year ago

OwningComponentBase creates its own scope. I don't think the child of a scope inherits instances from its parent.

Why are you using OwnedComponentBase for a view model? And shouldn't your state be your view model?

alexandrutatarciuc commented 1 year ago

OwningComponentBase creates its own scope.

Yes indeed. This is probably why I can inject the state, but it doesn't get updated.

I don't think the child of a scope inherits instances from its parent.

I am not really sure what you mean by that.

Why are you using OwnedComponentBase for a view model?

Because I want my components to control the lifetime of the ViewModel service. I want to be able to implement IDisposable on the ViewModel when needed. If I don't register the ViewModel as a service scoped to the component's lifetime, it wouldn't be called when the component gets disposed. I want "to marry" the component and the ViewModel, so that the ViewModel gets disposed when the component does.

And shouldn't your state be your view model?

My usage of "ViewModel" probably doesn't fall under the common definition of a ViewModel in a MVVM context. It is more like a code-behind class where I store all the logic related to a component so it is easier to test my components (because I can mock them). Therefore, every component has one and only one unique ViewModel, however different ViewModels of different components can inject the same State. For example, I can have components Product, AddToCartButton, etc. each with their unique ViewModel, but they are all interested in the CartState, so the Product would display that it is already added to the cart and the AddToCartButton would display an increment in quantity of the product. So no, my state isn't the ViewModel.

alexandrutatarciuc commented 1 year ago

But as I mentioned earlier, the same issue occurs for a handler deriving from DelegatingHandler. And now you can clearly see why some would do that because developers would intend to get some sort of key or id from their UserState to append as a value to a header in every request they are making to the backend.

mrpmorris commented 1 year ago

Instead of having the component dispose, try this

  1. Have a static readonly Empty member in that state. So ThatState.Empty
  2. Have a reducer that reduces ThatState and GoAction
  3. If the URL in GoAction is one relevant to that state then return the state you were given, otherwise return ThatState.Empty to clear it down.
alexandrutatarciuc commented 1 year ago

I'm confused. The issue is that Fluxor seems to not work at all when it is injected in a service that has a different scope than the application's root scope (thus, step 2 is not possible because the reducer will never be called). I need something to let Fluxor know that I also want new state changes for my DI scope.

Intuitively, I think that this should be added to Fluxor as a new feature. If you can point out why this is happening in Fluxor, I can contribute and add this new capability.

mrpmorris commented 1 year ago

It's not Fluxor, it's just how .Net dependency injection works. The Unity DI used to have HierarchicalLifetimeManager, but the modern one doesn't.

Why not use my suggested approach instead?

alexandrutatarciuc commented 1 year ago

I am just not sure what you meant. In my example provided in the repro, I have CounterState, a Counter component, an ICounterViewModel, and MyComponentBase from which my Counter component derives from. Do you suggest removing MyComponentBase and disposing the ViewModel manually using ViewModel = ViewModel.Empty? I assume this because I don't need my state to be cleared or disposed so I think you meant the ViewModel.

I want to reiterate this again, I can possibly find a workaround for this issue by not registering my ViewModel using ScopedServices from OwningComponentBase (even though this is what Microsoft seems to recommend doing), but if I have a custom message handler that derives from DelegatingHandler, the Fluxor State doesn't work there either. It would be really really annoying if I can't use a message handler for my HTTP requests.

mrpmorris commented 1 year ago

If it's an object instance per component you need, why not new it manually and Dispose?

alexandrutatarciuc commented 1 year ago

So I solved this inconvenience by avoiding inheriting from OwningComponentBase and disposing manually just as you suggested. Do you have any idea/suggestion on how I can use the state inside of an HTTP message handler that derives from DelegatingHandler? (it has its own DI scope)

mrpmorris commented 1 year ago

Sorry, you're on your own with that one :)

alexandrutatarciuc commented 1 year ago

Thanks a lot for the help!