microsoft / microsoft/fluentui-blazor
`FluentAutocomplete` renders an empty dropdown when used on a prerendered InteractiveServer page that hydrates over long-polling
Nobody has claimed this yet.
- Dominant language
- C#
- Stars
- 4.8k
- Forks
- 483
- Avg merge
- 14h 41m
- Merged PRs (30d)
- 68
Description
A FluentAutocomplete<T> whose item list is loaded asynchronously in OnInitializedAsync reliably renders zero options in its dropdown when all of these conditions hold:
- The host page uses
@rendermode InteractiveServerwithprerender: true(the default). - The browser falls back from WebSocket to HTTP long-polling for the SignalR circuit (Edge Tracking Prevention, restrictive corporate proxies, and some home networks all trigger this).
- The item list is non-trivial (~50+ entries) so the async load completes a measurable time after the prerender pass.
Under these conditions the underlying fluent-combobox web component appears to capture its option set during the SSR pass — when the list is still empty — and never re-syncs after hydration completes. The control renders correctly with the FluentUI shell (border, chevron, focus ring) but the dropdown is empty. Typing into the input shows "no matches".
The control is fine on localhost, fine over WebSocket transport, and fine when the host page uses prerender: false — but those three "fine" cases don't all hold simultaneously in production for every user.
Repro
The simplest repro is a picker page that loads its options from the database in OnInitializedAsync.
@page "/repro-picker"
@rendermode InteractiveServer
@inject MyService Svc
<h3>Pick a drug</h3>
<FluentAutocomplete TOption="DrugOption"
Items="@drugs"
OptionText="@(d => d.Name)"
OptionValue="@(d => d.Id.ToString())"
@bind-SelectedOptions="@selected"
Placeholder="Type to search..." />
@code {
private IEnumerable<DrugOption> drugs = Array.Empty<DrugOption>();
private IEnumerable<DrugOption> selected = Array.Empty<DrugOption>();
protected override async Task OnInitializedAsync()
{
// ~50–500 items from a DB call; await is meaningful (not synchronous-completed)
drugs = await Svc.GetAllDrugsAsync();
}
public record DrugOption(int Id, string Name);
}
To force the failure mode in a controlled environment:
- Run the app on a non-
localhosthostname (e.g. a staging URL). - In Microsoft Edge, set Settings → Privacy, search and services → Tracking prevention to Strict, OR open the dev-tools network panel and disable the WebSocket transport (Edge: throttle to "Slow 3G" sometimes also forces fallback; reliable repro is to block
wss:requests via an extension). - Navigate to
/repro-picker. - Open DevTools → Network → filter
_blazor. You should see HTTPnegotiatefollowed by long-polling requests instead of awss://.../_blazorupgrade. - Click the autocomplete. Observe: empty dropdown. Type any letter. Observe: "no matches". Inspect the
fluent-comboboxshadow DOM — there are zerofluent-optionchildren, even though the Blazor render tree shows the items being passed throughItems.
The same page renders the dropdown correctly when:
- Loaded over WebSocket transport (i.e. tracking-prevention disabled, no fallback).
- The page is changed to
prerender: false. - The host page is changed to
@rendermode InteractiveWebAssembly.
Expected behaviour
After hydration, the FluentAutocomplete's underlying fluent-combobox should reflect the post-hydration Items parameter. If hydration delivers a non-empty list that wasn't present at SSR time, the option set should be populated when the user opens the dropdown.
Actual behaviour
The component visually renders, but its option set is whatever was supplied during the prerender pass — empty, in the common case where OnInitializedAsync had not yet completed when the prerender HTML was emitted. Hydration produces no observable effect on the option list. There is no console error and no Blazor circuit error; the page is silently broken.
We've also observed a related symptom: when the prerender pass did complete the async load (faster DB, smaller list), the same control may render ~9 options instead of the expected ~50 — i.e. it captures whatever subset happened to be ready at SSR-emit time and ignores the rest.
Workaround we currently apply
We replaced both of our affected pickers (FluentDrugPicker, FluentGenePicker) with a styled native <select> element. The native <select> correctly reflects post-hydration state regardless of transport or prerender mode. We sacrificed type-ahead filtering, which is acceptable for our list sizes (50–600 entries, alphabetised).
<select class="fdp-select" @bind="SelectedDrugId" @bind:after="OnDrugChanged">
<option value="0">-- select drug --</option>
@foreach (var d in drugs.OrderBy(d => d.Name))
{
<option value="@d.Id">@d.Name</option>
}
</select>
<style>
.fdp-select {
height: 32px;
border: 1px solid #8A8886;
border-radius: 4px;
padding: 0 8px;
background: #fff;
font: inherit;
}
.fdp-select:focus { outline: 2px solid #0078D4; outline-offset: -1px; }
</style>
This works but loses the FluentAutocomplete UX (filter-as-you-type, multi-select, virtualisation).
Other workarounds we considered and rejected:
prerender: falseon every page that uses an autocomplete. Triggers a different problem on the same networks: when the circuit fails to establish, the page stays blank because there's no SSR fallback content to display.- Force the async load to complete before render (e.g. block
OnInitializedAsyncon a synchronous cache). Defeats the point of async loading and doesn't help if the page is reached via cold cache. - Reload the items in
OnAfterRenderAsync(firstRender: true). No effect — the underlying web component still doesn't pick up the new option set.
Why this matters
@rendermode InteractiveServer with prerender: true is the default rendermode produced by the Blazor Web App template. Our team's reasonable expectation is that picking the default rendermode and using a default-styled FluentUI input together should not produce a silently-broken control on a non-trivial percentage of real users' networks. The combination of "Edge tracking-prevention is on by default in many regions" + "long-polling is a normal SignalR fallback" + "prerender:true is the template default" means a baseline FluentUI Blazor app has a meaningful population of users for whom every FluentAutocomplete is empty.
Even if the underlying issue is in @fluentui/web-components rather than Microsoft.FluentUI.AspNetCore.Components, a Blazor-side workaround (e.g. defer rendering the fluent-combobox until first interactive render, or re-set the option set explicitly after hydration) would unblock affected apps.
Environment
- FluentUI Blazor: 4.13.x — 4.14.1 (4.14.1 is current; the bug persists)
- .NET: 9.0
- Hosting: Azure App Service (Windows), Blazor Web App template with hybrid InteractiveServer + InteractiveWebAssembly
- Affected rendermode:
@rendermode InteractiveServerwithprerender: true - Browser: Microsoft Edge (Chromium) with Tracking Prevention = Strict; also reproduced in Chrome on a network where WebSocket upgrade fails and SignalR falls back to long-polling
- Domain pattern: any public hostname; not reproducible on
localhostbecause tracking-prevention and most WebSocket-blocking heuristics exempt it
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with FluentAutocomplete and its underlying fluent-combobox behavior, then reproduce the issue using the /repro-picker page with prerendering enabled and SignalR long-polling. Inspect the shadow DOM before and after hydration; done means post-hydration Items reliably populate the dropdown under the reported transport and prerender conditions.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp
- Domain
- frontend, web-dev
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 42/100