In blazor wasm (v6), I have an app-wide state container (AppState.razor
) as a cascading parameter.
The typical code is:
AppState.razor
<CascadingValue Value="this">
@ChildContent
</CascadingValue>
@code {
[Parameter] public RenderFragment ChildContent { get; set; }
private bool _isFoo;
public bool IsFoo {
get {
return _isFoo;
}
set {
_isFoo = value;
StateHasChanged();
//await SomeAsyncMethod(); // <----------
}
}
//...
}
App.razor
<AppState>
<Router>
...
</Router>
</AppState>
MyComponent.razor
<!--- markup --->
@code {
[CascadingParameter] public AppState AppState { get; set; }
//...
}
After a component sets the AppState.IsFoo
property to update the state (and update the UI), I must call an async method (e.g. save to localstorage). But I cannot do that in a sync property setter.
I could change from a cascading parameter to an injectable service, but I prefer not to.
I may need to redesign - what is the typical approach for this use case? (I've seen code with InvokeAsync(SomeAsyncMethod)
without an await
but I'm wary of that.)
Here's a different solution that I think resolves most of the issues in your implementation. It loosely based on the way EditContext
works.
First separate out the data from the component. Note the Action
delegate that is raised whenever a parameter change takes place. This is basically the StateContainer
in the linked MSDocs article.
public class SPAStateContext
{
private bool _darkMode;
public bool DarkMode
{
get => _darkMode;
set
{
if (value != _darkMode)
{
_darkMode = value;
this.NotifyStateChanged();
}
}
}
public Action? StateChanged;
private void NotifyStateChanged()
=> this.StateChanged?.Invoke();
}
Now the State Manager Component.
SPAStateContext
not the component itself which is far safer (and cheaper).StateChanged
. This can be async as the invocation is fire and forget.@implements IDisposable
<CascadingValue Value=this.data>
@ChildContent
</CascadingValue>
@code {
private readonly SPAStateContext data = new SPAStateContext();
[Parameter] public RenderFragment? ChildContent { get; set; }
protected override void OnInitialized()
=> data.StateChanged += OnStateChanged;
private Action? StateChanged;
// This implements the async void pattern
// it should only be used in specific circumstances such as here in a fire and forget event handler
private async void OnStateChanged()
{
// Do your async work
// In your case do your state management saving
await SaveStateToLocalStorage();
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
await LoadStateFromLocalStorage();
}
public void Dispose()
=> data.StateChanged -= OnStateChanged;
}