Blazor - Prevent double HTTP call and wait first one

2.1k views Asked by At

In my Blazor application (WebAssembly), I need to load User data in my ApplicationState. Many of my pages needs this User data, so on their OnAfterRenderAsync method, I'm loading User data if it's not loaded :

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (firstRender)
    {
        loadingSession = true;
        if (!sessionState.UserDataLoaded)
            await sessionState.LoadUserData();
        loadingSession = false;

    }
    await base.OnAfterRenderAsync(firstRender);
}

But if I go to an other page quickly, the user data won't be loaded yet, and the page will ask a new request.

So I would like to wait if there is already a request, and not to start a new one.

    public async Task LoadUserData()
    {
        try
        {
            if (dataLoading)
            {
                // Need to wait the previous request
            }

            if (dataLoaded)
                return;

            dataLoading = true;
            User = await httpClient.GetFromJsonAsync<User>("users", options);
        }
        catch (Exception e)
        {
            OnException(e);
        }
        finally
        {
            if (User != null)
                dataLoaded = true;
            dataLoading = false;
        }
    }

I tried many things, ManualResetEvent, Thread.Sleep in a Task,... but I don't manage to do it correcly. What would be the best method ?

1

There are 1 answers

8
CobyC On

OnAfterRenderAsync and OnAfterRender are called after a component has finished rendering.

OnInitializedAsync and OnInitialized are invoked when the component is initialized after having received its initial parameters from its parent component in SetParametersAsync.

Moving your code to the OnInitializedAsync method will load the user data before the control has rendered.

Edit: (you want to pass state between components)

Microsoft documentation on State Management

Another example of keeping state in memory and passing it to components via Dependency Injection.

To make use of 'State management' you have to register a 'Service' that can be used for state management.

For example; Create a class named AppState with the values you would like to keep state of.

public class AppState
{
    public AppState()
    {
        //this class is to help maintain application state.
    }

    public UserData UserData { get; set; }

    public string SomeOtherStateValue { get; set; }
}

then register this class in your Services as Singleton (see Service Lifetimes):

public static async Task Main(string[] args)
{
    var builder = WebAssemblyHostBuilder.CreateDefault(args);
    builder.RootComponents.Add<App>("app");
    builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
   //...other services
   //...

   // add AppState as singleton
    builder.Services.AddSingleton<AppState>(); 

   //...
   //...more stuff
    await builder.Build().RunAsync();
}

Then in your Index.razor file (or wherever you need to call it for the first time) use dependency injection to get access to the AppState and in the OnInitialzedAsync() call the user data using HttpContent.

@page "/"
@using System.Text.Json
@using System.Text
@inject AppState appState
@inject HttpClient httpClient

<h1>..all your html or components on the page...</h1>

@code{

protected override async Task OnInitializedAsync()
{
    var userRequestData = new UserDataRequest(options);
    var request = new StringContent(JsonSerializer.Serialize(userRequestData), Encoding.UTF8, "application/json");

    var response = await httpClient.PostAsync($"api/users/", request);
    if (response.IsSuccessStatusCode)
    {
        appState.UserData = await JsonSerializer.DeserializeAsync<UserData>(await response.Content.ReadAsStreamAsync(), new JsonSerializerOptions() { PropertyNameCaseInsensitive = true });
    }

    await base.OnInitializedAsync();
}

}

This will await the call to the API until it has received a response. If the response was successful ie: response.IsSuccessStatusCode is true. extract the data from the response message and store it in the appState.UserDate.

In any subsequent component you can get the AppState by injecting it using @inject AppState appState or if it is in a code file [Inject] AppState appState;

In the subsequent components you can add a check in the OnInitializedAsync method :

.
.
if (appState?.UserData == null)
{
    //appData.UserData = await request user data...
}
// more code..
.
.

!! This is not how authentication should be handled though !!

If you need authentication look at ASP.NET Core Blazor authentication and authorization as it makes use of AuthenticationStateProvider.

In regards to checking if the call has already been made to the http client, the await should wait for the response from the httpClient before continuing.

If appState.UserData has some value then the call was successfull..