Skip to content

Fix SSR page rendering intermediate state instead of the end state of components #52823

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

Merged
merged 10 commits into from
Dec 21, 2023
Merged
6 changes: 5 additions & 1 deletion src/Components/Components/src/RenderTree/Renderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -593,7 +593,11 @@ protected virtual void AddPendingTask(ComponentState? componentState, Task task)
{
// The pendingTasks collection is only used during prerendering to track quiescence,
// so will be null at other times.
_pendingTasks?.Add(task);
if (_pendingTasks is { } tasks)
{
Dispatcher.AssertAccess();
tasks.Add(task);
}
}

internal void AssignEventHandlerId(int renderedByComponentId, ref RenderTreeFrame frame)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ await EndpointHtmlRenderer.InitializeStandardComponentServicesAsync(
return;
}

await Task.WhenAll(_renderer.NonStreamingPendingTasks);
await _renderer.WaitForNonStreamingPendingTasks();
}
catch (NavigationException ex)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,28 @@ private async Task WaitForResultReady(bool waitForQuiescence, PrerenderedCompone
}
else if (_nonStreamingPendingTasks.Count > 0)
{
// Just wait for quiescence of the non-streaming subtrees
await Task.WhenAll(_nonStreamingPendingTasks);
await WaitForNonStreamingPendingTasks();
}
}

public Task WaitForNonStreamingPendingTasks()
{
return NonStreamingPendingTasksCompletion ??= Execute();

async Task Execute()
{
while (_nonStreamingPendingTasks.Count > 0)
{
// Create a Task that represents the remaining ongoing work for the rendering process
var pendingWork = Task.WhenAll(_nonStreamingPendingTasks);

// Clear all pending work.
_nonStreamingPendingTasks.Clear();

// new work might be added before we check again as a result of waiting for all
// the child components to finish executing SetParametersAsync
await pendingWork;
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ protected override void AddPendingTask(ComponentState? componentState, Task task
}

// For tests only
internal List<Task> NonStreamingPendingTasks => _nonStreamingPendingTasks;
internal Task? NonStreamingPendingTasksCompletion;

protected override Task UpdateDisplayAsync(in RenderBatch renderBatch)
{
Expand Down
6 changes: 4 additions & 2 deletions src/Components/Endpoints/test/RazorComponentResultTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -336,10 +336,11 @@ public async Task StreamingRendering_IsOffByDefault_AndCanBeEnabledForSubtree()
{
// Arrange
var testContext = PrepareVaryStreamingScenariosTests();
var initialOutputTask = Task.WhenAll(testContext.Renderer.NonStreamingPendingTasks);
var initialOutputTask = testContext.Renderer.NonStreamingPendingTasksCompletion;

// Act/Assert: Even if all other blocking tasks complete, we don't produce output until the top-level
// nonstreaming component completes
Assert.NotNull(initialOutputTask);
testContext.WithinNestedNonstreamingRegionTask.SetResult();
await Task.Yield(); // Just to show it's still not completed after
Assert.False(initialOutputTask.IsCompleted);
Expand Down Expand Up @@ -368,10 +369,11 @@ public async Task StreamingRendering_CanBeDisabledForSubtree()
{
// Arrange
var testContext = PrepareVaryStreamingScenariosTests();
var initialOutputTask = Task.WhenAll(testContext.Renderer.NonStreamingPendingTasks);
var initialOutputTask = testContext.Renderer.NonStreamingPendingTasksCompletion;

// Act/Assert: Even if all other nonblocking tasks complete, we don't produce output until
// the component in the nonstreaming subtree is quiescent
Assert.NotNull(initialOutputTask);
testContext.TopLevelComponentTask.SetResult();
await Task.Yield(); // Just to show it's still not completed after
Assert.False(initialOutputTask.IsCompleted);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,21 @@ public async Task CanUseHttpContextRequestAndResponse()
var response = await new HttpClient().GetAsync(Browser.Url);
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
}

[Fact]
public void RendersEndStateOfComponentsOnSSRPage()
{
Navigate($"{ServerPathBase}/ssr-page-that-delays-loading");
Browser.Equal("loaded child", () => Browser.Exists(By.Id("child")).Text);
}

[Fact]
public void PostRequestRendersEndStateOfComponentsOnSSRPage()
{
Navigate($"{ServerPathBase}/forms/post-form-with-component-that-delays-loading");

Browser.Exists(By.Id("submit-button")).Click();

Browser.Equal("loaded child", () => Browser.Exists(By.Id("child")).Text);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<p id="child">@childString</p>

@code {
private string childString = "initial child";

protected override async Task OnInitializedAsync()
{
await Task.Yield();

childString = "loaded child";
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
@if (_loaded)
{
<ChildComponentThatDelaysLoading />
}

@code {
private bool _loaded;

protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
_loaded = await Load();
}

private async Task<bool> Load()
{
await Task.Yield();

return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
@page "/forms/post-form-with-component-that-delays-loading"
@using Microsoft.AspNetCore.Components.Forms

<h3>Post Form With Component That Delays Loading</h3>

@if (_render)
{
<ParentComponentThatDelaysLoading />
}

<form @onsubmit="@(() => _render = true)" @formname="myform" method="post">
<AntiforgeryToken />
<button id="submit-button">Submit</button>
</form>

@code
{
bool _render;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@page "/ssr-page-that-delays-loading"
<h1>SSR page that delays loading</h1>

<ParentComponentThatDelaysLoading />