Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Is there a way to use Fluxor with the new Blazor Web App paradigm? .NET8.0 #459

Open
Rhywden opened this issue Nov 14, 2023 · 37 comments
Open

Comments

@Rhywden
Copy link

Rhywden commented Nov 14, 2023

Now with .NET8 released into production we also have what was formerly called Blazor United, i.e. where there formerly was a strict distinction between Blazor Server and Blazor WASM we now have both in one application.

Problem is that I cannot quite figure out how to get Fluxor to get to work on the "client side". For example, App.razor simply does not exist anymore, only on the "server". This here does not work:
client/Program.cs

[...]
var currentAssembly = typeof(Program).Assembly;
builder.Services.AddFluxor(options => options.ScanAssemblies(currentAssembly).UseReduxDevTools());
[...]

client/Pages/Counter.razor

@page "/counter"
@rendermode InteractiveAuto

<Fluxor.Blazor.Web.StoreInitializer />

<PageTitle>Counter</PageTitle>

which yields:
InvalidOperationException: Cannot provide a value for property 'Store' on type 'Fluxor.Blazor.Web.StoreInitializer'. There is no registered service of type 'Fluxor.IStore'.

Changing to InteractiveWebAssembly does not change anything about that.

So, is there a way forward?

@Rhywden
Copy link
Author

Rhywden commented Nov 15, 2023

Okay, so I found a way to get things going. Works like this: You define your actions, state and reducers completely like normal on the Client.
Then you create a static method to register your Fluxor service on the Client:

public static class CommonServices
{
    public static void ConfigureServices(IServiceCollection services)
    {
        var currentAssembly = typeof(Program).Assembly;
        services.AddFluxor(options => options.ScanAssemblies(currentAssembly).UseReduxDevTools());
    }
}

You call this method in Client/Program.cs like so:

var builder = WebAssemblyHostBuilder.CreateDefault(args);

CommonServices.ConfigureServices(builder.Services);

await builder.Build().RunAsync();

and also on the Server like so:

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents()
    .AddInteractiveWebAssemblyComponents();

YourBlazorWebApp.Client.CommonServices.ConfigureServices(builder.Services);

I didn't find a good place to initialize the store yet. Currently it only gets initialized the first time you hit a Client-side page (like the Counter from the default template):

@page "/counter"
@using YourBlazorWebApp.Client.Store.CounterUseCase
@using Fluxor
@rendermode InteractiveAuto
@inherits Fluxor.Blazor.Web.Components.FluxorComponent

<Fluxor.Blazor.Web.StoreInitializer />

<PageTitle>Counter</PageTitle>

<h1>Counter</h1>

<p role="status">Current count: @CounterState?.Value.ClickCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    [Inject]
    private IState<CounterState>? CounterState { get; set; }
    [Inject]
    public IDispatcher? Dispatcher { get; set; }

    private void IncrementCount()
    {
        var action = new IncrementCounterAction();
        Dispatcher?.Dispatch(action);
    }
}

but then it'll work and seemingly won't reinitialize every time you hit this page. The store also doesn't get destroyed if you navigate away.

But registering the service on both sides is essential!

@two-thirty-seven
Copy link

two-thirty-seven commented Nov 16, 2023

Can confirm I get this same error on a clean .Net 8 webassembly app with a simple Fluxor state.

InvalidOperationException: Cannot provide a value for property 'Store' on type 'Fluxor.Blazor.Web.StoreInitializer'. There is no registered service of type 'Fluxor.IStore'.

Program.cs

builder.Services.AddScoped<TenantsState>();

builder.Services.AddFluxor(options =>
{
    options.ScanAssemblies(typeof(Program).Assembly);
    options.UseReduxDevTools(rdt =>
    {
        rdt.Name = "Backend.Client";
    });
});

Routes.razor

<Fluxor.Blazor.Web.StoreInitializer />
<Router AppAssembly="@typeof(Program).Assembly">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(Layout.MainLayout)" />
        <FocusOnNavigate RouteData="@routeData" Selector="h1" />
    </Found>
</Router>

Home.razor

@page "/"
@inherits Fluxor.Blazor.Web.Components.FluxorComponent
@inject IState<TenantsState> _tenantsState

<PageTitle>Home</PageTitle>

<h1>Hello, world!</h1>

Welcome to your new app.

@_tenantsState.Value.SelectedTenant

@two-thirty-seven
Copy link

Interestingly...... I just converted my static web app WebAssembly application to .Net 8 and Fluxor had no issues. So maybe it's some conflict with the hosted model?

@stagep
Copy link

stagep commented Nov 17, 2023

@Rhywden When you add Fluxor to your services in the server project, the ScanAssemblies method has an overload that allows you to include additional assemblies so you can reference the Client assembly.

builder.Services.AddFluxor(o => o
      .ScanAssemblies(typeof(Program).Assembly, new[] { typeof(Client._Imports).Assembly }));

I have also not found a way to initialize the store so for now I am also placing the initializer on any client side page that uses Fluxor in the same manner as you are doing.

@stagep
Copy link

stagep commented Nov 17, 2023

One way to initialize the client side store is to add a component in your client project that contains

@rendermode InteractiveWebAssembly
<Fluxor.Blazor.Web.StoreInitializer />

and then add this component to your main layout page in the server project.

@Rhywden
Copy link
Author

Rhywden commented Nov 19, 2023

One way to initialize the client side store is to add a component in your client project that contains

@rendermode InteractiveWebAssembly
<Fluxor.Blazor.Web.StoreInitializer />

and then add this component to your main layout page in the server project.

The default @rendermode InteractiveAuto works just fine. But yes, putting it somewhere that only gets loaded once (like a Navbar or an Appbar) is a good spot to put it.
I also put the initializers for Mudblazor there so I can get Dialogs / Modals / Snackbar.

@two-thirty-seven
Copy link

Okay, so I found a way to get things going. Works like this: You define your actions, state and reducers completely like normal on the Client.

But registering the service on both sides is essential!

This would scan with the official docs on DI:

https://learn.microsoft.com/en-us/aspnet/core/blazor/fundamentals/dependency-injection?view=aspnetcore-8.0#register-common-services

@Rhywden
Copy link
Author

Rhywden commented Nov 23, 2023

Yes, that's where I got it from.

@AndreaEBM
Copy link

Having the same problem but still struggling with following the above suggestions. Does anyone have a minimal implementation they could share?

@stagep
Copy link

stagep commented Nov 30, 2023

Please tell us what options you selected for Interactive render mode and Interactivity location when creating the project(s).

@AndreaEBM
Copy link

Please tell us what options you selected for Interactive render mode and Interactivity location when creating the project(s).

Auto and (is this my issue?) per page/component

@stagep
Copy link

stagep commented Dec 2, 2023

Do the components using Fluxor exist only in the Client (WebAssembly) project?

@stagep
Copy link

stagep commented Dec 3, 2023

I have created a sample application that demonstrates Fluxor working in the client (WASM) successfully with a client counter. The application also includes a server side counter that does not work consistently, and if it does work, the client side counter will not work.

Link

@AndreaEBM
Copy link

s using Fluxor exist only i

Thank you so much. Your solution works for me, but copying across into my solution doesn't work. I think I must have other issues to resolve as part of my .net8 migraion. I will continue hunting.

@mrpmorris
Copy link
Owner

Are you talking about the new "static server side rendering"?

If so, that's a stateless approach. The state of the page is determined on every render, so you don't need local state.

@Rhywden
Copy link
Author

Rhywden commented Dec 6, 2023

Are you talking about the new "static server side rendering"?

If so, that's a stateless approach. The state of the page is determined on every render, so you don't need local state.

Yes, but the original issue was on how to get it working at all. We found out but the docs might need an update.

@stagep
Copy link

stagep commented Dec 7, 2023

@mrpmorris The example that I put on Github is using InteractiveServer and InteractiveWebAssembly rendering to represent a stateful application on both the server and the client. This example will only work with one of these render modes, and the server rendered counter does not always work. Please try the example to see this.

https://github.com/stagep/Blazor8WithFluxor

@mrpmorris
Copy link
Owner

Isn't asking how to get a state library to work on a stateless architecture like asking where to put the petrol in a solar panel?

They are different things.

@stagep
Copy link

stagep commented Dec 12, 2023

InteractiveServer render mode is not stateless.

ASP.NET Core Blazor state management

@stagep
Copy link

stagep commented Dec 20, 2023

@mrpmorris Did you have a chance to look at my stateful server and client application? It seems that the Fluxor library will only work with either InteractiveServer or InteractiveWebAssembly rendering.

@SethVanderZanden
Copy link

SethVanderZanden commented Dec 29, 2023

So I had to register my fluxor and other services on both client and server. However, my stores are only on the client as I do not intend on using them ons server currently, or Ill move them to a shared library and register them that way.

However, I also required making the StoreInitializer on the Routes, utilize the InteractiveServer rendermode as it was running as static and thus, no functionality.

<Fluxor.Blazor.Web.StoreInitializer @rendermode="InteractiveServer" />

Hope that Helps

@pjh1974
Copy link

pjh1974 commented Jan 3, 2024

It looks like the Store can only be initialized once, either WASM or Server. If InteractiveAuto is used and only InteractiveServer pages are used, the store is initialized as InteractiveServer and then InteractiveAuto components can't use the store. If only InteractiveWebAssembly pages are used the store is initialized as InteractiveWebAssembly and then InteractiveServer pages don't work.

It seems like some work is required to initialize the store once but in a way that both WASM and Server components can use it.

@Rhywden
Copy link
Author

Rhywden commented Jan 3, 2024

It seems like some work is required to initialize the store once but in a way that both WASM and Server components can use it.

Just to make it clear: Even if both sides (server and client) can initialize a store, they'll be decoupled and completely independent of each other.

@SethVanderZanden
Copy link

SethVanderZanden commented Jan 3, 2024

It looks like the Store can only be initialized once, either WASM or Server. If InteractiveAuto is used and only InteractiveServer pages are used, the store is initialized as InteractiveServer and then InteractiveAuto components can't use the store. If only InteractiveWebAssembly pages are used the store is initialized as InteractiveWebAssembly and then InteractiveServer pages don't work.

It seems like some work is required to initialize the store once but in a way that both WASM and Server components can use it.

I have registered the services on both server and client, however the store has had no issues with interactive auto. The initial load uses server, subsequent loads use the DLL. If you refresh the page the state is always lost, which is normal Fluxor behaviour.

@pjh1974
Copy link

pjh1974 commented Jan 3, 2024

I don't think InteractiveAuto actually works, see the following link:

dotnet/aspnetcore#52154

All my tests show that it just does pre-rendering. I'm not sure if this affects the use of the <Fluxor.Blazor.Web.StoreInitializer /> component or not though.

In my tests I'm going with the @Rhywden suggestion of using the store initializer from a component in the client project with interactive auto rendering but it only initializes once, either on the server or the client. @SethVanderZanden how are you initializing the store if you have it working in both places?

@pjh1974
Copy link

pjh1974 commented Jan 3, 2024

It looks like the Store can only be initialized once, either WASM or Server. If InteractiveAuto is used and only InteractiveServer pages are used, the store is initialized as InteractiveServer and then InteractiveAuto components can't use the store. If only InteractiveWebAssembly pages are used the store is initialized as InteractiveWebAssembly and then InteractiveServer pages don't work.
It seems like some work is required to initialize the store once but in a way that both WASM and Server components can use it.

I have registered the services on both server and client, however the store has had no issues with interactive auto. The initial load uses server, subsequent loads use the DLL. If you refresh the page the state is always lost, which is normal Fluxor behaviour.

This would mean that InteractiveAuto (if it actually worked) wouldn't really work well with Fluxor. The page would load by fetching data from a server side store and then any interactivity on the page would then be handed off to WASM, which would be a different store, so the state would be different.

I see limited uses for InteractiveAuto anyway, so I wouldn't say this was a "show-stopper".

@SethVanderZanden
Copy link

I don't think InteractiveAuto actually works, see the following link:

dotnet/aspnetcore#52154

All my tests show that it just does pre-rendering. I'm not sure if this affects the use of the <Fluxor.Blazor.Web.StoreInitializer /> component or not though.

In my tests I'm going with the @Rhywden suggestion of using the store initializer from a component in the client project with interactive auto rendering but it only initializes once, either on the server or the client. @SethVanderZanden how are you initializing the store if you have it working in both places?

In routes.razor w rendermode as InteractiveServer. The base functionality will work fine for InteractiveAuto. The base functionality of Fluxor works the same on both interactiveserver and interactivewebassembly to my knowledge, correct me if I'm wrong. Just like in the past I don't recall setting up Fluxor differently or having 2 different set of code to work on Server or WebAssembly, they all store state within the tab and is lost on refresh no matter the rendering method.

@stagep
Copy link

stagep commented Jan 3, 2024

I updated my sample. Counters on both Server and WebAssembly work. State will be lost on the Server counters once you switch to using only WebAssembly as the connection to the server is cleaned up (this is to be expected). There are 2 separate Server counters (Server Counter and Server Double Counter) that you can switch between and see that they maintain state. A couple of points:

  • Prerendering on WebAssembly cannot be used
  • StoreInitializer is required on every interactive page

https://github.com/stagep/Blazor8WithFluxor

@orosbogdan
Copy link

It looks like the Store can only be initialized once, either WASM or Server. If InteractiveAuto is used and only InteractiveServer pages are used, the store is initialized as InteractiveServer and then InteractiveAuto components can't use the store. If only InteractiveWebAssembly pages are used the store is initialized as InteractiveWebAssembly and then InteractiveServer pages don't work.

It seems like some work is required to initialize the store once but in a way that both WASM and Server components can use it.

I'm facing the same issue as above.

@janusqa
Copy link

janusqa commented Feb 27, 2024

@mrpmorris can you elaborate?
The question is how can we get fluxor to work with the new blazor web app template.
In this template there is a server project and a client project. I would like to manage client state with fluxor in the client project.

Oringally there was an App.razor file in the client project of the old templates. With the new template there is no App.razor file, and no mention in the documentation on where to place <Fluxor.Blazor.Web.StoreInitializer /> in the new blazor web app template.
Placing it on every page does not seem like a sustainable or maintainable solution.

Is it that the new template is simply not supported at this time?

@janusqa
Copy link

janusqa commented Feb 27, 2024

All my routable components I have decided I will keep in /Pages
I've created a Layout folder at /Layout. In there I have created a ClientLayout.razor
In /Pages I've created a _Imports.razor and in it placed @layout ClientLayout. This means any page in the client project will automatically load with this template. It seems to be a good place to put <Fluxor.Blazor.Web.StoreInitializer /> with out having to put in on multiple pages.
This seems to be working for me now after i adjusted it to be <Fluxor.Blazor.Web.StoreInitializer @rendermode="new InteractiveWebAssemblyRenderMode(prerender: false)" />
I only want that component to load in the WebAssmbly without being per-rendered first on the server,

Thanks to @stagep for pointing me to putting the render mode on the component to ensure it only loads on client side.
A disadvantage is that the static pages and the assembly pages have two different layouts but am sure there is a way to reconcile that.

@pingu2k4
Copy link

pingu2k4 commented Mar 6, 2024

Hey - Have only just now started thinking about this, haven't had a play with the idea yet but thought I would give my first thoughts...

  • Initialise a store on the server only.
  • Each page should be on the server, no routable pages on the client
  • Anything that needs wasm interactivity goes into the client project as a component - if the entire page wants interactivity, then the routable page still lives in the server project, but just brings in the wasm component right away.
  • Any state needed by a wasm component, can be passed into the component from server
  • Dispatching from wasm will need to be passed back as an EventCallback or something to the server
  • Could look at building an interface around this....

Again, its just a first thought, I've not tried it but will be looking into this when I do get a chance...

@mrpmorris
Copy link
Owner

@Rhywden Is your intention to only use it in WASM?

@WhitWaldo
Copy link

I've created a PR at #481 that fixes the issues with Fluxor (currently only supports .NET 6 or newer) not appearing to work on both the server/web assembly (pre-render or not), but also added a persistence mechanism so state is maintained whether the user transitions between either render mode.

There's some smaller work remaining as described on the PR comments:

  • Brief flicker when rehydrating state - this is because my implementation uses session storage which isn't available until OnAfterRenderAsync, which means the page is rendered, the rehydration triggered and the store updated. Other persistence implementations may be able to shift this earlier, but that'd require a change to determining which method should be used.
  • Currently saves state when the location change event is thrown (as I've only observed a shift in render mode when switching pages) to limit unnecessary persist operations
  • Again, currently only compatible with .NET 6 or newer because of my use of newer features of System.Text.Json.

Demonstration (using .NET 8) available here.

@Rhywden
Copy link
Author

Rhywden commented Mar 28, 2024

@Rhywden Is your intention to only use it in WASM?

Sorry for the late reply - currently I'm only using it for the client side. The components which rely on Fluxor also make heavy use of interactivity like click events and thus don't really work server-rendered anyway ;)

@Rhywden
Copy link
Author

Rhywden commented Mar 28, 2024

I've created a PR at #481 that fixes the issues with Fluxor (currently only supports .NET 6 or newer) not appearing to work on both the server/web assembly (pre-render or not), but also added a persistence mechanism so state is maintained whether the user transitions between either render mode.

There's some smaller work remaining as described on the PR comments:

  • Brief flicker when rehydrating state - this is because my implementation uses session storage which isn't available until OnAfterRenderAsync, which means the page is rendered, the rehydration triggered and the store updated. Other persistence implementations may be able to shift this earlier, but that'd require a change to determining which method should be used.
  • Currently saves state when the location change event is thrown (as I've only observed a shift in render mode when switching pages) to limit unnecessary persist operations
  • Again, currently only compatible with .NET 6 or newer because of my use of newer features of System.Text.Json.

Demonstration (using .NET 8) available here.

You might consider using PersistentComponentState which is explicitly meant to adress the issue of double renders. It's also the way the Blazor WebApp hands over login data if you enable authentication (not the actual auth data - that's done using cookies - but stuff like the username or email)

@WhitWaldo
Copy link

WhitWaldo commented Apr 1, 2024

@Rhywden I've been playing around with PersistentComponentState and while it's an interesting tool, I don't know if it's necessary here. The only reason for the double renders was that I was originally persisting the data to session/local storage and either one required waiting for IJSRuntime on the page, which was only available after the page rendered, resulting in that double flicker.

Unfortunately, it doesn't appear I can write to the PersistentComponentState just anytime, but rather only as part of the event callback, so while a potentially nifty tool, that is a bit of a limitation given that the user may want to persist their state beyond just these render mode switch events, meaning that at this in this circumstance, Fluxor's state can't really be persisted there.

And when the data is being persisted to another state store (e.g. Redis, Cosmos, etc.) after each set of non-setup actions, you don't know why the rehydration is happening, so even if you were using the PersistentComponentState, it may well be out of sync with the Fluxor state if the last persistence action happened absent a render mode change.

So neat tool, especially in the use case described in the blog post you linked to (e.g. one-time data per circuit that need only be retrieved at startup), but I don't think it's a good fit here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests