Closed szalapski closed 2 years ago
Hi
I didn't know that about Blazor State worked in this way.
I'm not sure how Steve (a very good friend of mine) achieves what he did, but I suspect it is because
await
in a method invoked by Blazor, Blazor will immediately re-renderWe can confirm my hypothesis like so
{
// 1 - Change some of the state
await Task.Delay(1000); // This should re-render the Blazor UI
// 2 - Change the state again
await Task.Delay(1000); // This will not re-render the Blazor UI
// 3 - Change the state again
// The async method is complete, so Blazor will render again
}
If this behaves as I suspect it will, then I think you have actually found a bug in Blazor State rather than a feature.
The reducer should return the new state, and then the store notifies all interested parties that it has changed. If the state can be modified (rather than replaced) throughout the method then you are depending on a quirk of the UI (render on first await) to detect the changes rather than the library notifying subscribers there has been a change to state.
As the changes are not being notified to subscribers, then you have to have a call to StateHasChanged before the await in Step 2 in order to see the middle changes, but then you are putting UI logic into your reducer and also tying your logic to a single UI framework. Essentially, your state is getting concerned with rendering details, and that's not a good mix.
I wrote Fluxor according to how I understood the Flux pattern, which is
So, your dispatch should be synchronous and replace state immediately. Any async work needs to be done in the background (after the action has been reduced and the subscribers notified). The effect that does the background task cannot update the state directly, but must dispatch a new action to do so.
I don't know exactly what problem you are trying to solve by updating the state multiple times, so I will imagine a scenario that might need it.
[EffectMethod]
public async Task Handle(IDispatcher dispatcher, GetClientAction action)
{
var financial = await ...get data from finances service...;
var orderHistory = await ...get data from orders service...;
var shippingHistory = await ...get data from shipping service...;
var newChunkOfState = new ChunkOfState(financial, orderHistory, shippingHistory);
dispatcher.Dispatch(new GetClientActionResult(newChunkOfState));
}
But instead of displaying the end result in one go, you want to update the UI as it comes in
[EffectMethod]
public async Task GetFinancialData(IDispatcher dispatcher, GetClientAction action)
{
var result = await ............;
dispatcher.Dispatch(new GetClientActionResult(financialData: result.Value);
}
[EffectMethod]
public async Task GetOrderHistoryData(IDispatcher dispatcher, GetClientAction action)
{
var result = await ............;
dispatcher.Dispatch(new GetClientActionResult(orderHistoryData: result.Value);
}
[EffectMethod]
public async Task GetShippingHistoryData(IDispatcher dispatcher, GetClientAction action)
{
var result = await ............;
dispatcher.Dispatch(new GetClientActionResult(shippingHistoryData: result.Value);
}
The reducer for GetClientAction would null out those three chunks of data from your state. Your GetClientActionResult would only update the state with properties of GetClientActionResult that are not null - so you get an incremental update in the UI.
Your IsLoading state property would be calculated as
public bool IsLoading => FinancialData is null || OrderHistoryData is null || ShippingHistoryData is null;
Without knowing what you are doing it is difficult to advise. I hope this helps?
... // 2 - Change the state again await Task.Delay(1000); // This will not re-render the Blazor UI
If this behaves as I suspect it will, then I think you have actually found a bug in Blazor State rather than a feature.
Yes, confirmed this. You are exactly right: it will rerender on the first await and then again when the method is done, as well as on any other awaits after an intervening call to StateHasChanged--but not on each await generally.
I don't know exactly what problem you are trying to solve by updating the state multiple times...
Your example is pretty close, though mine is simpler: change the state to show a loading spinner before data is retrieved, retrieve data, and change to hide the loading spinner and show the new data.
Yes, I agree that the reducers and effects should not be concerned with refreshing the UI directly.
I will ponder this a bit.
By the way, doing...
Your IsLoading state property would be calculated as
public bool IsLoading => FinancialData is null || OrderHistoryData is null || ShippingHistoryData is null
Is often naive--the spinner might show before loading is even attempted, such as when we are waiting for a condition (or even initial rendering) or perhaps when loading encounters an error. I think loading is almost always part of state that must be managed, otherwise the edge cases bite you.
[ReducerMethod(typeof(GetCustomerAction))]
public static MyState ReduceGetCustomerAction(MyState state)
=> state with { IsLoading = true };
[ReducerMethod]
public static MyState Reduce(MyState state, GetCustomerActionResult action)
=> state with { IsLoading = false, CustomerInfo = action.CustomerInfo };
Then you'd have an effect method that reacts to GetCustomerAction, calls the server, then dispatches data from the server inside a GetCustomerActionResult.
Then you'd have an effect method that reacts to GetCustomerAction, calls the server, then dispatches data from the server inside a GetCustomerActionResult.
This is the way.
You'd have to have a non-null empty state, but a counter will work too.
Closing this. Let me know if you want it reopened.
I am looking for a simpler alternative to introduce to those brand new to state management, maybe as a "gateway" to the full model as you describe. As it is, for the most common case of updating the state both before and after an async API call, I need 2 actions, 2 reducers, and an effect. Is there anything that could be consolidated here? Maybe if so, it could be explained in the documentation that such a "handler" really is just sugar for a combined effect and reducer, and calling them async can result in, at best, one rerender before and one rerender after? It also has the nice effect of not needing a State event or variable to tell us when the action is done, as awaiting the handler takes care of that.
Make the simple things simple and the complicated things possible. You've done the latter; could we consider some way to enable the simple case?
I'm not aware of an approach that does what you want without breaking the rules of the Flux pattern, and I can't think of anything.
If you can find something in Redux or something then I'll happily look.
I still think that idea of providing async "handlers" an alternative to separate effects and reducers has some merit. Yes, they would be useful only for up to two state changes sandwiching one or more await
s, but this strikes me as a very common scenario.
Another way to think of it is that Fluxor has effects that run in the background, but Blazor-State has handlers that run via async-await, giving more exposure of what's happening to components.
(As an aside, I wonder if there would be a way to leverage IAsyncEnumerableawait foreach (_ in multiStepTask) StateHasChanged();
. Just a thought...)
Blazor State isn't the Flux pattern so is free to work differently.
I don't see the benefit of being able to update the state at the beginning and end of an async method but not in the middle. You can do that with an Effect though.
[EffectMethod(typeof(SomeAction))]
public async Task HandleSomeAction(IDispatcher dispatcher)
{
await Something1();
dispatcher.Dispatch(SomeActionUpdate { Name = "Bob" });
await Something2();
dispatcher.Dispatch(SomeActionUpdate { Age = 42 });
await Something3();
dispatcher.Dispatch(SomeActionUpdate { Salutation = "Mr" });
}
Well, isn't one of the most common needs of any single-page-app is change the state, do something that takes noticable time, and then change the state again? Don't the majority of API calls that any site uses follow this pattern?
That's a very common requirement, yes. Flux's pattern is to do that in effects, and to keep reducers synchronous in order to keep state updates predictable, thread safe, and to ensure UI is updated for the user every time you want it to, not just once at the start and once at the end.
Imagine your request streams a large file to a server. You don't just want to show your user 0% then a minute later 100%.
You want your code to notify updates frequently. To do this in Fluxor you'd dispatch progress actions you update the state and the UI would respond each time.
In an async reducer you'd either have to call the UI to tell it to update, or allow the reducer to dispatch further events before it has itself finished. This introduces race conditions on the state and you can end up with inconsistent state.
"Imagine your request streams a large file to a server. You don't just want to show your user 0% then a minute later 100%."
Sure, but that is the rarer case, and for that, I agree the standard flux-compliant approach should remain--and even remain as the "main" way you show in the README and any other documentation.
But I am just advocating for the common case, when the server interactions take a few seconds or less, to have a simpler alternative. You have made the complex things possible, but you haven't yet made the simple things simple.
It is possible, you can have an effect dispatch multiple actions rather than have a reducer update state multiple times in an impure function.
I agree it is possible, however, it isn't simple. I think I will avoid this for now, as I am concerned that the complexity of introducing state management to my team as a concept along with the complexity of having to code the state plus five other parts to make one property change is too much complexity for them. But thanks for the dialog about it.
Is your app something that shows many different views of the same data, or has many different ways of editing the same entity?
If not, then Flux probably isn't the approach next suited to your needs.
Not many views of many properties, but sometimes two views of some properties.
@szalapski Very interesting discussion, and agree that there are many moving parts to do simple things. (But also realise that it's generally necessary.) What did your team eventually do - did you team adopt the fluxor / "blazor-state" / some other way of doing things?
I think it is important to note that state management in general is not necessarily a best practice, just one option on how to do things. We've introduced a global "AppState" cascading parameter, but not much goes into it. It isn't the Redux pattern. We try to rely on injectible things with "Scoped" lifetime instead.
Thanks. So basically the way promoted on the MS docs site - sharing state via services and parameters.
That's how I was doing it until I found fluxor. Now I'm considering different approaches. Flux is amazing stuff, but for a small project there's a lot of boilerplate.
(No offence mrpmorris, I think this library is excellent! It's just important to know when to use it, and that's something I'm not yet able to do - I'm still a newbie!)
No offence taken, I mostly don't use this pattern myself.
I would consider it for an app in which there are many ways to view/modify the same data (e.g. gmail) but not for apps that have lots of entities that you perform CRUD operations on in isolation.
Thanks for that insight - suddenly "it just clicked" for me. :)
I'm going to slog through it anyway (despite the unnecessary burden on a small project) as I think it's a useful skill to have. Enjoying it so far, thank you. And blazor-university is an incredibly useful site, thanks for that too (the MS docs site is great, but gosh is it verbose!)
(PS: if you don't typically use it, do you also use the services/cascades promoted by MS?)
Can you give me a link to some example docs?
For simple stuff that I just need to persist across pages (e.g. search results) I just create a class and register it as scoped. Then I inject that into my component and update it.
After implementing the same state management using both Fluxor and Blazor-State, it strikes me that the most significant difference is that Blazor-State combines the notions of Reducers and Effects into one thing, called "Handlers". In Blazor-State, a handler is by default asynchronous, so that you can dispatch (called "Send" in Blazor-State) an action, then its handler can modify the state, then call out to a web service or other "outside" thing asynchronous, then modify the state again. So, for a routine action that gets data from a service, in Blazor-State, I need only one action and one handler. The caller can do things while the handler starts running, then
await
the single dispatch call when we want to react to it. On the await, the component can (will) rerender with the newly changed state (e.g. to show a Loading spinner based on the state) and then after the await completes, the component can (will) rerender with the newly changed state (e.g. to remove the spinner and show the new data from the state).Of course, Fluxor has separate Effects and Reducers, so to do the same thing in Fluxor, I need two separate actions and thus two reducers, and an effect: One parameterless action that results in a reducer and invokes the Effect, then the Effect calls an outside service in a background task that my component can't see, then the effect dispatches a second action, whose reducer can change the state with the newly received data.
My naive look at things makes me appreciate the simpler model of combined effects and reducers--I don't see what the added complexity of separating them gives me as a benefit.
Have you thought through the concept of enabling asynchronous effects or reducers? Did you avoid them in Fluxor with a design purpose in mind? Or was it just the first way to do it? Would the ability to combine Reducers and Effects into asynchronously-dispatched action handlers be welcome as an alternative way to do things?
The idea of introducing the simpler way to my team is quite appealing, though it seems in other ways Fluxor is the better library. But I'm guessing I'm missing something. Any insight you have time to share would be appreciated.
(Note: in Blazor-State the state is mutable by handlers, rather than purely immutable. I think this is rather irrelevant to my question, as it isn't how to get to a new state that matters here.)