Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

await inside OnInitAsync causes the browser to hang #15463

Closed
rodolfograve opened this issue Nov 19, 2018 · 4 comments
Closed

await inside OnInitAsync causes the browser to hang #15463

rodolfograve opened this issue Nov 19, 2018 · 4 comments
Labels
area-blazor Includes: Blazor, Razor Components

Comments

@rodolfograve
Copy link

Similar to aspnet/Blazor#1242 but I can consistently reproduce and it happens on the simplest Component. No Layout involved. Happened on 0.6.0 and kept happening after I upgraded to 0.7.0.

It is a very strange behavior which points to some mishandling of asynchronous actions inside OnInitAsync.

Version that behaves as expected

The code below fails as expected. A System.NullReferenceException is shown in the browser console.

@page "/"
<div>
    @foreach (var item in Items)
    {
        <div>@item</div>
    }
</div>

@functions {
    protected IEnumerable<string> Items { get; set; }
}

Result: Uncaught (in promise) Error: System.NullReferenceException: Object reference not set to an instance of an object.

Version that works as expected

@page "/"
<div>
    @foreach (var item in Items)
    {
        <div>@item</div>
    }
</div>

@functions {
    protected override async Task OnInitAsync()
    {
        await Task.CompletedTask;
        Items = new[] { "item1" };
    }

    protected IEnumerable<string> Items { get; set; }
}

Result: item1 is shown on the screen.

Version that unexpectedly hangs up

@page "/"
<div>
    @foreach (var item in Items)
    {
        <div>@item</div>
    }
</div>

@functions {
    protected override async Task OnInitAsync()
    {
        await Task.Delay(1); // This line is the only difference
        Items = new[] { "item1" };
    }

    protected IEnumerable<string> Items { get; set; }
}

Result: An exception is shown in the console (which I wasn't expecting), and from that point on the browser becomes unresponsive and I need to close the tab and re-run the Blazor application.

Uncaught (in promise) Error: System.NullReferenceException: Object reference not set to an instance of an object.

@Andrzej-W
Copy link

You have to understand that await in async method means "if the task is not completed temporarily suspend execution of this function and return to the caller (in this case Blazor runtime)". Blazor calls your OnInitAsync method and renders your page first time immediately after first suspension. Then it renders your page again when your method finishes. Run this code and watch the screen. Then comment first await, uncomment await Task.CompletedTask; and note the difference.

@page "/"
<div>
    @if (Items == null)
    {
        <div>List not initialized yet.</div>
    }
    else
    {
        @foreach (var item in Items)
        {
            <div>@item</div>
        }
    }
</div>
@functions {
    List<int> Items { get; set; }

    protected override async Task OnInitAsync()
    {
        await Task.Delay(1000);
        //await Task.CompletedTask;
        Items = new List<int>();
        Items.Add(1);
        await Task.Delay(1000);
        Items.Add(2);
        await Task.Delay(1000);
        Items.Add(3);
    }
}

@thewebchameleon
Copy link

try instantiate a new instance of Items first

@functions {
    List<int> Items { get; set; }

    protected override void OnInit()
    {
        Items = new List<int>();
    }

    protected override async Task OnInitAsync()
    {
        await Task.Delay(1000);
        Items.Add(1);
        await Task.Delay(1000);
        Items.Add(2);
        await Task.Delay(1000);
        Items.Add(3);
    }
}

@Andrzej-W
Copy link

@thewebchameleon your solution is based on some Blazor implementation detail and if you really want to create a new List instance before the page is rendered the first time you can simply move this line above the first await. You can also do this:

List<int> Items { get; set; } = new List<int>();

Both solutions are in my opinion much better than a new function override.

The most important fact to remember is: each time Blazor calls your async method your page can be rendered two times and your rendering code have to be prepared for this. This is true not only in OnInitAsync but also in all other async methods, for example in button's onclick event.
Second important fact: your async method has to have Task return type not void. Otherwise you page will not be rendered second time.

@rodolfograve
Copy link
Author

Thank you all for your quick response. I was wondering if this behavior was by-design but it looked bad enough that I thought it wasn't. Finding aspnet/Blazor#1242 didn't help either.

Is there any chance you could include this explanation in the documentation. I went through a lot and couldn't find a single mention of this behavior, plus all the examples I could find are simplistic and don't attempt to load data asynchronously which is realistic and what I was trying to do:

Blazor calls your OnInitAsync method and renders your page first time immediately after first suspension. Then it renders your page again when your method finishes

Keep up the great work. Blazor is an amazing idea!

@mkArtakMSFT mkArtakMSFT transferred this issue from dotnet/blazor Oct 27, 2019
@mkArtakMSFT mkArtakMSFT added the area-blazor Includes: Blazor, Razor Components label Oct 27, 2019
@ghost ghost locked as resolved and limited conversation to collaborators Dec 3, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-blazor Includes: Blazor, Razor Components
Projects
None yet
Development

No branches or pull requests

4 participants