Repository: dotnet/aspnetcore
File: src/Components/Components/src/RenderTree/Renderer.cs (C#)

Code before:
                var baseException = task.Exception.GetBaseException();
                HandleExceptionViaErrorBoundary(baseException, owningComponentState);
                break;
            default:
                // It's important to evaluate the following even if we're not going to use
                // handledErrorTask below, because it has the side-effect of calling HandleException.
                var handledErrorTask = GetErrorHandledTask(task, owningComponentState);
                AddPendingTask(owningComponentState, handledErrorTask);
                break;
        }
    }

    /// <summary>
    /// Notifies the renderer that there is a pending task associated with a component. The
    /// renderer is regarded as quiescent when all such tasks have completed.
    /// </summary>
    /// <param name="componentState">The <see cref="ComponentState"/> for the component associated with this pending task, if any.</param>
    /// <param name="task">The <see cref="Task"/>.</param>
    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.
        AddPendingTask(task);
    }

    internal void AddPendingTask(Task task)
    {
        // The pendingTasks collection is only used during prerendering to track quiescence,
        // so will be null at other times.
        if (_pendingTasks is { } tasks)
        {
            Dispatcher.AssertAccess();
            tasks.Add(task);
        }
    }

    internal void AssignEventHandlerId(int renderedByComponentId, ref RenderTreeFrame frame)
    {
        var id = ++_lastEventHandlerId;

        if (frame.AttributeValueField is EventCallback callback)
        {
            // We hit this case when a EventCallback object is produced that needs an explicit receiver.
            // Common cases for this are "chained bind" or "chained event handler" when a component
            // accepts a delegate as a parameter and then hooks it up to a DOM event.
            //
            // When that happens we intentionally box the EventCallback because we need to hold on to
            // the receiver.
            _eventBindings.Add(id, (renderedByComponentId, callback, frame.AttributeName));
        }
        else if (frame.AttributeValueField is MulticastDelegate @delegate)

Review comment (suggestion):
This is adding public API.

We can achieve the same effect in a slightly different way without having to add an additional overload nor additional API to RenderHandle.

The issue is that the router doesn't properly notify that it is still doing work, so we reach quiescence without the router being done (and hence we miss on later updates).

Quiescence is tracked whenever we call `SetParametersAsync`. Instead of doing things this way, we can keep whatever task we need to pass in a field, and on the call to `SetParametersAsync` we do something like
```csharp
if (!_renderHandle.RendererInfo.IsInteractive && _taskWeAddedCallingAddPendingTask != null) 
{
  await _taskWeAddedCallingAddPendingTask;
}
```

That will have the effect of having that task included in the quiescence computation (as SetParametersAsync won't complete until that task completes).

Code after:
                var baseException = task.Exception.GetBaseException();
                HandleExceptionViaErrorBoundary(baseException, owningComponentState);
                break;
            default:
                // It's important to evaluate the following even if we're not going to use
                // handledErrorTask below, because it has the side-effect of calling HandleException.
                var handledErrorTask = GetErrorHandledTask(task, owningComponentState);
                AddPendingTask(owningComponentState, handledErrorTask);
                break;
        }
    }

    /// <summary>
    /// Notifies the renderer that there is a pending task associated with a component. The
    /// renderer is regarded as quiescent when all such tasks have completed.
    /// </summary>
    /// <param name="componentState">The <see cref="ComponentState"/> for the component associated with this pending task, if any.</param>
    /// <param name="task">The <see cref="Task"/>.</param>
    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.
        if (_pendingTasks is { } tasks)
        {
            Dispatcher.AssertAccess();
            tasks.Add(task);
        }
    }

    internal void AssignEventHandlerId(int renderedByComponentId, ref RenderTreeFrame frame)
    {
        var id = ++_lastEventHandlerId;

        if (frame.AttributeValueField is EventCallback callback)
        {
            // We hit this case when a EventCallback object is produced that needs an explicit receiver.
            // Common cases for this are "chained bind" or "chained event handler" when a component
            // accepts a delegate as a parameter and then hooks it up to a DOM event.
            //
            // When that happens we intentionally box the EventCallback because we need to hold on to
            // the receiver.
            _eventBindings.Add(id, (renderedByComponentId, callback, frame.AttributeName));
        }
        else if (frame.AttributeValueField is MulticastDelegate @delegate)
        {
            // This is the common case for a delegate, where the receiver of the event
            // is the same as delegate.Target. In this case since the receiver is implicit we can
            // avoid boxing the EventCallback object and just re-hydrate it on the other side of the
            // render tree.
            _eventBindings.Add(id, (renderedByComponentId, new EventCallback(@delegate.Target as IHandleEvent, @delegate), frame.AttributeName));
        }