adospace / reactorui-maui

MauiReactor is a MVU UI framework built on top of .NET MAUI
MIT License
552 stars 45 forks source link

Global State #224

Closed Mithgroth closed 3 months ago

Mithgroth commented 3 months ago

Hey ado, just occured to me so I wanted to ask - does it make sense to build something like a GlobalState?

It can be based on IParameter. Components would somehow subscribe to it and invalidate themselves if there is a change. Or am I unintentionally describing ReactorData or some other common concept?

adospace commented 3 months ago

Absolutely! It makes sense, I usually have a GlobalState class that contains app-wide state values that I then inject as a parameter in my components (and yes components are invalidated automatically when it changes).

ReactorData is another package designed over SwiftData that may be useful to handle more advanced scenarios like: 1) cache data received from remote services 2) add off-line support to an app 3) use ef core to store data locally and sync back to the server

Mithgroth commented 3 months ago

Absolutely! It makes sense, I usually have a GlobalState class that contains app-wide state values that I then inject as a parameter in my components (and yes components are invalidated automatically when it changes).

Would you mind showing me a sample? Since GetOrCreateParameter() is under Component, I probably can't use IParameter in a static class for GlobalState

public static partial class GlobalState
{
    [Param]
    static IParameter<MyType> MyParam;
}

Also this would generate a non-static constructor, which should piss the compiler off.

A workaround would probably be storing the data as GlobalState properties and maybe trying to set Parameters in sets? Sounds pretty clunky, but even if that was the case, how do I get a Parameter reference outside of a Component anyway?

Uff, a bit confused I suppose.

Eventually what I'm trying to do is to centralize my data since I need to pull from my Api when my app starts. All the service methods are async, and don't really play well with OnMounted() - it leads to situations where my app launches, UI thread isn't blocked.

Sorry, I'm more of a backend person so - maybe I should just embrace async voids and just handle data being null for a bit. Put "loading..."s to everywhere. If I get to solve centeralization with Parameters, at least I get to invalidate relevant parts without pub-sub'ing to WeakReferenceMessengers everywhere.

adospace commented 3 months ago

Using a parameter is pretty easy, just create a class containing your state and inject it into your components using the [Param] attribute.

Have you looked at the documentation: https://adospace.gitbook.io/mauireactor/components/component-parameters

KeeMind sample (Global state class passed as parameter): https://github.com/adospace/kee-mind/blob/main/src/KeeMind/Pages/MainPage.cs

Trackizer app (User class passed over as parameter): https://github.com/adospace/mauireactor-samples/blob/main/TrackizerApp/Pages/HomeScreen.cs

Mithgroth commented 3 months ago

Yep, I mean to have parameters in a static centeralized GlobalState as well so I can set them. Getting them in a component is fairly straightforward.

Like:

// GlobalState.cs
public static partial class GlobalState
{
     [Param]
     static IParameter<MyData> MyParam; // Can't do this since this is not a Component

     public static async Task RefreshData()
     {
         MyParam.Set(_ => _.Value = someObject); // Obviously can't do this too
     }
}

// SomeComponent.cs
partial class SomeComponent : Component<SomeState>
{
    // Get and do stuff with MyParam
    [Param]
    IParameter<MyData> MyParam;
}

Isn't this what you meant initially as well?

adospace commented 3 months ago

ok I'm, not sure what you're trying to achieve but in MauiReactor you should create a class holding the state (global or component state) that should not contain methods. From what I understand you're mixing different behaviors that should be split up into different classes:

Something like:

interface IApiService 
{
   Task<DataModel> LoadDataFromServer();
}

class MyGlobalState
{   
   public DataModel? MyData {get;set;}
}

class MyComponent
{
   [Param]
   IParameter<MyGlobalState> _globalState;

   [Inject]
   IApiService _myService;

   override OnMounted()
   {
      Task.Run(LoadDataFromServer);
   } 

   public override Render()
   {
      return ContentPage(...render _globalState.Value );
   }

   async Task LoadDataFromServer()
  {
     var dataLoaded = await _myService.LoadDataFromServer();
     _globalState.Set(_ => _.MyData = dataLoaded);
  }

}
Mithgroth commented 3 months ago

Yup. I was imagining a static class with all application-wide data as parameters, and components would reference them too, and work like an implicit pub-sub model:

static class GlobalState
{
    [Parameter]
    IParameter<User> User;
    [Parameter]
    IParameter<List<Report>> Reports;
    [Parameter]
    IParameter<SomeData> Data;
}

Then we could set them (not necessarily on GlobalState, maybe in services):

class UserService
{
    public async Task GetUser()
    {
        // ...
        GlobalState.User.Set(_ => _.Value = user);
    }
}

And components referencing this parameter would be updated:

class UserComponent : Component
{
    [Parameter]
    IParameter<User> User;

    public override VisualNode Render()
    {
        // Do something with GlobalState.User
    }
}

Hope that made some sense. But I liked your sample as well, not centeralized as I put it, but it manages to communicate component to component, probably should result similarly.

adospace commented 3 months ago

I see, probably you could then be interested in state managers ala Flux, like Fluxor

https://github.com/mrpmorris/Fluxor

This is a POC integration for MauiReactor https://gist.github.com/adospace/cbede42c410c642dbdbcafe9ece5e90b

you could easily create a Component derived class that invalidates the component when the global state changes and derive your components from it.

Mithgroth commented 3 months ago

Thanks a lot man! Probably off-topic for this repository but, based on this in ReactorData's readme:

Without storage, ReactorData is more or less a state manager, keeping all the entities in memory.

Do you have plans for ReactorData to work like this?

adospace commented 3 months ago

Using ReactorData just as a global state manager is not advisable because ReactorData container is designed to maintain lists of models (IQuery) and notify subscribers when a new record is added/edited or removed from these lists.

I'm working on a new sample application that shows how to use MauiReactor+ReactorData to fetch and cache data locally.

I don't know the general context of your app but maybe that sample code could help in your case too.