Aspnetcore: Calling JsRuntime.Current.InvokeAsync from C# in HTML part of a component results in Uncaught (in promise)

Created on 4 Oct 2018  路  2Comments  路  Source: dotnet/aspnetcore

Title

Calling JsRuntime.Current.InvokeAsync from C# in HTML part of a component results in Uncaught (in promise)

Functional impact

JSInterop does not work in Async mode. I did manage to get the code to work by down-casting to IJSInProcessRuntime and then calling Invoke instead of InvokeAsync, but this still felt like a bug so I thought I should report it.

I have the following code in a Component which is supposed to show one section of code if a token is present in local storage, and another if not.

@inject AuthService AuthService

<nav class="navbar navbar-expand-md navbar-dark bg-dark">

    @if (IsLoggedIn())
    {
        <div class="dropdown">
            <a class="dropdown-toggle text-light">
                Welcome User
            </a>

            <div class="dropdown-menu">
                <a class="dropdown-item" href="#"><i class="fa fa-user"></i> Edit Profile</a>
                <div class="divider"></div>
                <a class="dropdown-item" href="#"><i class="fa fa-sign-out"></i> Log out</a>
            </div>
        </div>
    }
    else
    {
        <form class="form-inline my-2 my-lg-0" onsubmit="@LogIn"> @*Change to bind-to-oninput when it is fixed*@
            <input class="form-control mr-sm-2" name="username" placeholder="Username" type="text" required bind="@Username" />
            <input class="form-control mr-sm-2" name="password" placeholder="Password" type="text" required bind="@Password" />
            <button class="btn btn-success my-2 my-sm-0" type="submit" disabled="@_loginDisabled">Login</button>
        </form>
    }

</nav>

@functions {
    // Other methods and properties left out for brevity.

    bool IsLoggedIn()
    {
        // Using .Result as await cannot be used in the Razor HTML
        return AuthService.IsLoggedIn().Result;
    }
}

And then inside AuthService.cs

public async Task<bool> IsLoggedIn()
{
    try
    {
        var token = await JSRuntime.Current.InvokeAsync<string>("jsinterop.getLocalStorageItem", TOKEN_KEY);

        return !string.IsNullOrWhiteSpace(token);
    }
    catch (Exception e)
    {
        Console.WriteLine($"public async Task<bool> IsLoggedIn()': {e.Message}");

        return false;
    }    
}

Minimal repro steps

Load the component and look in the console.

Actual result

The following console output:

blazor.webassembly.js:1 WASM: 13
d.printErr @ blazor.webassembly.js:1
blazor.webassembly.js:1 Uncaught (in promise) abort(13). Build with -s ASSERTIONS=1 for more info.

Further technical details

Using Visual Studio 2017 15.8.6 and Blazor 0.6

area-blazor

Most helpful comment

I think the problem that we can not use Task.Result is the limitation of Blazor by design.

I think, your sample code cause "deadlock" like this figure. (from this post)

fig.1

I'll rewrite your sample code as follow.

Initialize the status of logged in or not, only once at the component initialization.

@functions {

    bool IsLoggedIn;

    protected override async Task OnInitAsync()
    {
        this.IsLoggedIn = await AuthService.IsLoggedIn();
    }
}

And reference it.

@inject AuthService AuthService

<nav class="navbar navbar-expand-md navbar-dark bg-dark">

    @if (IsLoggedIn)
    {
        ...

If you have to call JavaScript synchronously, I can provide the information to you about workaround / hack to do it.

See also: https://dev.to/j_sakamoto/workaround-how-to-resolve-local-time-zone-on-blazor-spa-v05x-foi

All 2 comments

I think the problem that we can not use Task.Result is the limitation of Blazor by design.

I think, your sample code cause "deadlock" like this figure. (from this post)

fig.1

I'll rewrite your sample code as follow.

Initialize the status of logged in or not, only once at the component initialization.

@functions {

    bool IsLoggedIn;

    protected override async Task OnInitAsync()
    {
        this.IsLoggedIn = await AuthService.IsLoggedIn();
    }
}

And reference it.

@inject AuthService AuthService

<nav class="navbar navbar-expand-md navbar-dark bg-dark">

    @if (IsLoggedIn)
    {
        ...

If you have to call JavaScript synchronously, I can provide the information to you about workaround / hack to do it.

See also: https://dev.to/j_sakamoto/workaround-how-to-resolve-local-time-zone-on-blazor-spa-v05x-foi

Thanks for the answer, @jsakamoto.

Yes, the basic fact is that the component's rendering logic must be synchronous. This is the desired design, because unlike a server-side HTML rendering process, the user is already looking at your UI and is waiting for it to update. If the rendering could take an arbitrary amount of time, the user would have no idea what was going on. So if you need to perform an async task, you need to model that in your UI. That is, start the async task in one of the lifecycle methods (e.g., OnInitAsync) and use your synchronous rendering to display some kind of "loading" indicator until that task is completed.

So if you want to use synchronous JS interop during rendering that is technically OK, but it might be better still to move it to async JS interop outside the render logic.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

barrytang picture barrytang  路  89Comments

reduckted picture reduckted  路  91Comments

radenkozec picture radenkozec  路  114Comments

MaximRouiller picture MaximRouiller  路  338Comments

moodya picture moodya  路  153Comments