mrpmorris / Fluxor

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

Add support for WasmPrerendering #410

Closed panchoaby closed 8 months ago

panchoaby commented 1 year ago

Context

Just migrated my current Blazor app from WASM to WASM Prerendered. Basically the app loads twice, once on the server and once on the client with the possibility of hydrating the data gathered on the server on the client using the Persisted Component State service.

At first I had a flicker in the page, as first the data was retrieved on the server and it returned the static content needed to display the page and after, when the WASM loaded it would run the app and it will retrieve the data again, hence the flicker.

To fix this i created a StateHydrator component to restore on the client side the last state available on the server:

public partial class StateHydrator
{
    [Inject] private IServiceProvider ServiceProvider { get; set; }
    [Inject] private IStore Store { get; set; }
    [Inject] PersistentComponentState ApplicationState { get; set; }

    private PersistingComponentStateSubscription persistingSubscription;
    private bool WasmRendering;

    protected override async Task OnInitializedAsync()
    {
        persistingSubscription = ApplicationState.RegisterOnPersisting(PersistData);

        foreach (var feature in Store.Features)
        {
            var hydrateState = this.GetType().GetMethod(nameof(HydrateState))!
                .MakeGenericMethod(feature.Value.GetStateType());

            hydrateState.Invoke(this,Array.Empty<object>());
        }
    }

    protected Task PersistData()
    {
        foreach (var feature in Store.Features)
        {
            ApplicationState.PersistAsJson(feature.Key, feature.Value.GetState());
        }

        return Task.CompletedTask;
    }

    public void HydrateState<T>() where T : class
    {
        if (!ApplicationState.TryTakeFromJson<T>(typeof(T).Name, out var persistedState))
            return;

        var feature = ServiceProvider.GetService<IFeature<T>>();
        feature.RestoreState(persistedState);
    }

    protected override void Dispose(bool disposing)
    {
        persistingSubscription.Dispose();
        base.Dispose(disposing);
    }
}

This works fine the only issue is that the app does again all the calls as the StoreInitializedAction is triggered on the client.

Proposed solution

mrpmorris commented 1 year ago

How do you tell the difference between a pre-render and wasm execution?

panchoaby commented 1 year ago

We can use ApplicationState.TryTakeFromJson for that, basically if it returns true that means you are on the client as in the server the value should not be persisted yet.

Given this is the case only for the first time you call the method for a particular value. After that i guess it removes it from the storage.

It is a bit odd, but i did not find another way to check this and I see that this is how the documentation differentiates between server and client on the OnInitializedAsync method

mrpmorris commented 1 year ago

I think you could do this with a Middleware.

If on the client then don't allow StoreInitialzed action to dispatch, deserialize from that state instead.

panchoaby commented 1 year ago

Yes, a Middleware will work for restoring the ReduxDevTools with the actions dispatched on the server. For the StoreInitialized action a bypass bool in the Store class constructor will do, unless there is another way to suppress it that I do not see.

But for the first point of keeping track of the effects I am not sure how to do it with a middleware without modifying the Store class as the executedEffect list of tasks is a local variable and there is no list of all currently running tasks that I can persist and restart on the client. Unless I am missing something.

mrpmorris commented 1 year ago

You won't need to know an effect executed, you only need to know

  1. At minimum - the current state of all features
  2. At most - also the actions that were dispatched (for RDTools), although I most likely wouldn't bother with this one.
panchoaby commented 1 year ago

Yes, for the current state of all features is easy to retrieve with the current implementation from the Store, and agreed that RDTools is more of a nice to have but you might have the following situation: ActionA triggers EffectA which lets say retrives data from ApiA and dispatches an ActionB to store the response using ReducerA ActionB => EffectB (ApiB) =>ActionC =>ReducerB And so on so that the effect are chained

At the cutoff point in the server will know the state but not if EffectB for example is finished or not and when we restore it on the client we will have to restart everything with ActionA even if we have the data from ApiA already in the state and if we knew what effects were still executing we could restart them on the client which will remove the flickering on overwriting data with previous states and make the app faster as we do not duplicate code.

mrpmorris commented 1 year ago

I doubt you'll get the results of an asynchronous API call in the state before the server renders and sends the state to the client. Does it?

panchoaby commented 1 year ago

Usually no, but it really depends on the app logic, the api call was just an example

mrpmorris commented 1 year ago

Does Blazor even support that yet?

panchoaby commented 1 year ago

If you are referring to prerendering, then yes. I have already deployed an app using wasm prerendering and the results are very good in terms of user experience.

I would be happy to try and implement the above mentioned changes to a branch and, depending on the results, see if it is something you would like to be added to the next release.

mrpmorris commented 1 year ago

Not just prerendering, also transferring the components' states down to the client too?

panchoaby commented 1 year ago

Oh sorry, misunderstood. Yes that is supported since .NET 6 (from what I can see in the documentation) but I have only implemented in .NET 7 in my app so I can only vouch for it. Link to docs

JeremyVm commented 1 year ago

@panchoaby Do you have an update on this? Did you get Fluxor and Blazor Wasm Prerendering working?

panchoaby commented 1 year ago

@JeremyVm I have some progress but I was not able to finish it as this work was deprioritized in my project, if you need it you can use the StateHydrator class from above to sync the state from server to client but unfortunately the actions will be dispatched again on the client (which might not be a problem depending on your usecase)

JeremyVm commented 1 year ago

@panchoaby Thanks for your reply. At the moment I'm not really using it as my data never gets downloaded as dispatchers are not async. How did you bypass this?