mrpmorris/Fluxor

Reducer not getting invoked in .NET 8 Blazor

WhitWaldo opened this issue · 4 comments

I recently migrated several websites to .NET 8's new Blazor Web App approach but swiftly noticed that my Fluxor-powered states weren't working as expected. I think I've narrowed it down to the reducer methods not being invoked as expected, though I'm not yet sure why, but I wanted to post this issue in case this is an issue someone else has experienced and could shed some light on.

I put together a really simple .NET 8 Blazor Web App aligned with the Blazor 01 tutorial just to start there to see if I could get a state with a reducer and action working and while registration appears to work as it should, when I click the button to increment the counter the Dispatcher.Dispatch method is called and.. that appears to be that. The breakpoint in the expected reducer never hits so the state never updates.

I've tried two approaches to the registration and neither one is throwing any issues.

Approach 1 - FeatureState attribute

[FeatureState]
public sealed record CounterState
{
    public int ClickCount { get; init; }

    public CounterState(int clickCount)
    {
        ClickCount = clickCount;
    }

    public CounterState()
    {
    }
}

This appears to pick up without issue as expected.

Approach 2 - Feature class

public sealed record CounterState(int ClickCount)
{
}

public class CounterFeature : Feature<CounterState>
{
    public override string GetName() => "CounterState";

    protected override CounterState GetInitialState() => new(0);
}

Again, no obvious issues about this not registering as it should. I've tried the reducer in both formats:

[ReducerMethod]
public static CounterState ReduceIncrementCounterAction(CounterState state, IncrementCounterAction action)
{
    return new CounterState(state.ClickCount + 1);
}

//and

[ReducerMethod(typeof(IncrementCounterAction))]
public static CounterState ReduceIncrementCounterAction(CounterState state)
{
    return new CounterState(state.ClickCount + 1);
}

I can put a breakpoint on the return statement and the debugger will never stop on it, but it's not clear what's changed between Blazor 7 and Blazor 8 that would have suddenly broken this. I've assembled a sample repo here.

Any ideas of how to proceed? Thanks!

Interestingly, I attached the Fluxor symbols to the debugger to step through and understand what's happening. In Store.cs, the QueuedActions queue is receiving all these actions as expected, but it never elects to dequeue any of them, meaning nothing is processed.

HasActivatedStore is false, so perhaps something has changed about how Blazor is indicating when a page has finished loading that's causing the issue - per the comments a few lines down, it's only when the page finishes loading that DequeueActions is called.

image

I think the issue lies with the use of OnAfterRenderAsync in the StoreInitializer. If I set a breakpoint on the OnInitialized method, it breaks and I can step through it without an issue. But HasActivatedStore is only set to true when Store.InitializeAsync() is called and this only happens during OnAfterRenderAsync which doesn't ever break for me, so all the actions accumulate and are never processed, yielding the effect I've been observing.

I took at look at the after component render documentation to get a sense of what changed here between .NET 7 and .NET 8 as I couldn't remember anything off the top of my head about it.

Interestingly, the .NET 8 docs differ from the .NET 7 docs is the added note that "these methods aren't invoked during rendering on the server". As I understand it, the Auto rendering starts on the server, finishes the various lifecycles necessary, then starts to download the WASM client at which point it switches contexts, but that suggests that this lifecycle method is never called in that case as it's always running on the server initially.

While this appears to continue to be the best place to put the JS invocations, I would urge you to consider moving the await Store.InitializeAsync() out of this method. As OnParametersSet is now called before OnInitialized (in .NET 7 it was flipped), I might suggest moving this line into the end of the OnInitializedAsync method instead as that's always called on either the server or client rendering modes, is called after the existing OnInitialized method implementation and happens after OnParametersSet now, making it the latest lifecycle method before OnAfterRender method invocation (which again, doesn't happen on server anymore).